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

@@ -8,7 +8,7 @@ and other common operations used by completion engines.
from .types import Position, WordRange
# Delimiters used to detect word boundaries
DELIMITERS = set('"\' ()[]{}=,:<>!\t\n\r')
DELIMITERS = set('"\' ()[]{}=,:<>!.\t\n\r')
def get_line_at(text: str, line_number: int) -> str:

View File

@@ -79,6 +79,7 @@ class NumberFormatter(Formatter):
decimal_sep: str = "."
precision: int = 0
multiplier: float = 1.0
absolute: bool = False
@dataclass

View File

@@ -4,7 +4,9 @@ Completion engine for the formatting DSL.
Implements the BaseCompletionEngine for DataGrid formatting rules.
"""
import logging
import re
from myfasthtml.core.dsl import utils
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
@@ -12,6 +14,17 @@ from . import presets
from .contexts import Context, DetectedScope, detect_scope, detect_context
from .provider import DatagridMetadataProvider
_PARAM_CONTEXTS = {
Context.STYLE_ARGS,
Context.STYLE_PARAM,
Context.FORMAT_PARAM_NUMBER,
Context.FORMAT_PARAM_DATE,
Context.FORMAT_PARAM_BOOLEAN,
Context.FORMAT_PARAM_TEXT,
Context.FORMAT_PARAM_ENUM,
Context.FORMAT_PARAM_CONSTANT,
}
logger = logging.getLogger("FormattingCompletionEngine")
class FormattingCompletionEngine(BaseCompletionEngine):
@@ -37,6 +50,55 @@ class FormattingCompletionEngine(BaseCompletionEngine):
self.table_name: str = table_name # current table name
self._id = "formatting_completion_engine#" + make_safe_id(table_name)
def get_completions(self, text: str, cursor: Position) -> CompletionResult:
"""
Get completions with parameter deduplication.
Extends the base implementation by filtering out parameters
that are already present in the current style() or format.*() call.
Args:
text: The full DSL document text
cursor: Cursor position
Returns:
CompletionResult with already-used parameters removed
"""
result = super().get_completions(text, cursor)
scope = self.detect_scope(text, cursor.line)
context = self.detect_context(text, cursor, scope)
if context in _PARAM_CONTEXTS:
line_to_cursor = utils.get_line_up_to_cursor(text, cursor)
used_params = self._extract_used_params(line_to_cursor)
if used_params:
result.suggestions = [
s for s in result.suggestions
if s.label.rstrip("=") not in used_params
]
return result
def _extract_used_params(self, line_to_cursor: str) -> set[str]:
"""
Extract parameter names already typed in the current function call.
Scans backwards from the last opening parenthesis to find all
'name=' patterns already present.
Args:
line_to_cursor: The current line up to the cursor position
Returns:
Set of parameter names already used (without the '=')
"""
paren_pos = line_to_cursor.rfind("(")
if paren_pos == -1:
return set()
args_text = line_to_cursor[paren_pos + 1:]
return set(re.findall(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\s*=", args_text))
def detect_scope(self, text: str, current_line: int) -> DetectedScope:
"""
Detect the current scope by scanning previous lines.
@@ -146,11 +208,23 @@ class FormattingCompletionEngine(BaseCompletionEngine):
case Context.FORMAT_TYPE:
return presets.FORMAT_TYPES
case Context.FORMAT_PARAM_NUMBER:
return presets.FORMAT_PARAMS_NUMBER
case Context.FORMAT_PARAM_DATE:
return presets.FORMAT_PARAMS_DATE
case Context.FORMAT_PARAM_BOOLEAN:
return presets.FORMAT_PARAMS_BOOLEAN
case Context.FORMAT_PARAM_TEXT:
return presets.FORMAT_PARAMS_TEXT
case Context.FORMAT_PARAM_ENUM:
return presets.FORMAT_PARAMS_ENUM
case Context.FORMAT_PARAM_CONSTANT:
return presets.FORMAT_PARAMS_CONSTANT
# =================================================================
# After style/format

View File

@@ -45,8 +45,12 @@ class Context(Enum):
# Format contexts
FORMAT_PRESET = auto() # Inside format("): preset names
FORMAT_TYPE = auto() # After "format.": number, date, etc.
FORMAT_PARAM_NUMBER = auto() # Inside format.number(): prefix=, suffix=, etc.
FORMAT_PARAM_DATE = auto() # Inside format.date(): format=
FORMAT_PARAM_BOOLEAN = auto() # Inside format.boolean(): true_value=, false_value=, etc.
FORMAT_PARAM_TEXT = auto() # Inside format.text(): transform=, etc.
FORMAT_PARAM_ENUM = auto() # Inside format.enum(): source=, default=, etc.
FORMAT_PARAM_CONSTANT = auto() # Inside format.constant(): value=
# After style/format
AFTER_STYLE_OR_FORMAT = auto() # After ")": style(, format(, if
@@ -234,17 +238,20 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
if re.search(r'style\s*\(\s*"[^"]*$', line_to_cursor):
return Context.STYLE_PRESET
# After style( without quote - args (preset or params)
if re.search(r"style\s*\(\s*$", line_to_cursor):
# After style( without quote - args (preset or params), including partial word
if re.search(r"style\s*\(\s*[a-zA-Z_]*$", line_to_cursor):
return Context.STYLE_ARGS
# After comma in style() - params
if re.search(r"style\s*\([^)]*,\s*$", line_to_cursor):
# After comma in style() - params, including partial word
if re.search(r"style\s*\([^)]*,\s*[a-zA-Z_]*$", line_to_cursor):
return Context.STYLE_PARAM
# After param= in style - check which param
# After boolean param= in style or format.number
if re.search(r"style\s*\([^)]*(?:bold|italic|underline|strikethrough)\s*=\s*$", line_to_cursor):
return Context.BOOLEAN_VALUE
if re.search(r"format\s*\.\s*\w+\s*\([^)]*absolute\s*=\s*$", line_to_cursor):
return Context.BOOLEAN_VALUE
if re.search(r"style\s*\([^)]*(?:color|background_color)\s*=\s*$", line_to_cursor):
return Context.COLOR_VALUE
@@ -253,29 +260,45 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
# Format contexts
# -------------------------------------------------------------------------
# After "format." - type
if re.search(r"format\s*\.\s*$", line_to_cursor):
# After "format." - type (cursor right after dot or while typing type name)
if re.search(r"format\s*\.\s*[a-zA-Z]*$", line_to_cursor):
return Context.FORMAT_TYPE
# Inside format(" - preset
if re.search(r'format\s*\(\s*"[^"]*$', line_to_cursor):
return Context.FORMAT_PRESET
# Inside format.date( - params
if re.search(r"format\s*\.\s*date\s*\(\s*$", line_to_cursor):
return Context.FORMAT_PARAM_DATE
# After format= in format.date
# After format= in format.date (checked before FORMAT_PARAM_DATE)
if re.search(r"format\s*\.\s*date\s*\([^)]*format\s*=\s*$", line_to_cursor):
return Context.DATE_FORMAT_VALUE
# Inside format.text( - params
if re.search(r"format\s*\.\s*text\s*\(\s*$", line_to_cursor):
return Context.FORMAT_PARAM_TEXT
# After transform= in format.text
# After transform= in format.text (checked before FORMAT_PARAM_TEXT)
if re.search(r"format\s*\.\s*text\s*\([^)]*transform\s*=\s*$", line_to_cursor):
return Context.TRANSFORM_VALUE
# Inside format.number( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*number\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_NUMBER
# Inside format.date( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*date\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_DATE
# Inside format.boolean( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*boolean\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_BOOLEAN
# Inside format.text( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*text\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_TEXT
# Inside format.enum( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*enum\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_ENUM
# Inside format.constant( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*constant\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_CONSTANT
# -------------------------------------------------------------------------
# After style/format - if or more style/format

View File

@@ -164,27 +164,56 @@ STYLE_PARAMS: list[Suggestion] = [
# =============================================================================
FORMAT_TYPES: list[Suggestion] = [
Suggestion("number", "Number formatting", "type"),
Suggestion("date", "Date formatting", "type"),
Suggestion("boolean", "Boolean formatting", "type"),
Suggestion("text", "Text transformation", "type"),
Suggestion("enum", "Value mapping", "type"),
Suggestion("number(", "Number formatting", "type"),
Suggestion("date(", "Date formatting", "type"),
Suggestion("boolean(", "Boolean formatting", "type"),
Suggestion("text(", "Text transformation", "type"),
Suggestion("enum(", "Value mapping", "type"),
Suggestion("constant(", "Constant value", "type"),
]
# =============================================================================
# Format Parameters by Type
# =============================================================================
FORMAT_PARAMS_NUMBER: list[Suggestion] = [
Suggestion("prefix=", "Text before the value", "parameter"),
Suggestion("suffix=", "Text after the value", "parameter"),
Suggestion("precision=", "Number of decimal places", "parameter"),
Suggestion("thousands_sep=", "Thousands separator", "parameter"),
Suggestion("decimal_sep=", "Decimal separator", "parameter"),
Suggestion("multiplier=", "Multiply value before display", "parameter"),
Suggestion("absolute=", "Display absolute value (no sign)", "parameter"),
]
FORMAT_PARAMS_DATE: list[Suggestion] = [
Suggestion("format=", "strftime pattern", "parameter"),
]
FORMAT_PARAMS_BOOLEAN: list[Suggestion] = [
Suggestion("true_value=", "Display text for True", "parameter"),
Suggestion("false_value=", "Display text for False", "parameter"),
Suggestion("null_value=", "Display text for null", "parameter"),
]
FORMAT_PARAMS_TEXT: list[Suggestion] = [
Suggestion("transform=", "Text transformation", "parameter"),
Suggestion("max_length=", "Maximum length", "parameter"),
Suggestion("ellipsis=", "Truncation suffix", "parameter"),
]
FORMAT_PARAMS_ENUM: list[Suggestion] = [
Suggestion("source=", "Column with labels", "parameter"),
Suggestion("default=", "Fallback display value", "parameter"),
Suggestion("allow_empty=", "Allow empty values", "parameter"),
Suggestion("empty_label=", "Label for empty values", "parameter"),
Suggestion("order_by=", "Sort order", "parameter"),
]
FORMAT_PARAMS_CONSTANT: list[Suggestion] = [
Suggestion("value=", "Constant value to display", "parameter"),
]
# =============================================================================
# Condition Keywords
# =============================================================================

View File

@@ -363,7 +363,7 @@ class DSLTransformer(Transformer):
def _filter_number_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for NumberFormatter."""
valid_keys = {"prefix", "suffix", "thousands_sep", "decimal_sep", "precision", "multiplier"}
valid_keys = {"prefix", "suffix", "thousands_sep", "decimal_sep", "precision", "multiplier", "absolute"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
def _filter_date_kwargs(self, kwargs: dict) -> dict:

View File

@@ -39,8 +39,11 @@ class NumberFormatterResolver(BaseFormatterResolver):
if value is None:
return ""
# Convert to float and apply multiplier
num = float(value) * formatter.multiplier
# Convert to float, apply absolute value if requested, then multiplier
num = float(value)
if formatter.absolute:
num = abs(num)
num = num * formatter.multiplier
# Round to precision
if formatter.precision > 0:
@@ -97,6 +100,7 @@ class NumberFormatterResolver(BaseFormatterResolver):
decimal_sep=formatter.decimal_sep if formatter.decimal_sep != "." else preset.get("decimal_sep", "."),
precision=formatter.precision if formatter.precision != 0 else preset.get("precision", 0),
multiplier=formatter.multiplier if formatter.multiplier != 1.0 else preset.get("multiplier", 1.0),
absolute=formatter.absolute if formatter.absolute else preset.get("absolute", False),
)
else:
return NumberFormatter(
@@ -107,6 +111,7 @@ class NumberFormatterResolver(BaseFormatterResolver):
decimal_sep=preset.get("decimal_sep", "."),
precision=preset.get("precision", 0),
multiplier=preset.get("multiplier", 1.0),
absolute=preset.get("absolute", False),
)