Improved auto-completion engine for formatting parameters and added support for absolute value in number formatting.

This commit is contained in:
2026-03-11 22:39:52 +01:00
parent e704dad62c
commit 3105b72ac2
11 changed files with 438 additions and 39 deletions

View File

@@ -431,14 +431,62 @@ def test_context_format_type_after_dot():
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)
def test_context_format_number_absolute_param_suggests_boolean():
"""Test that 'absolute=' in format.number() triggers BOOLEAN_VALUE context."""
text = "column amount:\n format.number(absolute="
cursor = Position(line=1, ch=len(" format.number(absolute="))
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_PARAM_DATE
assert context == Context.BOOLEAN_VALUE
def test_context_format_type_while_typing():
"""Test FORMAT_TYPE context while typing a format type name after 'format.'."""
text = "column amount:\n format.dat"
cursor = Position(line=1, ch=14)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_TYPE
@pytest.mark.parametrize("line,expected_context", [
(' format.number(', Context.FORMAT_PARAM_NUMBER),
(' format.date(', Context.FORMAT_PARAM_DATE),
(' format.boolean(', Context.FORMAT_PARAM_BOOLEAN),
(' format.text(', Context.FORMAT_PARAM_TEXT),
(' format.enum(', Context.FORMAT_PARAM_ENUM),
(' format.constant(', Context.FORMAT_PARAM_CONSTANT),
])
def test_i_can_detect_format_param_context_after_open_paren(line, expected_context):
"""Test FORMAT_PARAM_* context right after the opening parenthesis."""
text = f"column amount:\n{line}"
cursor = Position(line=1, ch=len(line))
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == expected_context
@pytest.mark.parametrize("line,expected_context", [
(' format.number(prefix="",', Context.FORMAT_PARAM_NUMBER),
(' format.date(format="%Y",', Context.FORMAT_PARAM_DATE),
(' format.boolean(true_value="Yes",', Context.FORMAT_PARAM_BOOLEAN),
(' format.text(transform="upper",', Context.FORMAT_PARAM_TEXT),
(' format.enum(source="label",', Context.FORMAT_PARAM_ENUM),
(' format.constant(value="N/A",', Context.FORMAT_PARAM_CONSTANT),
])
def test_i_can_detect_format_param_context_after_comma(line, expected_context):
"""Test FORMAT_PARAM_* context after a comma (second parameter position)."""
text = f"column amount:\n{line}"
cursor = Position(line=1, ch=len(line))
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == expected_context
def test_context_format_param_text():
@@ -682,11 +730,12 @@ def test_suggestions_format_type(provider):
suggestions = engine.get_suggestions(Context.FORMAT_TYPE, scope, "")
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
assert "number(" in labels
assert "date(" in labels
assert "boolean(" in labels
assert "text(" in labels
assert "enum(" in labels
assert "constant(" in labels
def test_suggestions_operators(provider):
@@ -761,6 +810,62 @@ def test_suggestions_rule_start(provider):
assert "format." in labels
def test_suggestions_format_param_number(provider):
"""Test suggestions for FORMAT_PARAM_NUMBER context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_NUMBER, scope, "")
labels = [s.label for s in suggestions]
assert "prefix=" in labels
assert "suffix=" in labels
assert "precision=" in labels
assert "thousands_sep=" in labels
assert "decimal_sep=" in labels
assert "multiplier=" in labels
assert "absolute=" in labels
def test_suggestions_format_param_boolean(provider):
"""Test suggestions for FORMAT_PARAM_BOOLEAN context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_BOOLEAN, scope, "")
labels = [s.label for s in suggestions]
assert "true_value=" in labels
assert "false_value=" in labels
assert "null_value=" in labels
def test_suggestions_format_param_enum(provider):
"""Test suggestions for FORMAT_PARAM_ENUM context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_ENUM, scope, "")
labels = [s.label for s in suggestions]
assert "source=" in labels
assert "default=" in labels
assert "allow_empty=" in labels
assert "empty_label=" in labels
assert "order_by=" in labels
def test_suggestions_format_param_constant(provider):
"""Test suggestions for FORMAT_PARAM_CONSTANT context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_CONSTANT, scope, "")
labels = [s.label for s in suggestions]
assert "value=" in labels
def test_suggestions_none_context(provider):
"""Test that NONE context returns empty suggestions."""
engine = FormattingCompletionEngine(provider, "app.orders")
@@ -853,6 +958,64 @@ def test_i_can_get_completions_in_comment_returns_empty(provider):
assert result.is_empty
@pytest.mark.parametrize("text,cursor_line,used_param,remaining_param", [
(
'column amount:\n format.number(prefix="",',
1, "prefix=", "suffix=",
),
(
'column amount:\n format.number(prefix="", suffix=" CHF",',
1, "suffix=", "precision=",
),
(
'column amount:\n style(bold=True,',
1, "bold=", "italic=",
),
(
'column amount:\n style(background_color="red",',
1, "background_color=", "color=",
),
])
def test_i_cannot_get_already_used_param_in_suggestions(provider, text, cursor_line, used_param, remaining_param):
"""Test that already-used parameters are not suggested again."""
cursor = Position(line=cursor_line, ch=len(text.split("\n")[cursor_line]))
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert used_param not in labels, f"'{used_param}' should not appear after being used"
assert remaining_param in labels, f"'{remaining_param}' should still appear"
def test_i_can_get_completions_while_typing_param_name(provider):
"""Test that suggestions appear while typing a partial parameter name.
Typing 'pre' after a comma should suggest 'precision=' (filtered by prefix)
and exclude 'prefix=' (already used).
"""
text = 'column amount:\n format.number(prefix="", pre'
cursor = Position(line=1, ch=len(text.split("\n")[1]))
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert "precision=" in labels
assert "prefix=" not in labels
def test_i_can_get_completions_condition_unaffected_by_param_filtering(provider):
"""Test that condition suggestions are not affected by parameter filtering."""
text = 'column amount:\n style(bold=True) if '
cursor = Position(line=1, ch=len(text.split("\n")[1]))
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert "value" in labels
assert "col." in labels
assert "not" in labels
def test_i_can_create_formatting_completion_engine(provider):
"""Test that FormattingCompletionEngine can be instantiated."""
engine = FormattingCompletionEngine(provider, "app.orders")
@@ -882,3 +1045,64 @@ def test_i_can_use_engine_detect_context(provider):
context = engine.detect_context(text, cursor, scope)
assert context == Context.STYLE_ARGS
@pytest.mark.parametrize("format_type,expected_params", [
("number", ["prefix=", "suffix=", "precision="]),
("date", ["format="]),
("boolean", ["true_value=", "false_value=", "null_value="]),
("text", ["transform=", "max_length=", "ellipsis="]),
("enum", ["source=", "default=", "allow_empty="]),
("constant", ["value="]),
])
def test_i_can_get_completions_for_format_params(provider, format_type, expected_params):
"""Test that correct parameters are suggested after 'format.<type>('."""
text = f"column amount:\n format.{format_type}("
cursor = Position(line=1, ch=len(f" format.{format_type}("))
result = get_completions(text, cursor, provider, "app.orders")
assert not result.is_empty
labels = [s.label for s in result.suggestions]
for param in expected_params:
assert param in labels, f"Expected '{param}' in suggestions for format.{format_type}("
def test_i_can_get_completions_for_format_type_after_dot(provider):
"""Test that format types are suggested after 'format.'
The dot must be treated as a word delimiter so that the prefix
is empty after the dot, allowing all FORMAT_TYPE suggestions through.
Without this, the prefix 'format.' would filter out 'number(', 'date(', etc.
"""
text = "column amount:\n format."
cursor = Position(line=1, ch=11)
result = get_completions(text, cursor, provider, "app.orders")
assert not result.is_empty
labels = [s.label for s in result.suggestions]
assert "number(" in labels
assert "date(" in labels
assert "boolean(" in labels
assert "text(" in labels
assert "enum(" in labels
assert "constant(" in labels
def test_i_can_get_completions_for_format_type_filtered_by_prefix(provider):
"""Test that format types are filtered by partial input after 'format.'
Typing 'format.dat' should only suggest 'date(', not the other types.
The dot delimiter ensures the prefix is 'dat', not 'format.dat'.
"""
text = "column amount:\n format.dat"
cursor = Position(line=1, ch=14)
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert "date(" in labels
assert "number(" not in labels
assert "boolean(" not in labels
assert "text(" not in labels

View File

@@ -47,6 +47,30 @@ class TestNumberFormatter:
)
assert resolver.resolve(formatter, value) == expected
@pytest.mark.parametrize("value,expected", [
(-42, "42"),
(-1.5, "2"), # rounds abs(-1.5) = 1.5 → 2
(10, "10"),
(-0.0, "0"),
])
def test_i_can_format_number_with_absolute(self, value, expected):
"""Test that absolute=True strips the sign before formatting."""
resolver = FormatterResolver()
formatter = NumberFormatter(absolute=True)
assert resolver.resolve(formatter, value) == expected
def test_i_can_combine_absolute_with_prefix_suffix(self):
"""Test that absolute=True works together with prefix and suffix."""
resolver = FormatterResolver()
formatter = NumberFormatter(absolute=True, prefix="$", suffix=" USD", precision=2)
assert resolver.resolve(formatter, -1234.56) == "$1234.56 USD"
def test_i_can_format_negative_number_without_absolute(self):
"""Test that absolute=False (default) preserves the sign."""
resolver = FormatterResolver()
formatter = NumberFormatter(absolute=False, precision=2)
assert resolver.resolve(formatter, -42.5) == "-42.50"
def test_number_none_value(self):
resolver = FormatterResolver()
formatter = NumberFormatter(precision=2)