Improved auto-completion engine for formatting parameters and added support for absolute value in number formatting.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -79,6 +79,7 @@ class NumberFormatter(Formatter):
|
||||
decimal_sep: str = "."
|
||||
precision: int = 0
|
||||
multiplier: float = 1.0
|
||||
absolute: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user