Working on Formating DSL completion

This commit is contained in:
2026-01-31 19:09:14 +01:00
parent 778e5ac69d
commit d7ec99c3d9
77 changed files with 7563 additions and 63 deletions

View File

View File

@@ -0,0 +1,770 @@
"""
Tests for formatting DSL autocompletion.
Tests for scope detection, context detection, suggestions, and engine integration.
"""
import pytest
from typing import Any
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
from myfasthtml.core.formatting.dsl.completion.contexts import (
Context,
DetectedScope,
detect_scope,
detect_context,
)
from myfasthtml.core.formatting.dsl.completion.suggestions import get_suggestions
from myfasthtml.core.formatting.dsl.completion.engine import (
FormattingCompletionEngine,
get_completions,
)
from myfasthtml.core.formatting.dsl.completion import presets
# =============================================================================
# Mock Provider Fixture
# =============================================================================
class MockProvider:
"""
Mock metadata provider for testing.
Provides predefined data for columns, values, and presets.
"""
def get_tables(self) -> list[str]:
return ["app.orders"]
def get_columns(self, table: str) -> list[str]:
return ["id", "amount", "status"]
def get_column_values(self, column: str) -> list[Any]:
if column == "status":
return ["draft", "pending", "approved"]
if column == "amount":
return [100, 250, 500]
return []
def get_row_count(self, table: str) -> int:
return 150
def get_style_presets(self) -> list[str]:
return ["custom_highlight"]
def get_format_presets(self) -> list[str]:
return ["CHF"]
@pytest.fixture
def provider():
"""Return a mock provider for tests."""
return MockProvider()
# =============================================================================
# Scope Detection Tests
# =============================================================================
def test_i_can_detect_column_scope():
"""Test detection of column scope."""
text = "column amount:\n style()"
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "column"
assert scope.column_name == "amount"
assert scope.row_index is None
def test_i_can_detect_column_scope_after_first_line():
"""Test detection of column scope."""
text = "column amount:\n style()"
scope = detect_scope(text, current_line=2)
assert scope.scope_type == "column"
assert scope.column_name == "amount"
assert scope.row_index is None
def test_i_can_detect_column_scope_quoted():
"""Test detection of column scope with quoted column name."""
text = 'column "total amount":\n style()'
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "column"
assert scope.column_name == "total amount"
def test_i_can_detect_row_scope():
"""Test detection of row scope."""
text = "row 5:\n style()"
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "row"
assert scope.row_index == 5
assert scope.column_name is None
def test_i_can_detect_cell_scope():
"""Test detection of cell scope."""
text = "cell (amount, 3):\n style()"
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "cell"
assert scope.column_name == "amount"
assert scope.row_index == 3
def test_i_can_detect_cell_scope_quoted():
"""Test detection of cell scope with quoted column name."""
text = 'cell ("total amount", 3):\n style()'
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "cell"
assert scope.column_name == "total amount"
assert scope.row_index == 3
def test_i_cannot_detect_scope_without_declaration():
"""Test that no scope is detected when there's no declaration."""
text = " style()"
scope = detect_scope(text, current_line=0)
assert scope.scope_type is None
def test_i_can_detect_scope_with_multiple_declarations():
"""Test that the most recent scope is detected."""
text = "column id:\n style()\ncolumn amount:\n format()"
scope = detect_scope(text, current_line=3)
assert scope.scope_type == "column"
assert scope.column_name == "amount"
# =============================================================================
# Context Detection - Scope Contexts
# =============================================================================
def test_context_scope_keyword_at_line_start():
"""Test SCOPE_KEYWORD context at start of non-indented line."""
text = ""
cursor = Position(line=0, ch=0)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.SCOPE_KEYWORD
def test_context_scope_keyword_partial():
"""Test SCOPE_KEYWORD context with partial keyword."""
text = "col"
cursor = Position(line=0, ch=3)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.SCOPE_KEYWORD
def test_context_column_name_after_column():
"""Test COLUMN_NAME context after 'column '."""
text = "column "
cursor = Position(line=0, ch=7)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.COLUMN_NAME
def test_context_row_index_after_row():
"""Test ROW_INDEX context after 'row '."""
text = "row "
cursor = Position(line=0, ch=4)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.ROW_INDEX
def test_context_cell_start_after_cell():
"""Test CELL_START context after 'cell '."""
text = "cell "
cursor = Position(line=0, ch=5)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.CELL_START
def test_context_cell_column_after_open_paren():
"""Test CELL_COLUMN context after 'cell ('."""
text = "cell ("
cursor = Position(line=0, ch=6)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.CELL_COLUMN
def test_context_cell_row_after_comma():
"""Test CELL_ROW context after 'cell (amount, '."""
text = "cell (amount, "
cursor = Position(line=0, ch=14)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.CELL_ROW
def test_context_cell_row_after_comma_quoted():
"""Test CELL_ROW context after 'cell ("column", '."""
text = 'cell ("amount", '
cursor = Position(line=0, ch=16)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.CELL_ROW
# =============================================================================
# Context Detection - Rule Contexts
# =============================================================================
def test_context_rule_start_on_indented_empty_line():
"""Test RULE_START context on empty indented line."""
text = "column amount:\n "
cursor = Position(line=1, ch=4)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.RULE_START
def test_context_rule_start_partial_keyword():
"""Test RULE_START context with partial keyword."""
text = "column amount:\n sty"
cursor = Position(line=1, ch=7)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.RULE_START
# =============================================================================
# Context Detection - Style Contexts
# =============================================================================
def test_context_style_args_after_open_paren():
"""Test STYLE_ARGS context after 'style('."""
text = "column amount:\n style("
cursor = Position(line=1, ch=10)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.STYLE_ARGS
def test_context_style_preset_inside_quotes():
"""Test STYLE_PRESET context inside style quotes."""
text = 'column amount:\n style("err'
cursor = Position(line=1, ch=14)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.STYLE_PRESET
def test_context_style_param_after_comma():
"""Test STYLE_PARAM context after comma in style()."""
text = 'column amount:\n style("error", '
cursor = Position(line=1, ch=21)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.STYLE_PARAM
def test_context_boolean_value_after_bold():
"""Test BOOLEAN_VALUE context after 'bold='."""
text = "column amount:\n style(bold="
cursor = Position(line=1, ch=15)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.BOOLEAN_VALUE
def test_context_boolean_value_after_italic():
"""Test BOOLEAN_VALUE context after 'italic='."""
text = "column amount:\n style(italic="
cursor = Position(line=1, ch=17)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.BOOLEAN_VALUE
def test_context_color_value_after_color():
"""Test COLOR_VALUE context after 'color='."""
text = "column amount:\n style(color="
cursor = Position(line=1, ch=16)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.COLOR_VALUE
def test_context_color_value_after_background_color():
"""Test COLOR_VALUE context after 'background_color='."""
text = "column amount:\n style(background_color="
cursor = Position(line=1, ch=27)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.COLOR_VALUE
# =============================================================================
# Context Detection - Format Contexts
# =============================================================================
def test_context_format_preset_inside_quotes():
"""Test FORMAT_PRESET context inside format quotes."""
text = 'column amount:\n format("EU'
cursor = Position(line=1, ch=15)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_PRESET
def test_context_format_type_after_dot():
"""Test FORMAT_TYPE context after 'format.'."""
text = "column amount:\n format."
cursor = Position(line=1, ch=11)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_TYPE
def test_context_format_param_date():
"""Test FORMAT_PARAM_DATE context inside format.date()."""
text = "column amount:\n format.date("
cursor = Position(line=1, ch=16)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_PARAM_DATE
def test_context_format_param_text():
"""Test FORMAT_PARAM_TEXT context inside format.text()."""
text = "column amount:\n format.text("
cursor = Position(line=1, ch=16)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_PARAM_TEXT
def test_context_date_format_value():
"""Test DATE_FORMAT_VALUE context after 'format=' in format.date."""
text = "column amount:\n format.date(format="
cursor = Position(line=1, ch=23)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.DATE_FORMAT_VALUE
def test_context_transform_value():
"""Test TRANSFORM_VALUE context after 'transform='."""
text = "column amount:\n format.text(transform="
cursor = Position(line=1, ch=26)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.TRANSFORM_VALUE
# =============================================================================
# Context Detection - After Style/Format
# =============================================================================
def test_context_after_style_or_format():
"""Test AFTER_STYLE_OR_FORMAT context after closing paren."""
text = 'column amount:\n style("error")'
cursor = Position(line=1, ch=19)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.AFTER_STYLE_OR_FORMAT
# =============================================================================
# Context Detection - Condition Contexts
# =============================================================================
def test_context_condition_start_after_if():
"""Test CONDITION_START context after 'if '."""
text = 'column amount:\n style("error") if '
cursor = Position(line=1, ch=23)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.CONDITION_START
def test_context_condition_after_not():
"""Test CONDITION_AFTER_NOT context after 'if not '."""
text = 'column amount:\n style("error") if not '
cursor = Position(line=1, ch=27)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.CONDITION_AFTER_NOT
def test_context_column_ref_after_col_dot():
"""Test COLUMN_REF context after 'col.'."""
text = 'column amount:\n style("error") if col.'
cursor = Position(line=1, ch=28)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.COLUMN_REF
def test_context_column_ref_quoted():
"""Test COLUMN_REF_QUOTED context after 'col."'."""
text = 'column amount:\n style("error") if col."'
cursor = Position(line=1, ch=29)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.COLUMN_REF_QUOTED
def test_context_operator_after_value():
"""Test OPERATOR context after 'value '."""
text = 'column amount:\n style("error") if value '
cursor = Position(line=1, ch=30)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.OPERATOR
def test_context_operator_value_after_equals():
"""Test OPERATOR_VALUE context after 'value == '."""
text = 'column amount:\n style("error") if value == '
cursor = Position(line=1, ch=33)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.OPERATOR_VALUE
def test_context_between_and():
"""Test BETWEEN_AND context after 'between 0 '."""
text = 'column amount:\n style("error") if value between 0 '
cursor = Position(line=1, ch=39)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.BETWEEN_AND
def test_context_between_value():
"""Test BETWEEN_VALUE context after 'between 0 and '."""
text = 'column amount:\n style("error") if value between 0 and '
cursor = Position(line=1, ch=43)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.BETWEEN_VALUE
def test_context_in_list_start():
"""Test IN_LIST_START context after 'in '."""
text = 'column status:\n style("error") if value in '
cursor = Position(line=1, ch=33)
scope = DetectedScope(scope_type="column", column_name="status")
context = detect_context(text, cursor, scope)
assert context == Context.IN_LIST_START
def test_context_in_list_value():
"""Test IN_LIST_VALUE context after 'in ['."""
text = 'column status:\n style("error") if value in ['
cursor = Position(line=1, ch=34)
scope = DetectedScope(scope_type="column", column_name="status")
context = detect_context(text, cursor, scope)
assert context == Context.IN_LIST_VALUE
def test_context_in_list_value_after_comma():
"""Test IN_LIST_VALUE context after comma in list."""
text = 'column status:\n style("error") if value in ["a", '
cursor = Position(line=1, ch=39)
scope = DetectedScope(scope_type="column", column_name="status")
context = detect_context(text, cursor, scope)
assert context == Context.IN_LIST_VALUE
# =============================================================================
# Context Detection - Special Cases
# =============================================================================
def test_context_none_in_comment():
"""Test NONE context when cursor is in comment."""
text = "column amount:\n # comment"
cursor = Position(line=1, ch=15)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.NONE
# =============================================================================
# Suggestions Tests
# =============================================================================
def test_suggestions_scope_keyword(provider):
"""Test suggestions for SCOPE_KEYWORD context."""
scope = DetectedScope()
suggestions = get_suggestions(Context.SCOPE_KEYWORD, scope, provider)
labels = [s.label for s in suggestions]
assert "column" in labels
assert "row" in labels
assert "cell" in labels
def test_suggestions_style_preset(provider):
"""Test suggestions for STYLE_PRESET context."""
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = get_suggestions(Context.STYLE_PRESET, scope, provider)
labels = [s.label for s in suggestions]
assert "primary" in labels
assert "error" in labels
assert "warning" in labels
assert "custom_highlight" in labels # From provider
def test_suggestions_format_type(provider):
"""Test suggestions for FORMAT_TYPE context."""
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = get_suggestions(Context.FORMAT_TYPE, scope, provider)
labels = [s.label for s in suggestions]
assert "number" in labels
assert "date" in labels
assert "boolean" in labels
assert "text" in labels
assert "enum" in labels
def test_suggestions_operators(provider):
"""Test suggestions for OPERATOR context."""
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = get_suggestions(Context.OPERATOR, scope, provider)
labels = [s.label for s in suggestions]
assert "==" in labels
assert "<" in labels
assert "contains" in labels
assert "in" in labels
assert "between" in labels
def test_suggestions_boolean_value(provider):
"""Test suggestions for BOOLEAN_VALUE context."""
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = get_suggestions(Context.BOOLEAN_VALUE, scope, provider)
labels = [s.label for s in suggestions]
assert "True" in labels
assert "False" in labels
def test_suggestions_color_value(provider):
"""Test suggestions for COLOR_VALUE context."""
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = get_suggestions(Context.COLOR_VALUE, scope, provider)
labels = [s.label for s in suggestions]
assert "red" in labels
assert "blue" in labels
assert "var(--color-primary)" in labels
def test_suggestions_column_values(provider):
"""Test suggestions for OPERATOR_VALUE context with column scope."""
scope = DetectedScope(scope_type="column", column_name="status")
suggestions = get_suggestions(Context.OPERATOR_VALUE, scope, provider)
labels = [s.label for s in suggestions]
# Base suggestions
assert "col." in labels
assert "True" in labels
assert "False" in labels
# Column values from provider
assert '"draft"' in labels
assert '"pending"' in labels
assert '"approved"' in labels
def test_suggestions_rule_start(provider):
"""Test suggestions for RULE_START context."""
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = get_suggestions(Context.RULE_START, scope, provider)
labels = [s.label for s in suggestions]
assert "style(" in labels
assert "format(" in labels
assert "format." in labels
def test_suggestions_none_context(provider):
"""Test that NONE context returns empty suggestions."""
scope = DetectedScope()
suggestions = get_suggestions(Context.NONE, scope, provider)
assert suggestions == []
# =============================================================================
# Engine Integration Tests
# =============================================================================
def test_i_can_get_completions_for_style_preset(provider):
"""Test complete flow for style preset completion."""
text = 'column amount:\n style("'
cursor = Position(line=1, ch=11)
result = get_completions(text, cursor, provider)
assert not result.is_empty
labels = [s.label for s in result.suggestions]
assert "primary" in labels
assert "error" in labels
def test_i_can_get_completions_filters_by_prefix(provider):
"""Test that completions are filtered by prefix."""
text = 'column amount:\n style("err'
cursor = Position(line=1, ch=14)
result = get_completions(text, cursor, provider)
labels = [s.label for s in result.suggestions]
assert "error" in labels
assert "primary" not in labels
def test_i_can_get_completions_returns_correct_positions(provider):
"""Test that completion result has correct from/to positions."""
text = 'column amount:\n style("err'
cursor = Position(line=1, ch=14) # After "err"
result = get_completions(text, cursor, provider)
# from_pos should be at start of "err"
assert result.from_pos.line == 1
assert result.from_pos.ch == 11 # Start of "err"
# to_pos should be at end of "err"
assert result.to_pos.line == 1
assert result.to_pos.ch == 14 # End of "err"
def test_i_can_get_completions_at_scope_start(provider):
"""Test completions at the start of a new line (scope keywords)."""
text = ""
cursor = Position(line=0, ch=0)
result = get_completions(text, cursor, provider)
labels = [s.label for s in result.suggestions]
assert "column" in labels
assert "row" in labels
assert "cell" in labels
def test_i_can_get_completions_for_column_names(provider):
"""Test completions for column names."""
text = "column "
cursor = Position(line=0, ch=7)
result = get_completions(text, cursor, provider)
labels = [s.label for s in result.suggestions]
assert "id" in labels
assert "amount" in labels
assert "status" in labels
def test_i_can_get_completions_in_comment_returns_empty(provider):
"""Test that completions in comment are empty."""
text = "column amount:\n # comment"
cursor = Position(line=1, ch=15)
result = get_completions(text, cursor, provider)
assert result.is_empty
def test_i_can_create_formatting_completion_engine(provider):
"""Test that FormattingCompletionEngine can be instantiated."""
engine = FormattingCompletionEngine(provider)
assert engine.provider == provider
def test_i_can_use_engine_detect_scope(provider):
"""Test engine's detect_scope method."""
engine = FormattingCompletionEngine(provider)
text = "column amount:\n style()"
scope = engine.detect_scope(text, current_line=1)
assert scope.scope_type == "column"
assert scope.column_name == "amount"
def test_i_can_use_engine_detect_context(provider):
"""Test engine's detect_context method."""
engine = FormattingCompletionEngine(provider)
text = "column amount:\n style("
cursor = Position(line=1, ch=10)
scope = DetectedScope(scope_type="column", column_name="amount")
context = engine.detect_context(text, cursor, scope)
assert context == Context.STYLE_ARGS