Compare commits
3 Commits
ab4f251f0c
...
6160e91665
| Author | SHA1 | Date | |
|---|---|---|---|
| 6160e91665 | |||
| 08c8c00e28 | |||
| 3fc4384251 |
@@ -48,12 +48,14 @@ Rules are indented (Python-style) under their scope.
|
|||||||
|
|
||||||
Scopes define which cells a rule applies to:
|
Scopes define which cells a rule applies to:
|
||||||
|
|
||||||
| Scope | Syntax | Applies To |
|
| Scope | Syntax | Applies To | Specificity |
|
||||||
|-------|--------|------------|
|
|-------|--------|------------|-------------|
|
||||||
| **Column** | `column <name>:` | All cells in the column |
|
| **Cell (coordinates)** | `cell (<col>, <row>):` | Single cell by position | Highest (1) |
|
||||||
| **Row** | `row <index>:` | All cells in the row |
|
| **Cell (ID)** | `cell <cell_id>:` | Single cell by ID | Highest (1) |
|
||||||
| **Cell (coordinates)** | `cell (<col>, <row>):` | Single cell by position |
|
| **Row** | `row <index>:` | All cells in the row | High (2) |
|
||||||
| **Cell (ID)** | `cell <cell_id>:` | Single cell by ID |
|
| **Column** | `column <name>:` | All cells in the column | Medium (3) |
|
||||||
|
| **Table** | `table "<name>":` | All cells in a specific table | Low (4) |
|
||||||
|
| **Tables** | `tables:` | All cells in all tables (global) | Lowest (5) |
|
||||||
|
|
||||||
**Column scope:**
|
**Column scope:**
|
||||||
|
|
||||||
@@ -97,6 +99,24 @@ cell tcell_grid1-3-2:
|
|||||||
style(background_color="yellow")
|
style(background_color="yellow")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Table scope:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Table by name (must match DataGrid _settings.name)
|
||||||
|
table "products":
|
||||||
|
style("neutral")
|
||||||
|
format.number(precision=2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tables scope (global):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# All tables in the application
|
||||||
|
tables:
|
||||||
|
style(color="#333")
|
||||||
|
format.date(format="%Y-%m-%d")
|
||||||
|
```
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
|
|
||||||
A rule consists of optional **style**, optional **format**, and optional **condition**:
|
A rule consists of optional **style**, optional **format**, and optional **condition**:
|
||||||
@@ -355,10 +375,12 @@ program : scope+
|
|||||||
|
|
||||||
// Scopes
|
// Scopes
|
||||||
scope : scope_header NEWLINE INDENT rule+ DEDENT
|
scope : scope_header NEWLINE INDENT rule+ DEDENT
|
||||||
scope_header : column_scope | row_scope | cell_scope
|
scope_header : column_scope | row_scope | cell_scope | table_scope | tables_scope
|
||||||
column_scope : "column" column_name ":"
|
column_scope : "column" column_name ":"
|
||||||
row_scope : "row" INTEGER ":"
|
row_scope : "row" INTEGER ":"
|
||||||
cell_scope : "cell" cell_ref ":"
|
cell_scope : "cell" cell_ref ":"
|
||||||
|
table_scope : "table" QUOTED_STRING ":"
|
||||||
|
tables_scope : "tables" ":"
|
||||||
column_name : NAME | QUOTED_STRING
|
column_name : NAME | QUOTED_STRING
|
||||||
cell_ref : "(" column_name "," INTEGER ")" | CELL_ID
|
cell_ref : "(" column_name "," INTEGER ")" | CELL_ID
|
||||||
|
|
||||||
@@ -452,6 +474,21 @@ row 0:
|
|||||||
style("neutral", bold=True)
|
style("neutral", bold=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Apply default style to all cells in a table:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
table "products":
|
||||||
|
style("neutral")
|
||||||
|
format.number(precision=2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply global styles to all tables:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
tables:
|
||||||
|
style(font_size="14px")
|
||||||
|
```
|
||||||
|
|
||||||
### Advanced Examples
|
### Advanced Examples
|
||||||
|
|
||||||
**Compare with another column:**
|
**Compare with another column:**
|
||||||
@@ -496,9 +533,17 @@ column status:
|
|||||||
format.enum(source={"draft": "Brouillon", "pending": "En attente", "approved": "Approuvé"}, default="Inconnu")
|
format.enum(source={"draft": "Brouillon", "pending": "En attente", "approved": "Approuvé"}, default="Inconnu")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Complete example - Financial report:**
|
**Complete example - Financial report with hierarchy:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Global styling for all tables
|
||||||
|
tables:
|
||||||
|
style(font_size="14px", color="#333")
|
||||||
|
|
||||||
|
# Table-specific defaults
|
||||||
|
table "financial_report":
|
||||||
|
format.number(precision=2)
|
||||||
|
|
||||||
# Header styling
|
# Header styling
|
||||||
row 0:
|
row 0:
|
||||||
style("neutral", bold=True)
|
style("neutral", bold=True)
|
||||||
@@ -533,6 +578,13 @@ cell (amount, 10):
|
|||||||
style("accent", bold=True)
|
style("accent", bold=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note on hierarchy:** In the example above, for the cell `(amount, 10)`, the styles are applied in this order:
|
||||||
|
1. Cell-specific rule wins (accent, bold)
|
||||||
|
2. If no cell rule, column rules apply (amount formatting + conditional styles)
|
||||||
|
3. If no column rule, row rules apply (header bold)
|
||||||
|
4. If no row rule, table rules apply (financial_report precision)
|
||||||
|
5. If no table rule, global rules apply (tables font size and color)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Autocompletion
|
## Autocompletion
|
||||||
@@ -579,8 +631,9 @@ The DSL editor provides context-aware autocompletion to help users write rules e
|
|||||||
|
|
||||||
| Context | Trigger | Suggestions |
|
| Context | Trigger | Suggestions |
|
||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| **Scope keyword** | Start of line | `column`, `row`, `cell` |
|
| **Scope keyword** | Start of line | `column`, `row`, `cell`, `table`, `tables` |
|
||||||
| **Column name** | After `column ` | Column names from DataGrid |
|
| **Column name** | After `column ` | Column names from DataGrid |
|
||||||
|
| **Table name** | After `table ` | Table name from current DataGrid (_settings.name) |
|
||||||
| **Style preset** | Inside `style("` | Style presets |
|
| **Style preset** | Inside `style("` | Style presets |
|
||||||
| **Style parameter** | Inside `style(... , ` | `bold`, `italic`, `color`, etc. |
|
| **Style parameter** | Inside `style(... , ` | `bold`, `italic`, `color`, etc. |
|
||||||
| **Format preset** | Inside `format("` | Format presets |
|
| **Format preset** | Inside `format("` | Format presets |
|
||||||
@@ -609,6 +662,13 @@ column amount: │ (new line, indent)
|
|||||||
style("error", bold=True) if │ value, col., row., not
|
style("error", bold=True) if │ value, col., row., not
|
||||||
style("error", bold=True) if value │ ==, !=, <, >, in, ...
|
style("error", bold=True) if value │ ==, !=, <, >, in, ...
|
||||||
style("error", bold=True) if value < │ [number input]
|
style("error", bold=True) if value < │ [number input]
|
||||||
|
|
||||||
|
tab │ table, tables
|
||||||
|
table │ [table name from current grid]
|
||||||
|
table "products": │ (new line, indent)
|
||||||
|
|
||||||
|
tables │ tables
|
||||||
|
tables: │ (new line, indent)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1166,12 +1226,14 @@ class DatagridMetadataProvider(Protocol):
|
|||||||
|
|
||||||
| Context | Trigger | Suggestions |
|
| Context | Trigger | Suggestions |
|
||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| `SCOPE_KEYWORD` | Start of non-indented line | `column`, `row`, `cell` |
|
| `SCOPE_KEYWORD` | Start of non-indented line | `column`, `row`, `cell`, `table`, `tables` |
|
||||||
| `COLUMN_NAME` | After `column ` | Column names from DataGrid |
|
| `COLUMN_NAME` | After `column ` | Column names from DataGrid |
|
||||||
| `ROW_INDEX` | After `row ` | First 10 indices + last index |
|
| `ROW_INDEX` | After `row ` | First 10 indices + last index |
|
||||||
| `CELL_START` | After `cell ` | `(` |
|
| `CELL_START` | After `cell ` | `(` |
|
||||||
| `CELL_COLUMN` | After `cell (` | Column names |
|
| `CELL_COLUMN` | After `cell (` | Column names |
|
||||||
| `CELL_ROW` | After `cell (col, ` | First 10 indices + last index |
|
| `CELL_ROW` | After `cell (col, ` | First 10 indices + last index |
|
||||||
|
| `TABLE_NAME` | After `table ` | Table name from current DataGrid (_settings.name) |
|
||||||
|
| `TABLES_SCOPE` | After `tables` | `:` |
|
||||||
| `RULE_START` | Start of indented line (after scope) | `style(`, `format(`, `format.` |
|
| `RULE_START` | Start of indented line (after scope) | `style(`, `format(`, `format.` |
|
||||||
| `STYLE_ARGS` | After `style(` (no quote) | Presets with quotes + named params (`bold=`, `color=`, ...) |
|
| `STYLE_ARGS` | After `style(` (no quote) | Presets with quotes + named params (`bold=`, `color=`, ...) |
|
||||||
| `STYLE_PRESET` | Inside `style("` | Style presets |
|
| `STYLE_PRESET` | Inside `style("` | Style presets |
|
||||||
@@ -1313,16 +1375,18 @@ The following features are excluded from autocompletion for simplicity:
|
|||||||
| DSL Grammar (lark) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/grammar.py` |
|
| DSL Grammar (lark) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/grammar.py` |
|
||||||
| DSL Parser | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/parser.py` |
|
| DSL Parser | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/parser.py` |
|
||||||
| DSL Transformer | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/transformer.py` |
|
| DSL Transformer | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/transformer.py` |
|
||||||
| Scope Dataclasses | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/scopes.py` |
|
| Scope Dataclasses (Column, Row, Cell) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/scopes.py` |
|
||||||
|
| Scope Dataclasses (Table, Tables) | :o: To implement | `src/myfasthtml/core/formatting/dsl/scopes.py` |
|
||||||
| Exceptions | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/exceptions.py` |
|
| Exceptions | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/exceptions.py` |
|
||||||
| Public API (`parse_dsl()`) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/__init__.py` |
|
| Public API (`parse_dsl()`) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/__init__.py` |
|
||||||
| Unit Tests (Parser) | :white_check_mark: ~35 tests | `tests/core/formatting/dsl/test_dsl_parser.py` |
|
| Unit Tests (Parser) | :white_check_mark: ~35 tests | `tests/core/formatting/dsl/test_dsl_parser.py` |
|
||||||
| **Autocompletion** | | |
|
| **Autocompletion** | | |
|
||||||
| DatagridMetadataProvider | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/provider.py` |
|
| DatagridMetadataProvider | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/provider.py` |
|
||||||
| Scope Detector | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` |
|
| Scope Detector (Column, Row, Cell) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` |
|
||||||
| Context Detector | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` |
|
| Scope Detector (Table, Tables) | :o: To implement | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` |
|
||||||
| Suggestions Generator | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/suggestions.py` |
|
| Context Detector | :white_check_mark: Implemented | `FormattingCompletionEngine.py` |
|
||||||
| Completion Engine | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/engine.py` |
|
| Suggestions Generator | :white_check_mark: Implemented | `FormattingCompletionEngine.py` |
|
||||||
|
| Completion Engine | :white_check_mark: Implemented | `FormattingCompletionEngine.py` |
|
||||||
| Presets | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/presets.py` |
|
| Presets | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/presets.py` |
|
||||||
| Unit Tests (Completion) | :white_check_mark: ~50 tests | `tests/core/formatting/dsl/test_completion.py` |
|
| Unit Tests (Completion) | :white_check_mark: ~50 tests | `tests/core/formatting/dsl/test_completion.py` |
|
||||||
| REST Endpoint | :white_check_mark: Implemented | `src/myfasthtml/core/utils.py` → `/myfasthtml/completions` |
|
| REST Endpoint | :white_check_mark: Implemented | `src/myfasthtml/core/utils.py` → `/myfasthtml/completions` |
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
| `col` parameter (row-level conditions) | :white_check_mark: Implemented | |
|
| `col` parameter (row-level conditions) | :white_check_mark: Implemented | |
|
||||||
| `row` parameter (column-level conditions) | :x: Not implemented | |
|
| `row` parameter (column-level conditions) | :x: Not implemented | |
|
||||||
| Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | |
|
| Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | |
|
||||||
|
| **Scope Levels** | | |
|
||||||
|
| Cell scope | :white_check_mark: Implemented | |
|
||||||
|
| Row scope | :white_check_mark: Implemented | |
|
||||||
|
| Column scope | :white_check_mark: Implemented | |
|
||||||
|
| Table scope | :o: To implement | |
|
||||||
|
| Tables scope (global) | :o: To implement | |
|
||||||
| **DataGrid Integration** | | |
|
| **DataGrid Integration** | | |
|
||||||
| Integration in `mk_body_cell_content()` | :white_check_mark: Implemented | `DataGrid.py` |
|
| Integration in `mk_body_cell_content()` | :white_check_mark: Implemented | `DataGrid.py` |
|
||||||
| DataGridFormattingEditor | :white_check_mark: Implemented | `DataGridFormattingEditor.py` |
|
| DataGridFormattingEditor | :white_check_mark: Implemented | `DataGridFormattingEditor.py` |
|
||||||
@@ -32,13 +38,15 @@
|
|||||||
|
|
||||||
This document describes the formatting capabilities for the DataGrid component.
|
This document describes the formatting capabilities for the DataGrid component.
|
||||||
|
|
||||||
**Formatting applies at three levels:**
|
**Formatting applies at five levels:**
|
||||||
|
|
||||||
| Level | Cells Targeted | Condition Evaluated On |
|
| Level | Cells Targeted | Condition Evaluated On | Specificity |
|
||||||
|------------|-------------------------|----------------------------------------------|
|
|------------|-----------------------------------|----------------------------------------------|-------------|
|
||||||
| **Cell** | 1 specific cell | The cell value |
|
| **Cell** | 1 specific cell | The cell value | Highest (1) |
|
||||||
| **Row** | All cells in the row | Each cell value (or fixed column with `col`) |
|
| **Row** | All cells in the row | Each cell value (or fixed column with `col`) | High (2) |
|
||||||
| **Column** | All cells in the column | Each cell value (or fixed row with `row`) |
|
| **Column** | All cells in the column | Each cell value (or fixed row with `row`) | Medium (3) |
|
||||||
|
| **Table** | All cells in a specific table | Each cell value | Low (4) |
|
||||||
|
| **Tables** | All cells in all tables (global) | Each cell value | Lowest (5) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -468,10 +476,12 @@ formatter_presets = {
|
|||||||
### Format Storage Location
|
### Format Storage Location
|
||||||
|
|
||||||
| Level | Storage | Key | Status |
|
| Level | Storage | Key | Status |
|
||||||
|------------|------------------------------|---------|--------|
|
|------------|--------------------------------|---------|--------|
|
||||||
| **Column** | `DataGridColumnState.format` | - | Structure exists |
|
|
||||||
| **Row** | `DataGridRowState.format` | - | Structure exists |
|
|
||||||
| **Cell** | `DatagridState.cell_formats` | Cell ID | Structure exists |
|
| **Cell** | `DatagridState.cell_formats` | Cell ID | Structure exists |
|
||||||
|
| **Row** | `DataGridRowState.format` | - | Structure exists |
|
||||||
|
| **Column** | `DataGridColumnState.format` | - | Structure exists |
|
||||||
|
| **Table** | `DatagridState.table_format` | - | :o: To implement |
|
||||||
|
| **Tables** | `DataGridsManager.all_tables_formats` | - | :o: To implement |
|
||||||
|
|
||||||
### Cell ID Format
|
### Cell ID Format
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ from myfasthtml.controls.helpers import mk, icons
|
|||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
|
from myfasthtml.core.dsls import DslsManager
|
||||||
|
from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import FormattingCompletionEngine
|
||||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||||
|
from myfasthtml.core.formatting.dsl.parser import DSLParser
|
||||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||||
from myfasthtml.core.instances import MultipleInstance
|
from myfasthtml.core.instances import MultipleInstance
|
||||||
from myfasthtml.core.optimized_ft import OptimizedDiv
|
from myfasthtml.core.optimized_ft import OptimizedDiv
|
||||||
@@ -75,6 +78,7 @@ class DatagridState(DbObject):
|
|||||||
self.edition: DatagridEditionState = DatagridEditionState()
|
self.edition: DatagridEditionState = DatagridEditionState()
|
||||||
self.selection: DatagridSelectionState = DatagridSelectionState()
|
self.selection: DatagridSelectionState = DatagridSelectionState()
|
||||||
self.cell_formats: dict = {}
|
self.cell_formats: dict = {}
|
||||||
|
self.table_format: list = []
|
||||||
self.ne_df = None
|
self.ne_df = None
|
||||||
|
|
||||||
self.ns_fast_access = None
|
self.ns_fast_access = None
|
||||||
@@ -97,6 +101,7 @@ class DatagridSettings(DbObject):
|
|||||||
self.open_file_visible: bool = True
|
self.open_file_visible: bool = True
|
||||||
self.open_settings_visible: bool = True
|
self.open_settings_visible: bool = True
|
||||||
self.text_size: str = "sm"
|
self.text_size: str = "sm"
|
||||||
|
self.enable_formatting: bool = True
|
||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
class Commands(BaseCommands):
|
||||||
@@ -206,12 +211,20 @@ class DataGrid(MultipleInstance):
|
|||||||
self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed())
|
self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed())
|
||||||
self._columns_manager.bind_command("UpdateColumn", self.commands.on_column_changed())
|
self._columns_manager.bind_command("UpdateColumn", self.commands.on_column_changed())
|
||||||
|
|
||||||
editor_conf = DslEditorConf()
|
if self._settings.enable_formatting:
|
||||||
|
completion_engine = FormattingCompletionEngine(self._parent, self.get_table_name())
|
||||||
|
editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
|
||||||
|
dsl = FormattingDSL()
|
||||||
self._formatting_editor = DataGridFormattingEditor(self,
|
self._formatting_editor = DataGridFormattingEditor(self,
|
||||||
conf=editor_conf,
|
conf=editor_conf,
|
||||||
dsl=FormattingDSL(),
|
dsl=dsl,
|
||||||
save_state=self._settings.save_state,
|
save_state=self._settings.save_state,
|
||||||
_id="-formatting_editor")
|
_id="-formatting_editor")
|
||||||
|
# register the auto-completion for the formatter DSL
|
||||||
|
DslsManager.register(completion_engine,
|
||||||
|
DSLParser())
|
||||||
|
else:
|
||||||
|
self._formatting_editor = None
|
||||||
|
|
||||||
# other definitions
|
# other definitions
|
||||||
self._mouse_support = {
|
self._mouse_support = {
|
||||||
@@ -220,7 +233,7 @@ class DataGrid(MultipleInstance):
|
|||||||
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(f"DataGrid '{self._get_full_name()}' with id='{self._id}' created.")
|
logger.debug(f"DataGrid '{self.get_table_name()}' with id='{self._id}' created.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _df(self):
|
def _df(self):
|
||||||
@@ -262,7 +275,7 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
def _get_filtered_df(self):
|
def _get_filtered_df(self):
|
||||||
if self._df is None:
|
if self._df is None:
|
||||||
return DataFrame()
|
return None
|
||||||
|
|
||||||
df = self._df.copy()
|
df = self._df.copy()
|
||||||
df = self._apply_sort(df) # need to keep the real type to sort
|
df = self._apply_sort(df) # need to keep the real type to sort
|
||||||
@@ -298,9 +311,6 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state.selection.selected = pos
|
self._state.selection.selected = pos
|
||||||
self._state.save()
|
self._state.save()
|
||||||
|
|
||||||
def _get_full_name(self):
|
|
||||||
return f"{self._settings.namespace}.{self._settings.name}" if self._settings.namespace else self._settings.name
|
|
||||||
|
|
||||||
def init_from_dataframe(self, df, init_state=True):
|
def init_from_dataframe(self, df, init_state=True):
|
||||||
|
|
||||||
def _get_column_type(dtype):
|
def _get_column_type(dtype):
|
||||||
@@ -387,6 +397,8 @@ class DataGrid(MultipleInstance):
|
|||||||
1. Cell-level: self._state.cell_formats[cell_id]
|
1. Cell-level: self._state.cell_formats[cell_id]
|
||||||
2. Row-level: row_state.format (if row has specific state)
|
2. Row-level: row_state.format (if row has specific state)
|
||||||
3. Column-level: col_def.format
|
3. Column-level: col_def.format
|
||||||
|
4. Table-level: self._state.table_format
|
||||||
|
5. Tables-level (global): manager.all_tables_formats
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
col_pos: Column position index
|
col_pos: Column position index
|
||||||
@@ -410,7 +422,11 @@ class DataGrid(MultipleInstance):
|
|||||||
if col_def.format:
|
if col_def.format:
|
||||||
return col_def.format
|
return col_def.format
|
||||||
|
|
||||||
return None
|
if self._state.table_format:
|
||||||
|
return self._state.table_format
|
||||||
|
|
||||||
|
# Get global tables formatting from manager
|
||||||
|
return self._parent.all_tables_formats
|
||||||
|
|
||||||
def set_column_width(self, col_id: str, width: str):
|
def set_column_width(self, col_id: str, width: str):
|
||||||
"""Update column width after resize. Called via Command from JS."""
|
"""Update column width after resize. Called via Command from JS."""
|
||||||
@@ -497,6 +513,9 @@ class DataGrid(MultipleInstance):
|
|||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
return self._settings
|
return self._settings
|
||||||
|
|
||||||
|
def get_table_name(self):
|
||||||
|
return f"{self._settings.namespace}.{self._settings.name}" if self._settings.namespace else self._settings.name
|
||||||
|
|
||||||
def mk_headers(self):
|
def mk_headers(self):
|
||||||
resize_cmd = self.commands.set_column_width()
|
resize_cmd = self.commands.set_column_width()
|
||||||
move_cmd = self.commands.move_column()
|
move_cmd = self.commands.move_column()
|
||||||
@@ -617,6 +636,9 @@ class DataGrid(MultipleInstance):
|
|||||||
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
||||||
"""
|
"""
|
||||||
df = self._get_filtered_df()
|
df = self._get_filtered_df()
|
||||||
|
if df is None:
|
||||||
|
return []
|
||||||
|
|
||||||
start = page_index * DATAGRID_PAGE_SIZE
|
start = page_index * DATAGRID_PAGE_SIZE
|
||||||
end = start + DATAGRID_PAGE_SIZE
|
end = start + DATAGRID_PAGE_SIZE
|
||||||
if self._state.ns_total_rows > end:
|
if self._state.ns_total_rows > end:
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from myfasthtml.controls.DslEditor import DslEditor
|
from myfasthtml.controls.DslEditor import DslEditor
|
||||||
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope
|
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope, TableScope, TablesScope
|
||||||
|
from myfasthtml.core.instances import InstancesManager
|
||||||
|
|
||||||
logger = logging.getLogger("DataGridFormattingEditor")
|
logger = logging.getLogger("DataGridFormattingEditor")
|
||||||
|
|
||||||
@@ -62,6 +63,8 @@ class DataGridFormattingEditor(DslEditor):
|
|||||||
columns_rules = defaultdict(list) # key = column name
|
columns_rules = defaultdict(list) # key = column name
|
||||||
rows_rules = defaultdict(list) # key = row index
|
rows_rules = defaultdict(list) # key = row index
|
||||||
cells_rules = defaultdict(list) # key = cell_id
|
cells_rules = defaultdict(list) # key = cell_id
|
||||||
|
table_rules = [] # rules for this table
|
||||||
|
tables_rules = [] # global rules for all tables
|
||||||
|
|
||||||
for scoped_rule in scoped_rules:
|
for scoped_rule in scoped_rules:
|
||||||
scope = scoped_rule.scope
|
scope = scoped_rule.scope
|
||||||
@@ -75,6 +78,14 @@ class DataGridFormattingEditor(DslEditor):
|
|||||||
cell_id = self._get_cell_id(scope)
|
cell_id = self._get_cell_id(scope)
|
||||||
if cell_id:
|
if cell_id:
|
||||||
cells_rules[cell_id].append(rule)
|
cells_rules[cell_id].append(rule)
|
||||||
|
elif isinstance(scope, TableScope):
|
||||||
|
# Validate table name matches current grid
|
||||||
|
if scope.table == self._parent._settings.name:
|
||||||
|
table_rules.append(rule)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Table name '{scope.table}' does not match grid name '{self._parent._settings.name}', skipping rules")
|
||||||
|
elif isinstance(scope, TablesScope):
|
||||||
|
tables_rules.append(rule)
|
||||||
|
|
||||||
# Step 3: Copy state for atomic update
|
# Step 3: Copy state for atomic update
|
||||||
state = self._parent.get_state().copy()
|
state = self._parent.get_state().copy()
|
||||||
@@ -85,6 +96,7 @@ class DataGridFormattingEditor(DslEditor):
|
|||||||
for row in state.rows:
|
for row in state.rows:
|
||||||
row.format = None
|
row.format = None
|
||||||
state.cell_formats.clear()
|
state.cell_formats.clear()
|
||||||
|
state.table_format = []
|
||||||
|
|
||||||
# Step 5: Apply grouped rules on the copy
|
# Step 5: Apply grouped rules on the copy
|
||||||
for column_name, rules in columns_rules.items():
|
for column_name, rules in columns_rules.items():
|
||||||
@@ -104,9 +116,20 @@ class DataGridFormattingEditor(DslEditor):
|
|||||||
for cell_id, rules in cells_rules.items():
|
for cell_id, rules in cells_rules.items():
|
||||||
state.cell_formats[cell_id] = rules
|
state.cell_formats[cell_id] = rules
|
||||||
|
|
||||||
|
# Apply table-level rules
|
||||||
|
if table_rules:
|
||||||
|
state.table_format = table_rules
|
||||||
|
|
||||||
|
# Apply global tables-level rules to manager
|
||||||
|
if tables_rules:
|
||||||
|
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||||
|
manager = InstancesManager.get_by_type(self._session, DataGridsManager)
|
||||||
|
if manager:
|
||||||
|
manager.all_tables_formats = tables_rules
|
||||||
|
|
||||||
# Step 6: Update state atomically
|
# Step 6: Update state atomically
|
||||||
self._parent.get_state().update(state)
|
self._parent.get_state().update(state)
|
||||||
|
|
||||||
# Step 7: Refresh the DataGrid
|
# Step 7: Refresh the DataGrid
|
||||||
logger.debug(f"Formatting applied: {len(columns_rules)} columns, {len(rows_rules)} rows, {len(cells_rules)} cells")
|
logger.debug(f"Formatting applied: {len(columns_rules)} columns, {len(rows_rules)} rows, {len(cells_rules)} cells, table: {len(table_rules)}, tables: {len(tables_rules)}")
|
||||||
return self._parent.render_partial("body")
|
return self._parent.render_partial("body")
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ from myfasthtml.controls.helpers import mk
|
|||||||
from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
|
from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
|
||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.dsls import DslsManager
|
|
||||||
from myfasthtml.core.formatting.dsl.completion.engine import FormattingCompletionEngine
|
|
||||||
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
|
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
|
||||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
|
||||||
from myfasthtml.core.formatting.dsl.parser import DSLParser
|
|
||||||
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
|
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
|
||||||
from myfasthtml.core.instances import InstancesManager, SingleInstance
|
from myfasthtml.core.instances import InstancesManager, SingleInstance
|
||||||
from myfasthtml.icons.fluent_p1 import table_add20_regular
|
from myfasthtml.icons.fluent_p1 import table_add20_regular
|
||||||
@@ -88,17 +84,13 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
|||||||
self._state = DataGridsState(self)
|
self._state = DataGridsState(self)
|
||||||
self._tree = self._mk_tree()
|
self._tree = self._mk_tree()
|
||||||
self._tree.bind_command("SelectNode", self.commands.show_document())
|
self._tree.bind_command("SelectNode", self.commands.show_document())
|
||||||
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager)
|
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager, None)
|
||||||
self._registry = DataGridsRegistry(parent)
|
self._registry = DataGridsRegistry(parent)
|
||||||
|
|
||||||
# Global presets shared across all DataGrids
|
# Global presets shared across all DataGrids
|
||||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||||
|
self.all_tables_formats: list = []
|
||||||
# register the auto-completion for the formatter DSL
|
|
||||||
DslsManager.register(FormattingDSL().get_id(),
|
|
||||||
FormattingCompletionEngine(self),
|
|
||||||
DSLParser())
|
|
||||||
|
|
||||||
def upload_from_source(self):
|
def upload_from_source(self):
|
||||||
file_upload = FileUpload(self)
|
file_upload = FileUpload(self)
|
||||||
@@ -112,7 +104,7 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
|||||||
namespace = file_upload.get_file_basename()
|
namespace = file_upload.get_file_basename()
|
||||||
name = file_upload.get_sheet_name()
|
name = file_upload.get_sheet_name()
|
||||||
dg_conf = DatagridConf(namespace=namespace, name=name)
|
dg_conf = DatagridConf(namespace=namespace, name=name)
|
||||||
dg = DataGrid(self._tabs_manager, conf=dg_conf, save_state=True) # first time the Datagrid is created
|
dg = DataGrid(self, conf=dg_conf, save_state=True) # first time the Datagrid is created
|
||||||
dg.init_from_dataframe(df)
|
dg.init_from_dataframe(df)
|
||||||
self._registry.put(namespace, name, dg.get_id())
|
self._registry.put(namespace, name, dg.get_id())
|
||||||
document = DocumentDefinition(
|
document = DocumentDefinition(
|
||||||
@@ -133,7 +125,7 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
|||||||
document_id = self._tree.get_bag(node_id)
|
document_id = self._tree.get_bag(node_id)
|
||||||
try:
|
try:
|
||||||
document = next(filter(lambda x: x.document_id == document_id, self._state.elements))
|
document = next(filter(lambda x: x.document_id == document_id, self._state.elements))
|
||||||
dg = DataGrid(self._tabs_manager, _id=document.datagrid_id) # reload the state & settings
|
dg = DataGrid(self, _id=document.datagrid_id) # reload the state & settings
|
||||||
return self._tabs_manager.show_or_create_tab(document.tab_id, document.name, dg)
|
return self._tabs_manager.show_or_create_tab(document.tab_id, document.name, dg)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
# the selected node is not a document (it's a folder)
|
# the selected node is not a document (it's a folder)
|
||||||
@@ -157,7 +149,7 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
|||||||
raise ValueError(f"No document found for tab {tab_id}")
|
raise ValueError(f"No document found for tab {tab_id}")
|
||||||
|
|
||||||
# Recreate the DataGrid with its saved state
|
# Recreate the DataGrid with its saved state
|
||||||
dg = DataGrid(self._tabs_manager, _id=document.datagrid_id) # reload the state & settings
|
dg = DataGrid(self, _id=document.datagrid_id) # reload the state & settings
|
||||||
return dg
|
return dg
|
||||||
|
|
||||||
def clear_tree(self):
|
def clear_tree(self):
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class DslEditorConf:
|
|||||||
linting: bool = True
|
linting: bool = True
|
||||||
placeholder: str = ""
|
placeholder: str = ""
|
||||||
readonly: bool = False
|
readonly: bool = False
|
||||||
|
engine_id: str = None # id of the DSL engine to use for autocompletion
|
||||||
|
|
||||||
|
|
||||||
class DslEditorState(DbObject):
|
class DslEditorState(DbObject):
|
||||||
@@ -98,7 +99,7 @@ class DslEditor(MultipleInstance):
|
|||||||
|
|
||||||
self._dsl = dsl
|
self._dsl = dsl
|
||||||
self.conf = conf or DslEditorConf()
|
self.conf = conf or DslEditorConf()
|
||||||
self._state = DslEditorState(self, name=conf.name, save_state=save_state)
|
self._state = DslEditorState(self, name=self.conf.name, save_state=save_state)
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
|
|
||||||
logger.debug(f"DslEditor created with id={self._id}, dsl={dsl.name}")
|
logger.debug(f"DslEditor created with id={self._id}, dsl={dsl.name}")
|
||||||
@@ -151,7 +152,7 @@ class DslEditor(MultipleInstance):
|
|||||||
"placeholder": self.conf.placeholder,
|
"placeholder": self.conf.placeholder,
|
||||||
"readonly": self.conf.readonly,
|
"readonly": self.conf.readonly,
|
||||||
"updateCommandId": str(self.commands.update_content().id),
|
"updateCommandId": str(self.commands.update_content().id),
|
||||||
"dslId": self._dsl.get_id(),
|
"dslId": self.conf.engine_id,
|
||||||
"dsl": {
|
"dsl": {
|
||||||
"name": self._dsl.name,
|
"name": self._dsl.name,
|
||||||
"completions": self._dsl.completions,
|
"completions": self._dsl.completions,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class BaseCompletionEngine(ABC):
|
|||||||
provider: Metadata provider for context-aware suggestions
|
provider: Metadata provider for context-aware suggestions
|
||||||
"""
|
"""
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
|
self._id = type(self).__name__
|
||||||
|
|
||||||
def get_completions(self, text: str, cursor: Position) -> CompletionResult:
|
def get_completions(self, text: str, cursor: Position) -> CompletionResult:
|
||||||
"""
|
"""
|
||||||
@@ -169,4 +170,4 @@ class BaseCompletionEngine(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
return type(self).__name__
|
return self._id
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class BaseMetadataProvider(Protocol):
|
|||||||
can extend this with additional methods.
|
can extend this with additional methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_style_presets(self) -> list[str]:
|
def list_style_presets(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of available style preset names.
|
Return the list of available style preset names.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class BaseMetadataProvider(Protocol):
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_format_presets(self) -> list[str]:
|
def list_format_presets(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of available format preset names.
|
Return the list of available format preset names.
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ class DslsManager:
|
|||||||
dsls: dict[str, DslDefinition] = {}
|
dsls: dict[str, DslDefinition] = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register(dsl_id: str, completion: BaseCompletionEngine, validation: DSLParser):
|
def register(completion: BaseCompletionEngine, validation: DSLParser):
|
||||||
# then engine_id is actually the DSL id
|
# then engine_id is actually the DSL id
|
||||||
DslsManager.dsls[dsl_id] = DslDefinition(completion, validation)
|
DslsManager.dsls[completion.get_id()] = DslDefinition(completion, validation)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_completion_engine(engine_id) -> BaseCompletionEngine:
|
def get_completion_engine(engine_id) -> BaseCompletionEngine:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Example:
|
|||||||
"""
|
"""
|
||||||
from .parser import get_parser
|
from .parser import get_parser
|
||||||
from .transformer import DSLTransformer
|
from .transformer import DSLTransformer
|
||||||
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule
|
from .scopes import ColumnScope, RowScope, CellScope, TableScope, TablesScope, ScopedRule
|
||||||
from .exceptions import DSLError, DSLSyntaxError, DSLValidationError
|
from .exceptions import DSLError, DSLSyntaxError, DSLValidationError
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +61,8 @@ __all__ = [
|
|||||||
"ColumnScope",
|
"ColumnScope",
|
||||||
"RowScope",
|
"RowScope",
|
||||||
"CellScope",
|
"CellScope",
|
||||||
|
"TableScope",
|
||||||
|
"TablesScope",
|
||||||
"ScopedRule",
|
"ScopedRule",
|
||||||
# Exceptions
|
# Exceptions
|
||||||
"DSLError",
|
"DSLError",
|
||||||
|
|||||||
@@ -0,0 +1,397 @@
|
|||||||
|
"""
|
||||||
|
Completion engine for the formatting DSL.
|
||||||
|
|
||||||
|
Implements the BaseCompletionEngine for DataGrid formatting rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
||||||
|
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
|
||||||
|
from myfasthtml.core.utils import make_safe_id
|
||||||
|
from . import presets
|
||||||
|
from .contexts import Context, DetectedScope, detect_scope, detect_context
|
||||||
|
from .provider import DatagridMetadataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class FormattingCompletionEngine(BaseCompletionEngine):
|
||||||
|
"""
|
||||||
|
Autocompletion engine for the DataGrid Formatting DSL.
|
||||||
|
|
||||||
|
Provides context-aware suggestions for:
|
||||||
|
- Scope definitions (column, row, cell)
|
||||||
|
- Style expressions with presets and parameters
|
||||||
|
- Format expressions with presets and types
|
||||||
|
- Conditions with operators and values
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, provider: DatagridMetadataProvider, table_name: str):
|
||||||
|
"""
|
||||||
|
Initialize the completion engine.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider: DataGrid metadata provider for dynamic suggestions
|
||||||
|
"""
|
||||||
|
super().__init__(provider)
|
||||||
|
self.provider: DatagridMetadataProvider = provider
|
||||||
|
self.table_name: str = table_name # current table name
|
||||||
|
self._id = "formatting_completion_engine#" + make_safe_id(table_name)
|
||||||
|
|
||||||
|
def detect_scope(self, text: str, current_line: int) -> DetectedScope:
|
||||||
|
"""
|
||||||
|
Detect the current scope by scanning previous lines.
|
||||||
|
|
||||||
|
Looks for the most recent scope declaration (column/row/cell).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The full document text
|
||||||
|
current_line: Current line number (0-based)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DetectedScope with scope information
|
||||||
|
"""
|
||||||
|
return detect_scope(text, current_line)
|
||||||
|
|
||||||
|
def detect_context(
|
||||||
|
self, text: str, cursor: Position, scope: DetectedScope
|
||||||
|
) -> Context:
|
||||||
|
"""
|
||||||
|
Detect the completion context at the cursor position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The full document text
|
||||||
|
cursor: Cursor position
|
||||||
|
scope: The detected scope
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Context enum value
|
||||||
|
"""
|
||||||
|
return detect_context(text, cursor, scope)
|
||||||
|
|
||||||
|
def get_suggestions(
|
||||||
|
self, context: Context, scope: DetectedScope, prefix: str
|
||||||
|
) -> list[Suggestion]:
|
||||||
|
"""
|
||||||
|
Generate suggestions for the given context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: The detected completion context
|
||||||
|
scope: The detected scope
|
||||||
|
prefix: The current word prefix (not used here, filtering done in base)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of suggestions
|
||||||
|
"""
|
||||||
|
match context:
|
||||||
|
# =================================================================
|
||||||
|
# Scope-level contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.NONE:
|
||||||
|
return []
|
||||||
|
|
||||||
|
case Context.SCOPE_KEYWORD:
|
||||||
|
return presets.SCOPE_KEYWORDS
|
||||||
|
|
||||||
|
case Context.COLUMN_NAME:
|
||||||
|
return self._get_column_suggestions()
|
||||||
|
|
||||||
|
case Context.ROW_INDEX:
|
||||||
|
return self._get_row_index_suggestions()
|
||||||
|
|
||||||
|
case Context.CELL_START:
|
||||||
|
return [Suggestion("(", "Start cell coordinates", "syntax")]
|
||||||
|
|
||||||
|
case Context.CELL_COLUMN:
|
||||||
|
return self._get_column_suggestions()
|
||||||
|
|
||||||
|
case Context.CELL_ROW:
|
||||||
|
return self._get_row_index_suggestions()
|
||||||
|
|
||||||
|
case Context.TABLE_NAME:
|
||||||
|
return self._get_table_name_suggestion()
|
||||||
|
|
||||||
|
case Context.TABLES_SCOPE:
|
||||||
|
return [Suggestion(":", "Define global rules for all tables", "syntax")]
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Rule-level contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.RULE_START:
|
||||||
|
return presets.RULE_START
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Style contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.STYLE_ARGS:
|
||||||
|
# Presets (with quotes) + params
|
||||||
|
style_presets = self._get_style_preset_suggestions_quoted()
|
||||||
|
return style_presets + presets.STYLE_PARAMS
|
||||||
|
|
||||||
|
case Context.STYLE_PRESET:
|
||||||
|
return self._get_style_preset_suggestions()
|
||||||
|
|
||||||
|
case Context.STYLE_PARAM:
|
||||||
|
return presets.STYLE_PARAMS
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Format contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.FORMAT_PRESET:
|
||||||
|
return self._get_format_preset_suggestions()
|
||||||
|
|
||||||
|
case Context.FORMAT_TYPE:
|
||||||
|
return presets.FORMAT_TYPES
|
||||||
|
|
||||||
|
case Context.FORMAT_PARAM_DATE:
|
||||||
|
return presets.FORMAT_PARAMS_DATE
|
||||||
|
|
||||||
|
case Context.FORMAT_PARAM_TEXT:
|
||||||
|
return presets.FORMAT_PARAMS_TEXT
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# After style/format
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.AFTER_STYLE_OR_FORMAT:
|
||||||
|
return presets.AFTER_STYLE_OR_FORMAT
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Condition contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.CONDITION_START:
|
||||||
|
return presets.CONDITION_START
|
||||||
|
|
||||||
|
case Context.CONDITION_AFTER_NOT:
|
||||||
|
return presets.CONDITION_AFTER_NOT
|
||||||
|
|
||||||
|
case Context.COLUMN_REF:
|
||||||
|
return self._get_column_suggestions()
|
||||||
|
|
||||||
|
case Context.COLUMN_REF_QUOTED:
|
||||||
|
return self._get_column_suggestions_with_closing_quote()
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Operator contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.OPERATOR:
|
||||||
|
return presets.COMPARISON_OPERATORS
|
||||||
|
|
||||||
|
case Context.OPERATOR_VALUE | Context.BETWEEN_VALUE:
|
||||||
|
# col., True, False + column values
|
||||||
|
base = presets.OPERATOR_VALUE_BASE.copy()
|
||||||
|
base.extend(self._get_column_value_suggestions(scope))
|
||||||
|
return base
|
||||||
|
|
||||||
|
case Context.BETWEEN_AND:
|
||||||
|
return presets.BETWEEN_AND
|
||||||
|
|
||||||
|
case Context.IN_LIST_START:
|
||||||
|
return presets.IN_LIST_START
|
||||||
|
|
||||||
|
case Context.IN_LIST_VALUE:
|
||||||
|
return self._get_column_value_suggestions(scope)
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Value contexts
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
case Context.BOOLEAN_VALUE:
|
||||||
|
return presets.BOOLEAN_VALUES
|
||||||
|
|
||||||
|
case Context.COLOR_VALUE:
|
||||||
|
return presets.ALL_COLORS
|
||||||
|
|
||||||
|
case Context.DATE_FORMAT_VALUE:
|
||||||
|
return presets.DATE_PATTERNS
|
||||||
|
|
||||||
|
case Context.TRANSFORM_VALUE:
|
||||||
|
return presets.TEXT_TRANSFORMS
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _get_column_suggestions(self) -> list[Suggestion]:
|
||||||
|
"""Get column name suggestions from provider."""
|
||||||
|
try:
|
||||||
|
# Try to get columns from the first available table
|
||||||
|
tables = self.provider.list_tables()
|
||||||
|
if tables:
|
||||||
|
columns = self.provider.list_columns(self.table_name)
|
||||||
|
return [Suggestion(col, "Column", "column") for col in columns]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _get_column_suggestions_with_closing_quote(self) -> list[Suggestion]:
|
||||||
|
"""Get column name suggestions with closing quote."""
|
||||||
|
try:
|
||||||
|
tables = self.provider.list_tables()
|
||||||
|
if tables:
|
||||||
|
columns = self.provider.list_columns(self.table_name)
|
||||||
|
return [Suggestion(f'{col}"', "Column", "column") for col in columns]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _get_table_name_suggestion(self) -> list[Suggestion]:
|
||||||
|
"""Get table name suggestion (current table only)."""
|
||||||
|
if self.table_name:
|
||||||
|
return [Suggestion(f'"{self.table_name}"', f"Current table: {self.table_name}", "table")]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _get_style_preset_suggestions(self) -> list[Suggestion]:
|
||||||
|
"""Get style preset suggestions (without quotes)."""
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
# Add provider presets if available
|
||||||
|
try:
|
||||||
|
custom_presets = self.provider.list_style_presets()
|
||||||
|
for preset in custom_presets:
|
||||||
|
# Check if it's already in default presets
|
||||||
|
if not any(s.label == preset for s in presets.STYLE_PRESETS):
|
||||||
|
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add default presets (just the name, no quotes - we're inside quotes)
|
||||||
|
for preset in presets.STYLE_PRESETS:
|
||||||
|
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
def _get_style_preset_suggestions_quoted(self) -> list[Suggestion]:
|
||||||
|
"""Get style preset suggestions with quotes."""
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
# Add provider presets if available
|
||||||
|
try:
|
||||||
|
custom_presets = self.provider.list_style_presets()
|
||||||
|
for preset in custom_presets:
|
||||||
|
if not any(s.label == preset for s in presets.STYLE_PRESETS):
|
||||||
|
suggestions.append(Suggestion(f'"{preset}"', "Custom preset", "preset"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add default presets with quotes
|
||||||
|
for preset in presets.STYLE_PRESETS:
|
||||||
|
suggestions.append(Suggestion(f'"{preset.label}"', preset.detail, preset.kind))
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
def _get_format_preset_suggestions(self) -> list[Suggestion]:
|
||||||
|
"""Get format preset suggestions (without quotes)."""
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
# Add provider presets if available
|
||||||
|
try:
|
||||||
|
custom_presets = self.provider.list_format_presets()
|
||||||
|
for preset in custom_presets:
|
||||||
|
if not any(s.label == preset for s in presets.FORMAT_PRESETS):
|
||||||
|
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add default presets
|
||||||
|
for preset in presets.FORMAT_PRESETS:
|
||||||
|
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
def _get_row_index_suggestions(self) -> list[Suggestion]:
|
||||||
|
"""Get row index suggestions (first 10 + last)."""
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
tables = self.provider.list_tables()
|
||||||
|
if tables:
|
||||||
|
row_count = self.provider.get_row_count(self.table_name)
|
||||||
|
if row_count > 0:
|
||||||
|
# First 10 rows
|
||||||
|
for i in range(min(10, row_count)):
|
||||||
|
suggestions.append(Suggestion(str(i), f"Row {i}", "index"))
|
||||||
|
|
||||||
|
# Last row if not already included
|
||||||
|
last_index = row_count - 1
|
||||||
|
if last_index >= 10:
|
||||||
|
suggestions.append(
|
||||||
|
Suggestion(str(last_index), f"Last row ({row_count} total)", "index")
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback if no provider data
|
||||||
|
if not suggestions:
|
||||||
|
for i in range(10):
|
||||||
|
suggestions.append(Suggestion(str(i), f"Row {i}", "index"))
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
def _get_column_value_suggestions(self, scope: DetectedScope) -> list[Suggestion]:
|
||||||
|
"""Get column value suggestions based on the current scope."""
|
||||||
|
if not scope.column_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use table_name from scope, or empty string as fallback
|
||||||
|
table_name = scope.table_name or ""
|
||||||
|
values = self.provider.list_column_values(table_name, scope.column_name)
|
||||||
|
suggestions = []
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
# Format value appropriately
|
||||||
|
if isinstance(value, str):
|
||||||
|
label = f'"{value}"'
|
||||||
|
detail = "Text value"
|
||||||
|
elif isinstance(value, bool):
|
||||||
|
label = str(value)
|
||||||
|
detail = "Boolean value"
|
||||||
|
elif isinstance(value, (int, float)):
|
||||||
|
label = str(value)
|
||||||
|
detail = "Numeric value"
|
||||||
|
else:
|
||||||
|
label = f'"{value}"'
|
||||||
|
detail = "Value"
|
||||||
|
|
||||||
|
suggestions.append(Suggestion(label, detail, "value"))
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
text: str,
|
||||||
|
cursor: Position,
|
||||||
|
provider: DatagridMetadataProvider,
|
||||||
|
table_name: str,
|
||||||
|
) -> CompletionResult:
|
||||||
|
"""
|
||||||
|
Get autocompletion suggestions for the formatting DSL.
|
||||||
|
|
||||||
|
This is the main entry point for the autocompletion API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The full DSL document text
|
||||||
|
cursor: Cursor position (line and ch are 0-based)
|
||||||
|
provider: DataGrid metadata provider
|
||||||
|
table_name: Table name for completions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletionResult with suggestions and replacement range
|
||||||
|
|
||||||
|
Example:
|
||||||
|
result = get_completions(
|
||||||
|
text='column amount:\\n style("err',
|
||||||
|
cursor=Position(line=1, ch=15),
|
||||||
|
provider=my_provider,
|
||||||
|
table_name="app.orders"
|
||||||
|
)
|
||||||
|
# result.suggestions contains ["error"] filtered by prefix "err"
|
||||||
|
"""
|
||||||
|
engine = FormattingCompletionEngine(provider, table_name)
|
||||||
|
return engine.get_completions(text, cursor)
|
||||||
@@ -25,12 +25,14 @@ class Context(Enum):
|
|||||||
NONE = auto()
|
NONE = auto()
|
||||||
|
|
||||||
# Scope-level contexts
|
# Scope-level contexts
|
||||||
SCOPE_KEYWORD = auto() # Start of non-indented line: column, row, cell
|
SCOPE_KEYWORD = auto() # Start of non-indented line: column, row, cell, table, tables
|
||||||
COLUMN_NAME = auto() # After "column ": column names
|
COLUMN_NAME = auto() # After "column ": column names
|
||||||
ROW_INDEX = auto() # After "row ": row indices
|
ROW_INDEX = auto() # After "row ": row indices
|
||||||
CELL_START = auto() # After "cell ": (
|
CELL_START = auto() # After "cell ": (
|
||||||
CELL_COLUMN = auto() # After "cell (": column names
|
CELL_COLUMN = auto() # After "cell (": column names
|
||||||
CELL_ROW = auto() # After "cell (col, ": row indices
|
CELL_ROW = auto() # After "cell (col, ": row indices
|
||||||
|
TABLE_NAME = auto() # After "table ": table name
|
||||||
|
TABLES_SCOPE = auto() # After "tables": colon
|
||||||
|
|
||||||
# Rule-level contexts
|
# Rule-level contexts
|
||||||
RULE_START = auto() # Start of indented line: style(, format(, format.
|
RULE_START = auto() # Start of indented line: style(, format(, format.
|
||||||
@@ -76,10 +78,10 @@ class DetectedScope:
|
|||||||
Represents the detected scope from scanning previous lines.
|
Represents the detected scope from scanning previous lines.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
scope_type: "column", "row", "cell", or None
|
scope_type: "column", "row", "cell", "table", "tables", or None
|
||||||
column_name: Column name (for column and cell scopes)
|
column_name: Column name (for column and cell scopes)
|
||||||
row_index: Row index (for row and cell scopes)
|
row_index: Row index (for row and cell scopes)
|
||||||
table_name: DataGrid name (if determinable)
|
table_name: Table name (for table scope) or DataGrid name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scope_type: str | None = None
|
scope_type: str | None = None
|
||||||
@@ -92,7 +94,7 @@ def detect_scope(text: str, current_line: int) -> DetectedScope:
|
|||||||
"""
|
"""
|
||||||
Detect the current scope by scanning backwards from the cursor line.
|
Detect the current scope by scanning backwards from the cursor line.
|
||||||
|
|
||||||
Looks for the most recent scope declaration (column/row/cell)
|
Looks for the most recent scope declaration (column/row/cell/table/tables)
|
||||||
that is not indented.
|
that is not indented.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -139,6 +141,17 @@ def detect_scope(text: str, current_line: int) -> DetectedScope:
|
|||||||
scope_type="cell", column_name=column_name, row_index=row_index
|
scope_type="cell", column_name=column_name, row_index=row_index
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for table scope
|
||||||
|
match = re.match(r'^table\s+"([^"]+)"\s*:', line)
|
||||||
|
if match:
|
||||||
|
table_name = match.group(1)
|
||||||
|
return DetectedScope(scope_type="table", table_name=table_name)
|
||||||
|
|
||||||
|
# Check for tables scope
|
||||||
|
match = re.match(r"^tables\s*:", line)
|
||||||
|
if match:
|
||||||
|
return DetectedScope(scope_type="tables")
|
||||||
|
|
||||||
return DetectedScope()
|
return DetectedScope()
|
||||||
|
|
||||||
|
|
||||||
@@ -192,6 +205,14 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
|
|||||||
if re.match(r'^cell\s+\(\s*(?:"[^"]*"|[a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*$', line_to_cursor):
|
if re.match(r'^cell\s+\(\s*(?:"[^"]*"|[a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*$', line_to_cursor):
|
||||||
return Context.CELL_ROW
|
return Context.CELL_ROW
|
||||||
|
|
||||||
|
# After "table "
|
||||||
|
if re.match(r"^table\s+", line_to_cursor) and not line_to_cursor.rstrip().endswith(":"):
|
||||||
|
return Context.TABLE_NAME
|
||||||
|
|
||||||
|
# After "tables"
|
||||||
|
if re.match(r"^tables\s*$", line_to_cursor):
|
||||||
|
return Context.TABLES_SCOPE
|
||||||
|
|
||||||
# Start of line or partial keyword
|
# Start of line or partial keyword
|
||||||
return Context.SCOPE_KEYWORD
|
return Context.SCOPE_KEYWORD
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
"""
|
|
||||||
Completion engine for the formatting DSL.
|
|
||||||
|
|
||||||
Implements the BaseCompletionEngine for DataGrid formatting rules.
|
|
||||||
"""
|
|
||||||
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
|
||||||
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
|
|
||||||
from . import suggestions as suggestions_module
|
|
||||||
from .contexts import Context, DetectedScope, detect_scope, detect_context
|
|
||||||
from .provider import DatagridMetadataProvider
|
|
||||||
|
|
||||||
|
|
||||||
class FormattingCompletionEngine(BaseCompletionEngine):
|
|
||||||
"""
|
|
||||||
Autocompletion engine for the DataGrid Formatting DSL.
|
|
||||||
|
|
||||||
Provides context-aware suggestions for:
|
|
||||||
- Scope definitions (column, row, cell)
|
|
||||||
- Style expressions with presets and parameters
|
|
||||||
- Format expressions with presets and types
|
|
||||||
- Conditions with operators and values
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, provider: DatagridMetadataProvider):
|
|
||||||
"""
|
|
||||||
Initialize the completion engine.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
provider: DataGrid metadata provider for dynamic suggestions
|
|
||||||
"""
|
|
||||||
super().__init__(provider)
|
|
||||||
self.provider: DatagridMetadataProvider = provider
|
|
||||||
|
|
||||||
def detect_scope(self, text: str, current_line: int) -> DetectedScope:
|
|
||||||
"""
|
|
||||||
Detect the current scope by scanning previous lines.
|
|
||||||
|
|
||||||
Looks for the most recent scope declaration (column/row/cell).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The full document text
|
|
||||||
current_line: Current line number (0-based)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DetectedScope with scope information
|
|
||||||
"""
|
|
||||||
return detect_scope(text, current_line)
|
|
||||||
|
|
||||||
def detect_context(
|
|
||||||
self, text: str, cursor: Position, scope: DetectedScope
|
|
||||||
) -> Context:
|
|
||||||
"""
|
|
||||||
Detect the completion context at the cursor position.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The full document text
|
|
||||||
cursor: Cursor position
|
|
||||||
scope: The detected scope
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Context enum value
|
|
||||||
"""
|
|
||||||
return detect_context(text, cursor, scope)
|
|
||||||
|
|
||||||
def get_suggestions(
|
|
||||||
self, context: Context, scope: DetectedScope, prefix: str
|
|
||||||
) -> list[Suggestion]:
|
|
||||||
"""
|
|
||||||
Generate suggestions for the given context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: The detected completion context
|
|
||||||
scope: The detected scope
|
|
||||||
prefix: The current word prefix (not used here, filtering done in base)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of suggestions
|
|
||||||
"""
|
|
||||||
return suggestions_module.get_suggestions(context, scope, self.provider)
|
|
||||||
|
|
||||||
|
|
||||||
def get_completions(
|
|
||||||
text: str,
|
|
||||||
cursor: Position,
|
|
||||||
provider: DatagridMetadataProvider,
|
|
||||||
) -> CompletionResult:
|
|
||||||
"""
|
|
||||||
Get autocompletion suggestions for the formatting DSL.
|
|
||||||
|
|
||||||
This is the main entry point for the autocompletion API.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The full DSL document text
|
|
||||||
cursor: Cursor position (line and ch are 0-based)
|
|
||||||
provider: DataGrid metadata provider
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
CompletionResult with suggestions and replacement range
|
|
||||||
|
|
||||||
Example:
|
|
||||||
result = get_completions(
|
|
||||||
text='column amount:\\n style("err',
|
|
||||||
cursor=Position(line=1, ch=15),
|
|
||||||
provider=my_provider
|
|
||||||
)
|
|
||||||
# result.suggestions contains ["error"] filtered by prefix "err"
|
|
||||||
"""
|
|
||||||
engine = FormattingCompletionEngine(provider)
|
|
||||||
return engine.get_completions(text, cursor)
|
|
||||||
@@ -120,6 +120,8 @@ SCOPE_KEYWORDS: list[Suggestion] = [
|
|||||||
Suggestion("column", "Define column scope", "keyword"),
|
Suggestion("column", "Define column scope", "keyword"),
|
||||||
Suggestion("row", "Define row scope", "keyword"),
|
Suggestion("row", "Define row scope", "keyword"),
|
||||||
Suggestion("cell", "Define cell scope", "keyword"),
|
Suggestion("cell", "Define cell scope", "keyword"),
|
||||||
|
Suggestion("table", "Define table scope", "keyword"),
|
||||||
|
Suggestion("tables", "Define global scope for all tables", "keyword"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ Provides access to DataGrid metadata (columns, values, row counts)
|
|||||||
for context-aware autocompletion.
|
for context-aware autocompletion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Protocol, Any
|
from typing import Any
|
||||||
|
|
||||||
|
from myfasthtml.core.dsl.base_provider import BaseMetadataProvider
|
||||||
|
|
||||||
|
|
||||||
class DatagridMetadataProvider(Protocol):
|
class DatagridMetadataProvider(BaseMetadataProvider):
|
||||||
"""
|
"""
|
||||||
Protocol for providing DataGrid metadata to the autocompletion engine.
|
Protocol for providing DataGrid metadata to the autocompletion engine.
|
||||||
|
|
||||||
@@ -70,25 +72,3 @@ class DatagridMetadataProvider(Protocol):
|
|||||||
Number of rows
|
Number of rows
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def list_style_presets(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Return the list of available style preset names.
|
|
||||||
|
|
||||||
Includes default presets (primary, error, etc.) and custom presets.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of style preset names
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def list_format_presets(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Return the list of available format preset names.
|
|
||||||
|
|
||||||
Includes default presets (EUR, USD, etc.) and custom presets.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of format preset names
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
"""
|
|
||||||
Suggestions generation for the formatting DSL.
|
|
||||||
|
|
||||||
Provides functions to generate appropriate suggestions
|
|
||||||
based on the detected context and scope.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from myfasthtml.core.dsl.types import Suggestion
|
|
||||||
from . import presets
|
|
||||||
from .contexts import Context, DetectedScope
|
|
||||||
from .provider import DatagridMetadataProvider
|
|
||||||
|
|
||||||
|
|
||||||
def get_suggestions(
|
|
||||||
context: Context,
|
|
||||||
scope: DetectedScope,
|
|
||||||
provider: DatagridMetadataProvider,
|
|
||||||
) -> list[Suggestion]:
|
|
||||||
"""
|
|
||||||
Generate suggestions for the given context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: The detected completion context
|
|
||||||
scope: The detected scope
|
|
||||||
provider: Metadata provider for dynamic data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of suggestions
|
|
||||||
"""
|
|
||||||
match context:
|
|
||||||
# =================================================================
|
|
||||||
# Scope-level contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.NONE:
|
|
||||||
return []
|
|
||||||
|
|
||||||
case Context.SCOPE_KEYWORD:
|
|
||||||
return presets.SCOPE_KEYWORDS
|
|
||||||
|
|
||||||
case Context.COLUMN_NAME:
|
|
||||||
return _get_column_suggestions(provider)
|
|
||||||
|
|
||||||
case Context.ROW_INDEX:
|
|
||||||
return _get_row_index_suggestions(provider)
|
|
||||||
|
|
||||||
case Context.CELL_START:
|
|
||||||
return [Suggestion("(", "Start cell coordinates", "syntax")]
|
|
||||||
|
|
||||||
case Context.CELL_COLUMN:
|
|
||||||
return _get_column_suggestions(provider)
|
|
||||||
|
|
||||||
case Context.CELL_ROW:
|
|
||||||
return _get_row_index_suggestions(provider)
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# Rule-level contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.RULE_START:
|
|
||||||
return presets.RULE_START
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# Style contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.STYLE_ARGS:
|
|
||||||
# Presets (with quotes) + params
|
|
||||||
style_presets = _get_style_preset_suggestions_quoted(provider)
|
|
||||||
return style_presets + presets.STYLE_PARAMS
|
|
||||||
|
|
||||||
case Context.STYLE_PRESET:
|
|
||||||
return _get_style_preset_suggestions(provider)
|
|
||||||
|
|
||||||
case Context.STYLE_PARAM:
|
|
||||||
return presets.STYLE_PARAMS
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# Format contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.FORMAT_PRESET:
|
|
||||||
return _get_format_preset_suggestions(provider)
|
|
||||||
|
|
||||||
case Context.FORMAT_TYPE:
|
|
||||||
return presets.FORMAT_TYPES
|
|
||||||
|
|
||||||
case Context.FORMAT_PARAM_DATE:
|
|
||||||
return presets.FORMAT_PARAMS_DATE
|
|
||||||
|
|
||||||
case Context.FORMAT_PARAM_TEXT:
|
|
||||||
return presets.FORMAT_PARAMS_TEXT
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# After style/format
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.AFTER_STYLE_OR_FORMAT:
|
|
||||||
return presets.AFTER_STYLE_OR_FORMAT
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# Condition contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.CONDITION_START:
|
|
||||||
return presets.CONDITION_START
|
|
||||||
|
|
||||||
case Context.CONDITION_AFTER_NOT:
|
|
||||||
return presets.CONDITION_AFTER_NOT
|
|
||||||
|
|
||||||
case Context.COLUMN_REF:
|
|
||||||
return _get_column_suggestions(provider)
|
|
||||||
|
|
||||||
case Context.COLUMN_REF_QUOTED:
|
|
||||||
return _get_column_suggestions_with_closing_quote(provider)
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# Operator contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.OPERATOR:
|
|
||||||
return presets.COMPARISON_OPERATORS
|
|
||||||
|
|
||||||
case Context.OPERATOR_VALUE | Context.BETWEEN_VALUE:
|
|
||||||
# col., True, False + column values
|
|
||||||
base = presets.OPERATOR_VALUE_BASE.copy()
|
|
||||||
base.extend(_get_column_value_suggestions(scope, provider))
|
|
||||||
return base
|
|
||||||
|
|
||||||
case Context.BETWEEN_AND:
|
|
||||||
return presets.BETWEEN_AND
|
|
||||||
|
|
||||||
case Context.IN_LIST_START:
|
|
||||||
return presets.IN_LIST_START
|
|
||||||
|
|
||||||
case Context.IN_LIST_VALUE:
|
|
||||||
return _get_column_value_suggestions(scope, provider)
|
|
||||||
|
|
||||||
# =================================================================
|
|
||||||
# Value contexts
|
|
||||||
# =================================================================
|
|
||||||
|
|
||||||
case Context.BOOLEAN_VALUE:
|
|
||||||
return presets.BOOLEAN_VALUES
|
|
||||||
|
|
||||||
case Context.COLOR_VALUE:
|
|
||||||
return presets.ALL_COLORS
|
|
||||||
|
|
||||||
case Context.DATE_FORMAT_VALUE:
|
|
||||||
return presets.DATE_PATTERNS
|
|
||||||
|
|
||||||
case Context.TRANSFORM_VALUE:
|
|
||||||
return presets.TEXT_TRANSFORMS
|
|
||||||
|
|
||||||
case _:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _get_column_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
|
|
||||||
"""Get column name suggestions from provider."""
|
|
||||||
try:
|
|
||||||
# Try to get columns from the first available table
|
|
||||||
tables = provider.list_tables()
|
|
||||||
if tables:
|
|
||||||
columns = provider.list_columns(tables[0])
|
|
||||||
return [Suggestion(col, "Column", "column") for col in columns]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _get_column_suggestions_with_closing_quote(
|
|
||||||
provider: DatagridMetadataProvider,
|
|
||||||
) -> list[Suggestion]:
|
|
||||||
"""Get column name suggestions with closing quote."""
|
|
||||||
try:
|
|
||||||
tables = provider.list_tables()
|
|
||||||
if tables:
|
|
||||||
columns = provider.list_columns(tables[0])
|
|
||||||
return [Suggestion(f'{col}"', "Column", "column") for col in columns]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _get_style_preset_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
|
|
||||||
"""Get style preset suggestions (without quotes)."""
|
|
||||||
suggestions = []
|
|
||||||
|
|
||||||
# Add provider presets if available
|
|
||||||
try:
|
|
||||||
custom_presets = provider.list_style_presets()
|
|
||||||
for preset in custom_presets:
|
|
||||||
# Check if it's already in default presets
|
|
||||||
if not any(s.label == preset for s in presets.STYLE_PRESETS):
|
|
||||||
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Add default presets (just the name, no quotes - we're inside quotes)
|
|
||||||
for preset in presets.STYLE_PRESETS:
|
|
||||||
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
|
|
||||||
|
|
||||||
def _get_style_preset_suggestions_quoted(
|
|
||||||
provider: DatagridMetadataProvider,
|
|
||||||
) -> list[Suggestion]:
|
|
||||||
"""Get style preset suggestions with quotes."""
|
|
||||||
suggestions = []
|
|
||||||
|
|
||||||
# Add provider presets if available
|
|
||||||
try:
|
|
||||||
custom_presets = provider.list_style_presets()
|
|
||||||
for preset in custom_presets:
|
|
||||||
if not any(s.label == preset for s in presets.STYLE_PRESETS):
|
|
||||||
suggestions.append(Suggestion(f'"{preset}"', "Custom preset", "preset"))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Add default presets with quotes
|
|
||||||
for preset in presets.STYLE_PRESETS:
|
|
||||||
suggestions.append(Suggestion(f'"{preset.label}"', preset.detail, preset.kind))
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
|
|
||||||
|
|
||||||
def _get_format_preset_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
|
|
||||||
"""Get format preset suggestions (without quotes)."""
|
|
||||||
suggestions = []
|
|
||||||
|
|
||||||
# Add provider presets if available
|
|
||||||
try:
|
|
||||||
custom_presets = provider.list_format_presets()
|
|
||||||
for preset in custom_presets:
|
|
||||||
if not any(s.label == preset for s in presets.FORMAT_PRESETS):
|
|
||||||
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Add default presets
|
|
||||||
for preset in presets.FORMAT_PRESETS:
|
|
||||||
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
|
|
||||||
|
|
||||||
def _get_row_index_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
|
|
||||||
"""Get row index suggestions (first 10 + last)."""
|
|
||||||
suggestions = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
tables = provider.list_tables()
|
|
||||||
if tables:
|
|
||||||
row_count = provider.get_row_count(tables[0])
|
|
||||||
if row_count > 0:
|
|
||||||
# First 10 rows
|
|
||||||
for i in range(min(10, row_count)):
|
|
||||||
suggestions.append(Suggestion(str(i), f"Row {i}", "index"))
|
|
||||||
|
|
||||||
# Last row if not already included
|
|
||||||
last_index = row_count - 1
|
|
||||||
if last_index >= 10:
|
|
||||||
suggestions.append(
|
|
||||||
Suggestion(str(last_index), f"Last row ({row_count} total)", "index")
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fallback if no provider data
|
|
||||||
if not suggestions:
|
|
||||||
for i in range(10):
|
|
||||||
suggestions.append(Suggestion(str(i), f"Row {i}", "index"))
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
|
|
||||||
|
|
||||||
def _get_column_value_suggestions(
|
|
||||||
scope: DetectedScope,
|
|
||||||
provider: DatagridMetadataProvider,
|
|
||||||
) -> list[Suggestion]:
|
|
||||||
"""Get column value suggestions based on the current scope."""
|
|
||||||
if not scope.column_name:
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Use table_name from scope, or empty string as fallback
|
|
||||||
table_name = scope.table_name or ""
|
|
||||||
values = provider.list_column_values(table_name, scope.column_name)
|
|
||||||
suggestions = []
|
|
||||||
for value in values:
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
# Format value appropriately
|
|
||||||
if isinstance(value, str):
|
|
||||||
label = f'"{value}"'
|
|
||||||
detail = "Text value"
|
|
||||||
elif isinstance(value, bool):
|
|
||||||
label = str(value)
|
|
||||||
detail = "Boolean value"
|
|
||||||
elif isinstance(value, (int, float)):
|
|
||||||
label = str(value)
|
|
||||||
detail = "Numeric value"
|
|
||||||
else:
|
|
||||||
label = f'"{value}"'
|
|
||||||
detail = "Value"
|
|
||||||
|
|
||||||
suggestions.append(Suggestion(label, detail, "value"))
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
@@ -16,10 +16,14 @@ GRAMMAR = r"""
|
|||||||
scope_header: column_scope
|
scope_header: column_scope
|
||||||
| row_scope
|
| row_scope
|
||||||
| cell_scope
|
| cell_scope
|
||||||
|
| table_scope
|
||||||
|
| tables_scope
|
||||||
|
|
||||||
column_scope: "column" column_name
|
column_scope: "column" column_name
|
||||||
row_scope: "row" INTEGER
|
row_scope: "row" INTEGER
|
||||||
cell_scope: "cell" cell_ref
|
cell_scope: "cell" cell_ref
|
||||||
|
table_scope: "table" QUOTED_STRING
|
||||||
|
tables_scope: "tables"
|
||||||
|
|
||||||
column_name: NAME -> name
|
column_name: NAME -> name
|
||||||
| QUOTED_STRING -> quoted_name
|
| QUOTED_STRING -> quoted_name
|
||||||
|
|||||||
@@ -67,7 +67,13 @@ class DSLParser:
|
|||||||
|
|
||||||
# Strip leading whitespace/newlines and ensure text ends with newline
|
# Strip leading whitespace/newlines and ensure text ends with newline
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
if text and not text.endswith("\n"):
|
|
||||||
|
# Handle empty text (return empty tree that will transform to empty list)
|
||||||
|
if not text:
|
||||||
|
from lark import Tree
|
||||||
|
return Tree('start', [])
|
||||||
|
|
||||||
|
if not text.endswith("\n"):
|
||||||
text += "\n"
|
text += "\n"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ class CellScope:
|
|||||||
cell_id: str = None
|
cell_id: str = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TableScope:
|
||||||
|
"""Scope targeting a specific table by name."""
|
||||||
|
table: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TablesScope:
|
||||||
|
"""Scope targeting all tables (global)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ScopedRule:
|
class ScopedRule:
|
||||||
"""
|
"""
|
||||||
@@ -40,8 +52,8 @@ class ScopedRule:
|
|||||||
The DSL parser returns a list of ScopedRule objects.
|
The DSL parser returns a list of ScopedRule objects.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
scope: Where the rule applies (ColumnScope, RowScope, or CellScope)
|
scope: Where the rule applies (ColumnScope, RowScope, CellScope, TableScope, or TablesScope)
|
||||||
rule: The FormatRule (condition + style + formatter)
|
rule: The FormatRule (condition + style + formatter)
|
||||||
"""
|
"""
|
||||||
scope: ColumnScope | RowScope | CellScope
|
scope: ColumnScope | RowScope | CellScope | TableScope | TablesScope
|
||||||
rule: FormatRule
|
rule: FormatRule
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Converts lark AST into FormatRule and ScopedRule objects.
|
|||||||
from lark import Transformer
|
from lark import Transformer
|
||||||
|
|
||||||
from .exceptions import DSLValidationError
|
from .exceptions import DSLValidationError
|
||||||
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule
|
from .scopes import ColumnScope, RowScope, CellScope, TableScope, TablesScope, ScopedRule
|
||||||
from ..dataclasses import (
|
from ..dataclasses import (
|
||||||
Condition,
|
Condition,
|
||||||
Style,
|
Style,
|
||||||
@@ -68,6 +68,13 @@ class DSLTransformer(Transformer):
|
|||||||
cell_id = str(items[0])
|
cell_id = str(items[0])
|
||||||
return CellScope(cell_id=cell_id)
|
return CellScope(cell_id=cell_id)
|
||||||
|
|
||||||
|
def table_scope(self, items):
|
||||||
|
table_name = self._unquote(items[0])
|
||||||
|
return TableScope(table=table_name)
|
||||||
|
|
||||||
|
def tables_scope(self, items):
|
||||||
|
return TablesScope()
|
||||||
|
|
||||||
def name(self, items):
|
def name(self, items):
|
||||||
return str(items[0])
|
return str(items[0])
|
||||||
|
|
||||||
|
|||||||
@@ -202,12 +202,17 @@ class InstancesManager:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_by_type(session: dict, cls: type):
|
def get_by_type(session: dict, cls: type, default=NO_DEFAULT_VALUE):
|
||||||
session_id = InstancesManager.get_session_id(session)
|
session_id = InstancesManager.get_session_id(session)
|
||||||
res = [i for s, i in InstancesManager.instances.items() if s[0] == session_id and isinstance(i, cls)]
|
res = [i for s, i in InstancesManager.instances.items() if s[0] == session_id and isinstance(i, cls)]
|
||||||
assert len(res) <= 1, f"Multiple instances of type {cls.__name__} found"
|
assert len(res) <= 1, f"Multiple instances of type {cls.__name__} found"
|
||||||
|
try:
|
||||||
assert len(res) > 0, f"No instance of type {cls.__name__} found"
|
assert len(res) > 0, f"No instance of type {cls.__name__} found"
|
||||||
return res[0]
|
return res[0]
|
||||||
|
except AssertionError:
|
||||||
|
if default is NO_DEFAULT_VALUE:
|
||||||
|
raise
|
||||||
|
return default
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dynamic_get(session, component_parent: tuple, component: tuple):
|
def dynamic_get(session, component_parent: tuple, component: tuple):
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ def post(session, b_id: str, values: dict):
|
|||||||
:param values:
|
:param values:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Entering {Routes.Bindings} with {session=}, {b_id=}, {values=}")
|
logger.debug(f"Entering {Routes.Bindings} with session='{debug_session(session)}', {b_id=}, {values=}")
|
||||||
from myfasthtml.core.bindings import BindingsManager
|
from myfasthtml.core.bindings import BindingsManager
|
||||||
binding = BindingsManager.get_binding(b_id)
|
binding = BindingsManager.get_binding(b_id)
|
||||||
if binding:
|
if binding:
|
||||||
@@ -396,7 +396,8 @@ def get(session, e_id: str, text: str, line: int, ch: int):
|
|||||||
:param ch:
|
:param ch:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Entering {Routes.Completions} with {session=}, {e_id=}, {text=}, {line=}, {ch}")
|
logger.debug(
|
||||||
|
f"Entering {Routes.Completions} with session='{debug_session(session)}', {e_id=}, text={len(text)} char(s), {line=}, {ch=}")
|
||||||
completion = DslsManager.get_completion_engine(e_id)
|
completion = DslsManager.get_completion_engine(e_id)
|
||||||
result = completion.get_completions(text, Position(line, ch))
|
result = completion.get_completions(text, Position(line, ch))
|
||||||
return result.to_dict()
|
return result.to_dict()
|
||||||
@@ -413,7 +414,8 @@ def get(session, e_id: str, text: str, line: int, ch: int):
|
|||||||
:param ch:
|
:param ch:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Entering {Routes.Validations} with {session=}, {e_id=}, {text=}, {line=}, {ch}")
|
logger.debug(
|
||||||
|
f"Entering {Routes.Validations} with session='{debug_session(session)}', {e_id=}, text={len(text)} char(s), {line=}, {ch=}")
|
||||||
validation = DslsManager.get_validation_parser(e_id)
|
validation = DslsManager.get_validation_parser(e_id)
|
||||||
try:
|
try:
|
||||||
validation.parse(text)
|
validation.parse(text)
|
||||||
|
|||||||
279
tests/controls/test_datagrid_formatting.py
Normal file
279
tests/controls/test_datagrid_formatting.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
"""
|
||||||
|
Tests for DataGrid formatting integration with table/tables scopes.
|
||||||
|
|
||||||
|
Tests the complete formatting flow: DSL → Storage → Application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from myfasthtml.controls.DataGrid import DataGrid
|
||||||
|
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
|
||||||
|
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||||
|
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState
|
||||||
|
from myfasthtml.core.constants import ColumnType
|
||||||
|
from myfasthtml.core.formatting.dataclasses import FormatRule, Style
|
||||||
|
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||||
|
from myfasthtml.core.instances import InstancesManager
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def manager(root_instance):
|
||||||
|
"""Create a DataGridsManager instance."""
|
||||||
|
mgr = DataGridsManager(root_instance, _id="test-manager")
|
||||||
|
yield mgr
|
||||||
|
InstancesManager.reset()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def datagrid(manager):
|
||||||
|
"""Create a DataGrid instance."""
|
||||||
|
from myfasthtml.controls.DataGrid import DatagridConf
|
||||||
|
conf = DatagridConf(namespace="app", name="products", id="test-grid")
|
||||||
|
grid = DataGrid(manager, conf=conf, save_state=False, _id="test-datagrid")
|
||||||
|
|
||||||
|
# Add some columns
|
||||||
|
grid._state.columns = [
|
||||||
|
DataGridColumnState(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number, visible=True),
|
||||||
|
DataGridColumnState(col_id="status", col_index=1, title="Status", type=ColumnType.Text, visible=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add some rows
|
||||||
|
grid._state.rows = [
|
||||||
|
DataGridRowState(0),
|
||||||
|
DataGridRowState(1),
|
||||||
|
]
|
||||||
|
|
||||||
|
yield grid
|
||||||
|
InstancesManager.reset()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def editor(datagrid):
|
||||||
|
return DataGridFormattingEditor(datagrid, FormattingDSL())
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# _get_format_rules() Hierarchy Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatRulesHierarchy:
|
||||||
|
"""Tests for format rules hierarchy (cell > row > column > table > tables)."""
|
||||||
|
|
||||||
|
def test_i_can_get_cell_level_rules(self, datagrid):
|
||||||
|
"""Test that cell-level rules have highest priority."""
|
||||||
|
# Setup rules at different levels
|
||||||
|
cell_rules = [FormatRule(style=Style(preset="error"))]
|
||||||
|
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||||
|
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||||
|
|
||||||
|
datagrid._state.cell_formats["tcell_test-datagrid-0-0"] = cell_rules
|
||||||
|
datagrid._state.columns[0].format = column_rules
|
||||||
|
datagrid._state.table_format = table_rules
|
||||||
|
|
||||||
|
# Get rules for cell (0, 0)
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
|
||||||
|
# Should return cell rules (highest priority)
|
||||||
|
assert rules == cell_rules
|
||||||
|
|
||||||
|
def test_i_can_get_row_level_rules(self, datagrid):
|
||||||
|
"""Test that row-level rules have second priority."""
|
||||||
|
# Setup rules at different levels
|
||||||
|
row_rules = [FormatRule(style=Style(preset="warning"))]
|
||||||
|
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||||
|
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||||
|
|
||||||
|
datagrid._state.rows[0].format = row_rules
|
||||||
|
datagrid._state.columns[0].format = column_rules
|
||||||
|
datagrid._state.table_format = table_rules
|
||||||
|
|
||||||
|
# Get rules for row 0 (no cell-level rules)
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
|
||||||
|
# Should return row rules
|
||||||
|
assert rules == row_rules
|
||||||
|
|
||||||
|
def test_i_can_get_column_level_rules(self, datagrid):
|
||||||
|
"""Test that column-level rules have third priority."""
|
||||||
|
# Setup rules at different levels
|
||||||
|
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||||
|
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||||
|
|
||||||
|
datagrid._state.columns[0].format = column_rules
|
||||||
|
datagrid._state.table_format = table_rules
|
||||||
|
|
||||||
|
# Get rules for column (no cell or row rules)
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
|
||||||
|
# Should return column rules
|
||||||
|
assert rules == column_rules
|
||||||
|
|
||||||
|
def test_i_can_get_table_level_rules(self, datagrid, manager):
|
||||||
|
"""Test that table-level rules have fourth priority."""
|
||||||
|
# Setup rules at different levels
|
||||||
|
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||||
|
tables_rules = [FormatRule(style=Style(preset="neutral"))]
|
||||||
|
|
||||||
|
datagrid._state.table_format = table_rules
|
||||||
|
manager.all_tables_formats = tables_rules
|
||||||
|
|
||||||
|
# Get rules for cell (no higher level rules)
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
|
||||||
|
# Should return table rules
|
||||||
|
assert rules == table_rules
|
||||||
|
|
||||||
|
def test_i_can_get_tables_level_rules(self, datagrid, manager):
|
||||||
|
"""Test that tables-level rules have lowest priority."""
|
||||||
|
# Setup global rules
|
||||||
|
tables_rules = [FormatRule(style=Style(preset="neutral"))]
|
||||||
|
manager.all_tables_formats = tables_rules
|
||||||
|
|
||||||
|
# Get rules for cell (no other rules)
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
|
||||||
|
# Should return global tables rules
|
||||||
|
assert rules == tables_rules
|
||||||
|
|
||||||
|
def test_i_get_none_when_no_rules(self, datagrid):
|
||||||
|
"""Test that None is returned when no rules are defined."""
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
assert rules == []
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("level,setup_func,expected_preset", [
|
||||||
|
("cell", lambda dg: dg._state.cell_formats.__setitem__("tcell_test-datagrid-0-0",
|
||||||
|
[FormatRule(style=Style(preset="error"))]), "error"),
|
||||||
|
("row", lambda dg: setattr(dg._state.rows[0], "format",
|
||||||
|
[FormatRule(style=Style(preset="warning"))]), "warning"),
|
||||||
|
("column", lambda dg: setattr(dg._state.columns[0], "format",
|
||||||
|
[FormatRule(style=Style(preset="success"))]), "success"),
|
||||||
|
("table", lambda dg: setattr(dg._state, "table_format",
|
||||||
|
[FormatRule(style=Style(preset="info"))]), "info"),
|
||||||
|
])
|
||||||
|
def test_hierarchy_priority(self, datagrid, level, setup_func, expected_preset):
|
||||||
|
"""Test that each level has correct priority in the hierarchy."""
|
||||||
|
setup_func(datagrid)
|
||||||
|
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||||
|
|
||||||
|
assert rules is not None
|
||||||
|
assert len(rules) == 1
|
||||||
|
assert rules[0].style.preset == expected_preset
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DataGridFormattingEditor Integration Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormattingEditorIntegration:
|
||||||
|
"""Tests for DataGridFormattingEditor with table/tables scopes."""
|
||||||
|
|
||||||
|
def test_i_can_dispatch_table_rules(self, datagrid, editor):
|
||||||
|
"""Test that table rules are dispatched to DatagridState.table_format."""
|
||||||
|
|
||||||
|
dsl = '''
|
||||||
|
table "products":
|
||||||
|
style("info")
|
||||||
|
'''
|
||||||
|
editor.set_content(dsl)
|
||||||
|
editor.on_content_changed()
|
||||||
|
|
||||||
|
# Check that table_format is populated
|
||||||
|
assert len(datagrid._state.table_format) == 1
|
||||||
|
assert datagrid._state.table_format[0].style.preset == "info"
|
||||||
|
|
||||||
|
def test_i_cannot_use_wrong_table_name(self, datagrid, editor):
|
||||||
|
"""Test that wrong table name is rejected."""
|
||||||
|
|
||||||
|
dsl = '''
|
||||||
|
table "wrong_name":
|
||||||
|
style("error")
|
||||||
|
'''
|
||||||
|
editor.set_content(dsl)
|
||||||
|
editor.on_content_changed()
|
||||||
|
|
||||||
|
# Rules should not be applied (wrong table name)
|
||||||
|
assert len(datagrid._state.table_format) == 0
|
||||||
|
|
||||||
|
def test_i_can_dispatch_tables_rules(self, manager, datagrid, editor):
|
||||||
|
"""Test that tables rules are dispatched to DataGridsManager."""
|
||||||
|
|
||||||
|
dsl = '''
|
||||||
|
tables:
|
||||||
|
style("neutral")
|
||||||
|
format.number(precision=2)
|
||||||
|
'''
|
||||||
|
editor.set_content(dsl)
|
||||||
|
editor.on_content_changed()
|
||||||
|
|
||||||
|
# Check that manager.all_tables_formats is populated
|
||||||
|
assert len(manager.all_tables_formats) == 2
|
||||||
|
assert manager.all_tables_formats[0].style.preset == "neutral"
|
||||||
|
assert manager.all_tables_formats[1].formatter.precision == 2
|
||||||
|
|
||||||
|
def test_i_can_combine_all_scope_types(self, manager, datagrid, editor):
|
||||||
|
"""Test that all 5 scope types can be used together."""
|
||||||
|
|
||||||
|
dsl = '''
|
||||||
|
tables:
|
||||||
|
style(font_size="14px")
|
||||||
|
|
||||||
|
table "products":
|
||||||
|
format.number(precision=2)
|
||||||
|
|
||||||
|
column amount:
|
||||||
|
style("success") if value > 0
|
||||||
|
|
||||||
|
row 0:
|
||||||
|
style("neutral", bold=True)
|
||||||
|
|
||||||
|
cell (amount, 1):
|
||||||
|
style("error")
|
||||||
|
'''
|
||||||
|
editor.set_content(dsl)
|
||||||
|
editor.on_content_changed()
|
||||||
|
|
||||||
|
# Check all levels are populated
|
||||||
|
assert len(manager.all_tables_formats) == 1
|
||||||
|
assert len(datagrid._state.table_format) == 1
|
||||||
|
assert len(datagrid._state.columns[0].format) == 1
|
||||||
|
assert len(datagrid._state.rows[0].format) == 1
|
||||||
|
assert len(datagrid._state.cell_formats) == 1
|
||||||
|
|
||||||
|
def test_i_can_clear_table_format(self, datagrid, editor):
|
||||||
|
"""Test that table_format is cleared when DSL changes."""
|
||||||
|
# First set table rules
|
||||||
|
dsl = '''
|
||||||
|
table "products":
|
||||||
|
style("info")
|
||||||
|
'''
|
||||||
|
editor.set_content(dsl)
|
||||||
|
editor.on_content_changed()
|
||||||
|
assert len(datagrid._state.table_format) == 1
|
||||||
|
|
||||||
|
# Then remove them
|
||||||
|
editor.set_content("")
|
||||||
|
editor.on_content_changed()
|
||||||
|
assert len(datagrid._state.table_format) == 0
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("table_name,should_apply", [
|
||||||
|
("products", True), # Correct name
|
||||||
|
("PRODUCTS", False), # Case sensitive
|
||||||
|
("product", False), # Partial match not allowed
|
||||||
|
("app.products", False), # Namespace not included
|
||||||
|
("other", False), # Completely wrong
|
||||||
|
])
|
||||||
|
def test_table_name_validation(self, datagrid, editor, table_name, should_apply):
|
||||||
|
"""Test that table name validation is case-sensitive and exact."""
|
||||||
|
|
||||||
|
dsl = f'''
|
||||||
|
table "{table_name}":
|
||||||
|
style("info")
|
||||||
|
'''
|
||||||
|
editor.set_content(dsl)
|
||||||
|
editor.on_content_changed()
|
||||||
|
|
||||||
|
if should_apply:
|
||||||
|
assert len(datagrid._state.table_format) == 1
|
||||||
|
else:
|
||||||
|
assert len(datagrid._state.table_format) == 0
|
||||||
@@ -14,8 +14,7 @@ from myfasthtml.core.formatting.dsl.completion.contexts import (
|
|||||||
detect_scope,
|
detect_scope,
|
||||||
detect_context,
|
detect_context,
|
||||||
)
|
)
|
||||||
from myfasthtml.core.formatting.dsl.completion.suggestions import get_suggestions
|
from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import (
|
||||||
from myfasthtml.core.formatting.dsl.completion.engine import (
|
|
||||||
FormattingCompletionEngine,
|
FormattingCompletionEngine,
|
||||||
get_completions,
|
get_completions,
|
||||||
)
|
)
|
||||||
@@ -143,6 +142,49 @@ def test_i_can_detect_scope_with_multiple_declarations():
|
|||||||
assert scope.column_name == "amount"
|
assert scope.column_name == "amount"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_detect_table_scope():
|
||||||
|
"""Test detection of table scope."""
|
||||||
|
text = 'table "products":\n style()'
|
||||||
|
scope = detect_scope(text, current_line=1)
|
||||||
|
|
||||||
|
assert scope.scope_type == "table"
|
||||||
|
assert scope.table_name == "products"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_detect_table_scope_with_spaces():
|
||||||
|
"""Test detection of table scope with spaces in name."""
|
||||||
|
text = 'table "financial report":\n format()'
|
||||||
|
scope = detect_scope(text, current_line=1)
|
||||||
|
|
||||||
|
assert scope.scope_type == "table"
|
||||||
|
assert scope.table_name == "financial report"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_detect_tables_scope():
|
||||||
|
"""Test detection of global tables scope."""
|
||||||
|
text = "tables:\n style()"
|
||||||
|
scope = detect_scope(text, current_line=1)
|
||||||
|
|
||||||
|
assert scope.scope_type == "tables"
|
||||||
|
assert scope.table_name is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("scope_def,expected_type,expected_attrs", [
|
||||||
|
('column amount:\n style()', "column", {"column_name": "amount"}),
|
||||||
|
('row 5:\n style()', "row", {"row_index": 5}),
|
||||||
|
('cell (amount, 3):\n style()', "cell", {"column_name": "amount", "row_index": 3}),
|
||||||
|
('table "products":\n style()', "table", {"table_name": "products"}),
|
||||||
|
('tables:\n style()', "tables", {}),
|
||||||
|
])
|
||||||
|
def test_i_can_detect_all_scope_types(scope_def, expected_type, expected_attrs):
|
||||||
|
"""Test detection of all 5 scope types."""
|
||||||
|
scope = detect_scope(scope_def, current_line=1)
|
||||||
|
|
||||||
|
assert scope.scope_type == expected_type
|
||||||
|
for attr, value in expected_attrs.items():
|
||||||
|
assert getattr(scope, attr) == value
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Context Detection - Scope Contexts
|
# Context Detection - Scope Contexts
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -228,6 +270,42 @@ def test_context_cell_row_after_comma_quoted():
|
|||||||
assert context == Context.CELL_ROW
|
assert context == Context.CELL_ROW
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_table_name_after_table():
|
||||||
|
"""Test TABLE_NAME context after 'table '."""
|
||||||
|
text = "table "
|
||||||
|
cursor = Position(line=0, ch=6)
|
||||||
|
scope = DetectedScope()
|
||||||
|
|
||||||
|
context = detect_context(text, cursor, scope)
|
||||||
|
assert context == Context.TABLE_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_tables_scope_after_tables():
|
||||||
|
"""Test TABLES_SCOPE context after 'tables'."""
|
||||||
|
text = "tables"
|
||||||
|
cursor = Position(line=0, ch=6)
|
||||||
|
scope = DetectedScope()
|
||||||
|
|
||||||
|
context = detect_context(text, cursor, scope)
|
||||||
|
assert context == Context.TABLES_SCOPE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("text,cursor_ch,expected_context", [
|
||||||
|
("column ", 7, Context.COLUMN_NAME),
|
||||||
|
("row ", 4, Context.ROW_INDEX),
|
||||||
|
("cell ", 5, Context.CELL_START),
|
||||||
|
("table ", 6, Context.TABLE_NAME),
|
||||||
|
("tables", 6, Context.TABLES_SCOPE),
|
||||||
|
])
|
||||||
|
def test_i_can_detect_all_scope_contexts(text, cursor_ch, expected_context):
|
||||||
|
"""Test detection of all scope-related contexts."""
|
||||||
|
cursor = Position(line=0, ch=cursor_ch)
|
||||||
|
scope = DetectedScope()
|
||||||
|
|
||||||
|
context = detect_context(text, cursor, scope)
|
||||||
|
assert context == expected_context
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Context Detection - Rule Contexts
|
# Context Detection - Rule Contexts
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -545,21 +623,49 @@ def test_context_none_in_comment():
|
|||||||
|
|
||||||
def test_suggestions_scope_keyword(provider):
|
def test_suggestions_scope_keyword(provider):
|
||||||
"""Test suggestions for SCOPE_KEYWORD context."""
|
"""Test suggestions for SCOPE_KEYWORD context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope()
|
scope = DetectedScope()
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.SCOPE_KEYWORD, scope, provider)
|
suggestions = engine.get_suggestions(Context.SCOPE_KEYWORD, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "column" in labels
|
assert "column" in labels
|
||||||
assert "row" in labels
|
assert "row" in labels
|
||||||
assert "cell" in labels
|
assert "cell" in labels
|
||||||
|
assert "table" in labels
|
||||||
|
assert "tables" in labels
|
||||||
|
|
||||||
|
|
||||||
|
def test_suggestions_table_name(provider):
|
||||||
|
"""Test suggestions for TABLE_NAME context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
|
scope = DetectedScope()
|
||||||
|
|
||||||
|
suggestions = engine.get_suggestions(Context.TABLE_NAME, scope, "")
|
||||||
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
|
# Should suggest the current table name in quotes
|
||||||
|
assert '"app.orders"' in labels
|
||||||
|
|
||||||
|
|
||||||
|
def test_suggestions_tables_scope(provider):
|
||||||
|
"""Test suggestions for TABLES_SCOPE context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
|
scope = DetectedScope()
|
||||||
|
|
||||||
|
suggestions = engine.get_suggestions(Context.TABLES_SCOPE, scope, "")
|
||||||
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
|
# Should suggest colon to complete the scope
|
||||||
|
assert ":" in labels
|
||||||
|
|
||||||
|
|
||||||
def test_suggestions_style_preset(provider):
|
def test_suggestions_style_preset(provider):
|
||||||
"""Test suggestions for STYLE_PRESET context."""
|
"""Test suggestions for STYLE_PRESET context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.STYLE_PRESET, scope, provider)
|
suggestions = engine.get_suggestions(Context.STYLE_PRESET, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "primary" in labels
|
assert "primary" in labels
|
||||||
@@ -570,9 +676,10 @@ def test_suggestions_style_preset(provider):
|
|||||||
|
|
||||||
def test_suggestions_format_type(provider):
|
def test_suggestions_format_type(provider):
|
||||||
"""Test suggestions for FORMAT_TYPE context."""
|
"""Test suggestions for FORMAT_TYPE context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.FORMAT_TYPE, scope, provider)
|
suggestions = engine.get_suggestions(Context.FORMAT_TYPE, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "number" in labels
|
assert "number" in labels
|
||||||
@@ -584,9 +691,10 @@ def test_suggestions_format_type(provider):
|
|||||||
|
|
||||||
def test_suggestions_operators(provider):
|
def test_suggestions_operators(provider):
|
||||||
"""Test suggestions for OPERATOR context."""
|
"""Test suggestions for OPERATOR context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.OPERATOR, scope, provider)
|
suggestions = engine.get_suggestions(Context.OPERATOR, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "==" in labels
|
assert "==" in labels
|
||||||
@@ -598,9 +706,10 @@ def test_suggestions_operators(provider):
|
|||||||
|
|
||||||
def test_suggestions_boolean_value(provider):
|
def test_suggestions_boolean_value(provider):
|
||||||
"""Test suggestions for BOOLEAN_VALUE context."""
|
"""Test suggestions for BOOLEAN_VALUE context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.BOOLEAN_VALUE, scope, provider)
|
suggestions = engine.get_suggestions(Context.BOOLEAN_VALUE, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "True" in labels
|
assert "True" in labels
|
||||||
@@ -609,9 +718,10 @@ def test_suggestions_boolean_value(provider):
|
|||||||
|
|
||||||
def test_suggestions_color_value(provider):
|
def test_suggestions_color_value(provider):
|
||||||
"""Test suggestions for COLOR_VALUE context."""
|
"""Test suggestions for COLOR_VALUE context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.COLOR_VALUE, scope, provider)
|
suggestions = engine.get_suggestions(Context.COLOR_VALUE, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "red" in labels
|
assert "red" in labels
|
||||||
@@ -621,9 +731,10 @@ def test_suggestions_color_value(provider):
|
|||||||
|
|
||||||
def test_suggestions_column_values(provider):
|
def test_suggestions_column_values(provider):
|
||||||
"""Test suggestions for OPERATOR_VALUE context with column scope."""
|
"""Test suggestions for OPERATOR_VALUE context with column scope."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="status")
|
scope = DetectedScope(scope_type="column", column_name="status")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.OPERATOR_VALUE, scope, provider)
|
suggestions = engine.get_suggestions(Context.OPERATOR_VALUE, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
# Base suggestions
|
# Base suggestions
|
||||||
@@ -639,9 +750,10 @@ def test_suggestions_column_values(provider):
|
|||||||
|
|
||||||
def test_suggestions_rule_start(provider):
|
def test_suggestions_rule_start(provider):
|
||||||
"""Test suggestions for RULE_START context."""
|
"""Test suggestions for RULE_START context."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.RULE_START, scope, provider)
|
suggestions = engine.get_suggestions(Context.RULE_START, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "style(" in labels
|
assert "style(" in labels
|
||||||
@@ -651,9 +763,10 @@ def test_suggestions_rule_start(provider):
|
|||||||
|
|
||||||
def test_suggestions_none_context(provider):
|
def test_suggestions_none_context(provider):
|
||||||
"""Test that NONE context returns empty suggestions."""
|
"""Test that NONE context returns empty suggestions."""
|
||||||
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
scope = DetectedScope()
|
scope = DetectedScope()
|
||||||
|
|
||||||
suggestions = get_suggestions(Context.NONE, scope, provider)
|
suggestions = engine.get_suggestions(Context.NONE, scope, "")
|
||||||
|
|
||||||
assert suggestions == []
|
assert suggestions == []
|
||||||
|
|
||||||
@@ -668,7 +781,7 @@ def test_i_can_get_completions_for_style_preset(provider):
|
|||||||
text = 'column amount:\n style("'
|
text = 'column amount:\n style("'
|
||||||
cursor = Position(line=1, ch=11)
|
cursor = Position(line=1, ch=11)
|
||||||
|
|
||||||
result = get_completions(text, cursor, provider)
|
result = get_completions(text, cursor, provider, "app.orders")
|
||||||
|
|
||||||
assert not result.is_empty
|
assert not result.is_empty
|
||||||
labels = [s.label for s in result.suggestions]
|
labels = [s.label for s in result.suggestions]
|
||||||
@@ -681,7 +794,7 @@ def test_i_can_get_completions_filters_by_prefix(provider):
|
|||||||
text = 'column amount:\n style("err'
|
text = 'column amount:\n style("err'
|
||||||
cursor = Position(line=1, ch=14)
|
cursor = Position(line=1, ch=14)
|
||||||
|
|
||||||
result = get_completions(text, cursor, provider)
|
result = get_completions(text, cursor, provider, "app.orders")
|
||||||
|
|
||||||
labels = [s.label for s in result.suggestions]
|
labels = [s.label for s in result.suggestions]
|
||||||
assert "error" in labels
|
assert "error" in labels
|
||||||
@@ -693,7 +806,7 @@ def test_i_can_get_completions_returns_correct_positions(provider):
|
|||||||
text = 'column amount:\n style("err'
|
text = 'column amount:\n style("err'
|
||||||
cursor = Position(line=1, ch=14) # After "err"
|
cursor = Position(line=1, ch=14) # After "err"
|
||||||
|
|
||||||
result = get_completions(text, cursor, provider)
|
result = get_completions(text, cursor, provider, "app.orders")
|
||||||
|
|
||||||
# from_pos should be at start of "err"
|
# from_pos should be at start of "err"
|
||||||
assert result.from_pos.line == 1
|
assert result.from_pos.line == 1
|
||||||
@@ -709,7 +822,7 @@ def test_i_can_get_completions_at_scope_start(provider):
|
|||||||
text = ""
|
text = ""
|
||||||
cursor = Position(line=0, ch=0)
|
cursor = Position(line=0, ch=0)
|
||||||
|
|
||||||
result = get_completions(text, cursor, provider)
|
result = get_completions(text, cursor, provider, "app.orders")
|
||||||
|
|
||||||
labels = [s.label for s in result.suggestions]
|
labels = [s.label for s in result.suggestions]
|
||||||
assert "column" in labels
|
assert "column" in labels
|
||||||
@@ -722,7 +835,7 @@ def test_i_can_get_completions_for_column_names(provider):
|
|||||||
text = "column "
|
text = "column "
|
||||||
cursor = Position(line=0, ch=7)
|
cursor = Position(line=0, ch=7)
|
||||||
|
|
||||||
result = get_completions(text, cursor, provider)
|
result = get_completions(text, cursor, provider, "app.orders")
|
||||||
|
|
||||||
labels = [s.label for s in result.suggestions]
|
labels = [s.label for s in result.suggestions]
|
||||||
assert "id" in labels
|
assert "id" in labels
|
||||||
@@ -735,21 +848,22 @@ def test_i_can_get_completions_in_comment_returns_empty(provider):
|
|||||||
text = "column amount:\n # comment"
|
text = "column amount:\n # comment"
|
||||||
cursor = Position(line=1, ch=15)
|
cursor = Position(line=1, ch=15)
|
||||||
|
|
||||||
result = get_completions(text, cursor, provider)
|
result = get_completions(text, cursor, provider, "app.orders")
|
||||||
|
|
||||||
assert result.is_empty
|
assert result.is_empty
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_create_formatting_completion_engine(provider):
|
def test_i_can_create_formatting_completion_engine(provider):
|
||||||
"""Test that FormattingCompletionEngine can be instantiated."""
|
"""Test that FormattingCompletionEngine can be instantiated."""
|
||||||
engine = FormattingCompletionEngine(provider)
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
|
|
||||||
assert engine.provider == provider
|
assert engine.provider == provider
|
||||||
|
assert engine.table_name == "app.orders"
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_use_engine_detect_scope(provider):
|
def test_i_can_use_engine_detect_scope(provider):
|
||||||
"""Test engine's detect_scope method."""
|
"""Test engine's detect_scope method."""
|
||||||
engine = FormattingCompletionEngine(provider)
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
text = "column amount:\n style()"
|
text = "column amount:\n style()"
|
||||||
|
|
||||||
scope = engine.detect_scope(text, current_line=1)
|
scope = engine.detect_scope(text, current_line=1)
|
||||||
@@ -760,7 +874,7 @@ def test_i_can_use_engine_detect_scope(provider):
|
|||||||
|
|
||||||
def test_i_can_use_engine_detect_context(provider):
|
def test_i_can_use_engine_detect_context(provider):
|
||||||
"""Test engine's detect_context method."""
|
"""Test engine's detect_context method."""
|
||||||
engine = FormattingCompletionEngine(provider)
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
text = "column amount:\n style("
|
text = "column amount:\n style("
|
||||||
cursor = Position(line=1, ch=10)
|
cursor = Position(line=1, ch=10)
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from myfasthtml.core.formatting.dsl import (
|
|||||||
ColumnScope,
|
ColumnScope,
|
||||||
RowScope,
|
RowScope,
|
||||||
CellScope,
|
CellScope,
|
||||||
|
TableScope,
|
||||||
|
TablesScope,
|
||||||
DSLSyntaxError,
|
DSLSyntaxError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,6 +127,62 @@ cell tcell_grid1-3-2:
|
|||||||
assert rules[0].scope.row is None
|
assert rules[0].scope.row is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestTableScope:
|
||||||
|
"""Tests for table scope parsing."""
|
||||||
|
|
||||||
|
def test_i_can_parse_table_scope(self):
|
||||||
|
"""Test parsing a table scope."""
|
||||||
|
dsl = """
|
||||||
|
table "products":
|
||||||
|
style("neutral")
|
||||||
|
"""
|
||||||
|
rules = parse_dsl(dsl)
|
||||||
|
|
||||||
|
assert len(rules) == 1
|
||||||
|
assert isinstance(rules[0].scope, TableScope)
|
||||||
|
assert rules[0].scope.table == "products"
|
||||||
|
|
||||||
|
def test_i_can_parse_table_scope_with_spaces(self):
|
||||||
|
"""Test parsing a table scope with spaces in name."""
|
||||||
|
dsl = """
|
||||||
|
table "financial report":
|
||||||
|
style("info")
|
||||||
|
"""
|
||||||
|
rules = parse_dsl(dsl)
|
||||||
|
|
||||||
|
assert len(rules) == 1
|
||||||
|
assert isinstance(rules[0].scope, TableScope)
|
||||||
|
assert rules[0].scope.table == "financial report"
|
||||||
|
|
||||||
|
|
||||||
|
class TestTablesScope:
|
||||||
|
"""Tests for tables scope (global) parsing."""
|
||||||
|
|
||||||
|
def test_i_can_parse_tables_scope(self):
|
||||||
|
"""Test parsing the global tables scope."""
|
||||||
|
dsl = """
|
||||||
|
tables:
|
||||||
|
style("neutral")
|
||||||
|
"""
|
||||||
|
rules = parse_dsl(dsl)
|
||||||
|
|
||||||
|
assert len(rules) == 1
|
||||||
|
assert isinstance(rules[0].scope, TablesScope)
|
||||||
|
|
||||||
|
def test_i_can_parse_tables_scope_with_multiple_rules(self):
|
||||||
|
"""Test parsing tables scope with multiple rules."""
|
||||||
|
dsl = """
|
||||||
|
tables:
|
||||||
|
style("neutral")
|
||||||
|
format.number(precision=2)
|
||||||
|
"""
|
||||||
|
rules = parse_dsl(dsl)
|
||||||
|
|
||||||
|
assert len(rules) == 2
|
||||||
|
assert isinstance(rules[0].scope, TablesScope)
|
||||||
|
assert isinstance(rules[1].scope, TablesScope)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Style Tests
|
# Style Tests
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -497,6 +555,48 @@ row 0:
|
|||||||
assert isinstance(rules[2].scope, RowScope)
|
assert isinstance(rules[2].scope, RowScope)
|
||||||
assert rules[2].scope.row == 0
|
assert rules[2].scope.row == 0
|
||||||
|
|
||||||
|
def test_i_can_parse_all_scope_types(self):
|
||||||
|
"""Test parsing all 5 scope types together."""
|
||||||
|
dsl = """
|
||||||
|
tables:
|
||||||
|
style(font_size="14px")
|
||||||
|
|
||||||
|
table "products":
|
||||||
|
format.number(precision=2)
|
||||||
|
|
||||||
|
column amount:
|
||||||
|
format("EUR")
|
||||||
|
|
||||||
|
row 0:
|
||||||
|
style("neutral", bold=True)
|
||||||
|
|
||||||
|
cell (amount, 5):
|
||||||
|
style("highlight")
|
||||||
|
"""
|
||||||
|
rules = parse_dsl(dsl)
|
||||||
|
|
||||||
|
assert len(rules) == 5
|
||||||
|
|
||||||
|
# First rule: tables (global)
|
||||||
|
assert isinstance(rules[0].scope, TablesScope)
|
||||||
|
|
||||||
|
# Second rule: table "products"
|
||||||
|
assert isinstance(rules[1].scope, TableScope)
|
||||||
|
assert rules[1].scope.table == "products"
|
||||||
|
|
||||||
|
# Third rule: column amount
|
||||||
|
assert isinstance(rules[2].scope, ColumnScope)
|
||||||
|
assert rules[2].scope.column == "amount"
|
||||||
|
|
||||||
|
# Fourth rule: row 0
|
||||||
|
assert isinstance(rules[3].scope, RowScope)
|
||||||
|
assert rules[3].scope.row == 0
|
||||||
|
|
||||||
|
# Fifth rule: cell (amount, 5)
|
||||||
|
assert isinstance(rules[4].scope, CellScope)
|
||||||
|
assert rules[4].scope.column == "amount"
|
||||||
|
assert rules[4].scope.row == 5
|
||||||
|
|
||||||
def test_i_can_parse_style_and_format_combined(self):
|
def test_i_can_parse_style_and_format_combined(self):
|
||||||
"""Test parsing style and format on same line."""
|
"""Test parsing style and format on same line."""
|
||||||
dsl = """
|
dsl = """
|
||||||
|
|||||||
Reference in New Issue
Block a user