Added "table" and "tables" in the DSL
This commit is contained in:
@@ -23,7 +23,7 @@ Example:
|
||||
"""
|
||||
from .parser import get_parser
|
||||
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
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ __all__ = [
|
||||
"ColumnScope",
|
||||
"RowScope",
|
||||
"CellScope",
|
||||
"TableScope",
|
||||
"TablesScope",
|
||||
"ScopedRule",
|
||||
# Exceptions
|
||||
"DSLError",
|
||||
|
||||
@@ -105,7 +105,13 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
|
||||
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
|
||||
# =================================================================
|
||||
@@ -230,7 +236,13 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
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 = []
|
||||
|
||||
@@ -25,12 +25,14 @@ class Context(Enum):
|
||||
NONE = auto()
|
||||
|
||||
# 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
|
||||
ROW_INDEX = auto() # After "row ": row indices
|
||||
CELL_START = auto() # After "cell ": (
|
||||
CELL_COLUMN = auto() # After "cell (": column names
|
||||
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_START = auto() # Start of indented line: style(, format(, format.
|
||||
@@ -76,10 +78,10 @@ class DetectedScope:
|
||||
Represents the detected scope from scanning previous lines.
|
||||
|
||||
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)
|
||||
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
|
||||
@@ -92,7 +94,7 @@ def detect_scope(text: str, current_line: int) -> DetectedScope:
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
@@ -138,6 +140,17 @@ def detect_scope(text: str, current_line: int) -> DetectedScope:
|
||||
return DetectedScope(
|
||||
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()
|
||||
|
||||
@@ -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):
|
||||
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
|
||||
return Context.SCOPE_KEYWORD
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ SCOPE_KEYWORDS: list[Suggestion] = [
|
||||
Suggestion("column", "Define column scope", "keyword"),
|
||||
Suggestion("row", "Define row scope", "keyword"),
|
||||
Suggestion("cell", "Define cell scope", "keyword"),
|
||||
Suggestion("table", "Define table scope", "keyword"),
|
||||
Suggestion("tables", "Define global scope for all tables", "keyword"),
|
||||
]
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -16,10 +16,14 @@ GRAMMAR = r"""
|
||||
scope_header: column_scope
|
||||
| row_scope
|
||||
| cell_scope
|
||||
| table_scope
|
||||
| tables_scope
|
||||
|
||||
column_scope: "column" column_name
|
||||
row_scope: "row" INTEGER
|
||||
cell_scope: "cell" cell_ref
|
||||
table_scope: "table" QUOTED_STRING
|
||||
tables_scope: "tables"
|
||||
|
||||
column_name: NAME -> name
|
||||
| QUOTED_STRING -> quoted_name
|
||||
|
||||
@@ -64,12 +64,18 @@ class DSLParser:
|
||||
lines = text.split("\n")
|
||||
lines = ["" if line.strip().startswith("#") else line for line in lines]
|
||||
text = "\n".join(lines)
|
||||
|
||||
|
||||
# Strip leading whitespace/newlines and ensure text ends with newline
|
||||
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"
|
||||
|
||||
|
||||
try:
|
||||
return self._parser.parse(text)
|
||||
except UnexpectedInput as e:
|
||||
|
||||
@@ -32,6 +32,18 @@ class CellScope:
|
||||
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
|
||||
class ScopedRule:
|
||||
"""
|
||||
@@ -40,8 +52,8 @@ class ScopedRule:
|
||||
The DSL parser returns a list of ScopedRule objects.
|
||||
|
||||
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)
|
||||
"""
|
||||
scope: ColumnScope | RowScope | CellScope
|
||||
scope: ColumnScope | RowScope | CellScope | TableScope | TablesScope
|
||||
rule: FormatRule
|
||||
|
||||
@@ -6,7 +6,7 @@ Converts lark AST into FormatRule and ScopedRule objects.
|
||||
from lark import Transformer
|
||||
|
||||
from .exceptions import DSLValidationError
|
||||
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule
|
||||
from .scopes import ColumnScope, RowScope, CellScope, TableScope, TablesScope, ScopedRule
|
||||
from ..dataclasses import (
|
||||
Condition,
|
||||
Style,
|
||||
@@ -67,7 +67,14 @@ class DSLTransformer(Transformer):
|
||||
def cell_id(self, items):
|
||||
cell_id = str(items[0])
|
||||
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):
|
||||
return str(items[0])
|
||||
|
||||
|
||||
@@ -202,12 +202,17 @@ class InstancesManager:
|
||||
return default
|
||||
|
||||
@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)
|
||||
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) > 0, f"No instance of type {cls.__name__} found"
|
||||
return res[0]
|
||||
try:
|
||||
assert len(res) > 0, f"No instance of type {cls.__name__} found"
|
||||
return res[0]
|
||||
except AssertionError:
|
||||
if default is NO_DEFAULT_VALUE:
|
||||
raise
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def dynamic_get(session, component_parent: tuple, component: tuple):
|
||||
|
||||
Reference in New Issue
Block a user