Improved auto-completion engine for formatting parameters and added support for absolute value in number formatting.
This commit is contained in:
5
.idea/MyFastHtml.iml
generated
5
.idea/MyFastHtml.iml
generated
@@ -4,6 +4,11 @@
|
|||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.myFastHtmlDb" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/benchmarks" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/examples" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/images" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/src/.myFastHtmlDb" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.12.3 WSL (Ubuntu-24.04): (/home/kodjo/.virtualenvs/MyFastHtml/bin/python)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.12.3 WSL (Ubuntu-24.04): (/home/kodjo/.virtualenvs/MyFastHtml/bin/python)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ Formats numbers, currencies, and percentages.
|
|||||||
| `decimal_sep` | string | `"."` | Decimal separator (e.g., ".", ",") |
|
| `decimal_sep` | string | `"."` | Decimal separator (e.g., ".", ",") |
|
||||||
| `precision` | int | `0` | Number of decimal places |
|
| `precision` | int | `0` | Number of decimal places |
|
||||||
| `multiplier` | number | `1` | Multiply value before display (e.g., 100 for %) |
|
| `multiplier` | number | `1` | Multiply value before display (e.g., 100 for %) |
|
||||||
|
| `absolute` | boolean | `False` | Display absolute value (no sign) |
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
@@ -349,6 +350,10 @@ column rate:
|
|||||||
# Large numbers with thousands separator
|
# Large numbers with thousands separator
|
||||||
column population:
|
column population:
|
||||||
format.number(thousands_sep=",", precision=0)
|
format.number(thousands_sep=",", precision=0)
|
||||||
|
|
||||||
|
# Absolute value - display variance without sign
|
||||||
|
column variance:
|
||||||
|
format.number(absolute=True, suffix=" €", precision=2)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Date Formatter
|
#### Date Formatter
|
||||||
@@ -1085,6 +1090,14 @@ DaisyUI 5 color presets:
|
|||||||
|
|
||||||
### Formatter Types
|
### Formatter Types
|
||||||
|
|
||||||
|
Possible formatter types :
|
||||||
|
- `number`
|
||||||
|
- `date`
|
||||||
|
- `boolean`
|
||||||
|
- `text`
|
||||||
|
- `enum`
|
||||||
|
- `constant`
|
||||||
|
|
||||||
#### Number Formatter
|
#### Number Formatter
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
@@ -1095,6 +1108,7 @@ DaisyUI 5 color presets:
|
|||||||
| `decimal_sep` | string | `"."` | Decimal separator |
|
| `decimal_sep` | string | `"."` | Decimal separator |
|
||||||
| `precision` | int | `0` | Decimal places |
|
| `precision` | int | `0` | Decimal places |
|
||||||
| `multiplier` | number | `1` | Multiply before display |
|
| `multiplier` | number | `1` | Multiply before display |
|
||||||
|
| `absolute` | boolean | `False` | Display absolute value (no sign) |
|
||||||
|
|
||||||
#### Date Formatter
|
#### Date Formatter
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ and other common operations used by completion engines.
|
|||||||
from .types import Position, WordRange
|
from .types import Position, WordRange
|
||||||
|
|
||||||
# Delimiters used to detect word boundaries
|
# 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:
|
def get_line_at(text: str, line_number: int) -> str:
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class NumberFormatter(Formatter):
|
|||||||
decimal_sep: str = "."
|
decimal_sep: str = "."
|
||||||
precision: int = 0
|
precision: int = 0
|
||||||
multiplier: float = 1.0
|
multiplier: float = 1.0
|
||||||
|
absolute: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ Completion engine for the formatting DSL.
|
|||||||
Implements the BaseCompletionEngine for DataGrid formatting rules.
|
Implements the BaseCompletionEngine for DataGrid formatting rules.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from myfasthtml.core.dsl import utils
|
||||||
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
||||||
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
|
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
|
||||||
from myfasthtml.core.utils import make_safe_id
|
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 .contexts import Context, DetectedScope, detect_scope, detect_context
|
||||||
from .provider import DatagridMetadataProvider
|
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")
|
logger = logging.getLogger("FormattingCompletionEngine")
|
||||||
|
|
||||||
class FormattingCompletionEngine(BaseCompletionEngine):
|
class FormattingCompletionEngine(BaseCompletionEngine):
|
||||||
@@ -37,6 +50,55 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
|||||||
self.table_name: str = table_name # current table name
|
self.table_name: str = table_name # current table name
|
||||||
self._id = "formatting_completion_engine#" + make_safe_id(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:
|
def detect_scope(self, text: str, current_line: int) -> DetectedScope:
|
||||||
"""
|
"""
|
||||||
Detect the current scope by scanning previous lines.
|
Detect the current scope by scanning previous lines.
|
||||||
@@ -146,12 +208,24 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
|||||||
case Context.FORMAT_TYPE:
|
case Context.FORMAT_TYPE:
|
||||||
return presets.FORMAT_TYPES
|
return presets.FORMAT_TYPES
|
||||||
|
|
||||||
|
case Context.FORMAT_PARAM_NUMBER:
|
||||||
|
return presets.FORMAT_PARAMS_NUMBER
|
||||||
|
|
||||||
case Context.FORMAT_PARAM_DATE:
|
case Context.FORMAT_PARAM_DATE:
|
||||||
return presets.FORMAT_PARAMS_DATE
|
return presets.FORMAT_PARAMS_DATE
|
||||||
|
|
||||||
|
case Context.FORMAT_PARAM_BOOLEAN:
|
||||||
|
return presets.FORMAT_PARAMS_BOOLEAN
|
||||||
|
|
||||||
case Context.FORMAT_PARAM_TEXT:
|
case Context.FORMAT_PARAM_TEXT:
|
||||||
return presets.FORMAT_PARAMS_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
|
# After style/format
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
@@ -45,8 +45,12 @@ class Context(Enum):
|
|||||||
# Format contexts
|
# Format contexts
|
||||||
FORMAT_PRESET = auto() # Inside format("): preset names
|
FORMAT_PRESET = auto() # Inside format("): preset names
|
||||||
FORMAT_TYPE = auto() # After "format.": number, date, etc.
|
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_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_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/format
|
||||||
AFTER_STYLE_OR_FORMAT = auto() # After ")": style(, format(, if
|
AFTER_STYLE_OR_FORMAT = auto() # After ")": style(, format(, if
|
||||||
@@ -234,18 +238,21 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
|
|||||||
if re.search(r'style\s*\(\s*"[^"]*$', line_to_cursor):
|
if re.search(r'style\s*\(\s*"[^"]*$', line_to_cursor):
|
||||||
return Context.STYLE_PRESET
|
return Context.STYLE_PRESET
|
||||||
|
|
||||||
# After style( without quote - args (preset or params)
|
# After style( without quote - args (preset or params), including partial word
|
||||||
if re.search(r"style\s*\(\s*$", line_to_cursor):
|
if re.search(r"style\s*\(\s*[a-zA-Z_]*$", line_to_cursor):
|
||||||
return Context.STYLE_ARGS
|
return Context.STYLE_ARGS
|
||||||
|
|
||||||
# After comma in style() - params
|
# After comma in style() - params, including partial word
|
||||||
if re.search(r"style\s*\([^)]*,\s*$", line_to_cursor):
|
if re.search(r"style\s*\([^)]*,\s*[a-zA-Z_]*$", line_to_cursor):
|
||||||
return Context.STYLE_PARAM
|
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):
|
if re.search(r"style\s*\([^)]*(?:bold|italic|underline|strikethrough)\s*=\s*$", line_to_cursor):
|
||||||
return Context.BOOLEAN_VALUE
|
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):
|
if re.search(r"style\s*\([^)]*(?:color|background_color)\s*=\s*$", line_to_cursor):
|
||||||
return Context.COLOR_VALUE
|
return Context.COLOR_VALUE
|
||||||
|
|
||||||
@@ -253,30 +260,46 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
|
|||||||
# Format contexts
|
# Format contexts
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
# After "format." - type
|
# After "format." - type (cursor right after dot or while typing type name)
|
||||||
if re.search(r"format\s*\.\s*$", line_to_cursor):
|
if re.search(r"format\s*\.\s*[a-zA-Z]*$", line_to_cursor):
|
||||||
return Context.FORMAT_TYPE
|
return Context.FORMAT_TYPE
|
||||||
|
|
||||||
# Inside format(" - preset
|
# Inside format(" - preset
|
||||||
if re.search(r'format\s*\(\s*"[^"]*$', line_to_cursor):
|
if re.search(r'format\s*\(\s*"[^"]*$', line_to_cursor):
|
||||||
return Context.FORMAT_PRESET
|
return Context.FORMAT_PRESET
|
||||||
|
|
||||||
# Inside format.date( - params
|
# After format= in format.date (checked before FORMAT_PARAM_DATE)
|
||||||
if re.search(r"format\s*\.\s*date\s*\(\s*$", line_to_cursor):
|
|
||||||
return Context.FORMAT_PARAM_DATE
|
|
||||||
|
|
||||||
# After format= in format.date
|
|
||||||
if re.search(r"format\s*\.\s*date\s*\([^)]*format\s*=\s*$", line_to_cursor):
|
if re.search(r"format\s*\.\s*date\s*\([^)]*format\s*=\s*$", line_to_cursor):
|
||||||
return Context.DATE_FORMAT_VALUE
|
return Context.DATE_FORMAT_VALUE
|
||||||
|
|
||||||
# Inside format.text( - params
|
# After transform= in format.text (checked before FORMAT_PARAM_TEXT)
|
||||||
if re.search(r"format\s*\.\s*text\s*\(\s*$", line_to_cursor):
|
|
||||||
return Context.FORMAT_PARAM_TEXT
|
|
||||||
|
|
||||||
# After transform= in format.text
|
|
||||||
if re.search(r"format\s*\.\s*text\s*\([^)]*transform\s*=\s*$", line_to_cursor):
|
if re.search(r"format\s*\.\s*text\s*\([^)]*transform\s*=\s*$", line_to_cursor):
|
||||||
return Context.TRANSFORM_VALUE
|
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
|
# After style/format - if or more style/format
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -164,27 +164,56 @@ STYLE_PARAMS: list[Suggestion] = [
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
FORMAT_TYPES: list[Suggestion] = [
|
FORMAT_TYPES: list[Suggestion] = [
|
||||||
Suggestion("number", "Number formatting", "type"),
|
Suggestion("number(", "Number formatting", "type"),
|
||||||
Suggestion("date", "Date formatting", "type"),
|
Suggestion("date(", "Date formatting", "type"),
|
||||||
Suggestion("boolean", "Boolean formatting", "type"),
|
Suggestion("boolean(", "Boolean formatting", "type"),
|
||||||
Suggestion("text", "Text transformation", "type"),
|
Suggestion("text(", "Text transformation", "type"),
|
||||||
Suggestion("enum", "Value mapping", "type"),
|
Suggestion("enum(", "Value mapping", "type"),
|
||||||
|
Suggestion("constant(", "Constant value", "type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Format Parameters by 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] = [
|
FORMAT_PARAMS_DATE: list[Suggestion] = [
|
||||||
Suggestion("format=", "strftime pattern", "parameter"),
|
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] = [
|
FORMAT_PARAMS_TEXT: list[Suggestion] = [
|
||||||
Suggestion("transform=", "Text transformation", "parameter"),
|
Suggestion("transform=", "Text transformation", "parameter"),
|
||||||
Suggestion("max_length=", "Maximum length", "parameter"),
|
Suggestion("max_length=", "Maximum length", "parameter"),
|
||||||
Suggestion("ellipsis=", "Truncation suffix", "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
|
# Condition Keywords
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ class DSLTransformer(Transformer):
|
|||||||
|
|
||||||
def _filter_number_kwargs(self, kwargs: dict) -> dict:
|
def _filter_number_kwargs(self, kwargs: dict) -> dict:
|
||||||
"""Filter kwargs for NumberFormatter."""
|
"""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}
|
return {k: v for k, v in kwargs.items() if k in valid_keys}
|
||||||
|
|
||||||
def _filter_date_kwargs(self, kwargs: dict) -> dict:
|
def _filter_date_kwargs(self, kwargs: dict) -> dict:
|
||||||
|
|||||||
@@ -39,8 +39,11 @@ class NumberFormatterResolver(BaseFormatterResolver):
|
|||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Convert to float and apply multiplier
|
# Convert to float, apply absolute value if requested, then multiplier
|
||||||
num = float(value) * formatter.multiplier
|
num = float(value)
|
||||||
|
if formatter.absolute:
|
||||||
|
num = abs(num)
|
||||||
|
num = num * formatter.multiplier
|
||||||
|
|
||||||
# Round to precision
|
# Round to precision
|
||||||
if formatter.precision > 0:
|
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", "."),
|
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),
|
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),
|
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:
|
else:
|
||||||
return NumberFormatter(
|
return NumberFormatter(
|
||||||
@@ -107,6 +111,7 @@ class NumberFormatterResolver(BaseFormatterResolver):
|
|||||||
decimal_sep=preset.get("decimal_sep", "."),
|
decimal_sep=preset.get("decimal_sep", "."),
|
||||||
precision=preset.get("precision", 0),
|
precision=preset.get("precision", 0),
|
||||||
multiplier=preset.get("multiplier", 1.0),
|
multiplier=preset.get("multiplier", 1.0),
|
||||||
|
absolute=preset.get("absolute", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -431,14 +431,62 @@ def test_context_format_type_after_dot():
|
|||||||
assert context == Context.FORMAT_TYPE
|
assert context == Context.FORMAT_TYPE
|
||||||
|
|
||||||
|
|
||||||
def test_context_format_param_date():
|
def test_context_format_number_absolute_param_suggests_boolean():
|
||||||
"""Test FORMAT_PARAM_DATE context inside format.date()."""
|
"""Test that 'absolute=' in format.number() triggers BOOLEAN_VALUE context."""
|
||||||
text = "column amount:\n format.date("
|
text = "column amount:\n format.number(absolute="
|
||||||
cursor = Position(line=1, ch=16)
|
cursor = Position(line=1, ch=len(" format.number(absolute="))
|
||||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||||
|
|
||||||
context = detect_context(text, cursor, scope)
|
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():
|
def test_context_format_param_text():
|
||||||
@@ -682,11 +730,12 @@ def test_suggestions_format_type(provider):
|
|||||||
suggestions = engine.get_suggestions(Context.FORMAT_TYPE, scope, "")
|
suggestions = engine.get_suggestions(Context.FORMAT_TYPE, scope, "")
|
||||||
labels = [s.label for s in suggestions]
|
labels = [s.label for s in suggestions]
|
||||||
|
|
||||||
assert "number" in labels
|
assert "number(" in labels
|
||||||
assert "date" in labels
|
assert "date(" in labels
|
||||||
assert "boolean" in labels
|
assert "boolean(" in labels
|
||||||
assert "text" in labels
|
assert "text(" in labels
|
||||||
assert "enum" in labels
|
assert "enum(" in labels
|
||||||
|
assert "constant(" in labels
|
||||||
|
|
||||||
|
|
||||||
def test_suggestions_operators(provider):
|
def test_suggestions_operators(provider):
|
||||||
@@ -761,6 +810,62 @@ def test_suggestions_rule_start(provider):
|
|||||||
assert "format." in labels
|
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):
|
def test_suggestions_none_context(provider):
|
||||||
"""Test that NONE context returns empty suggestions."""
|
"""Test that NONE context returns empty suggestions."""
|
||||||
engine = FormattingCompletionEngine(provider, "app.orders")
|
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||||
@@ -853,6 +958,64 @@ def test_i_can_get_completions_in_comment_returns_empty(provider):
|
|||||||
assert result.is_empty
|
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):
|
def test_i_can_create_formatting_completion_engine(provider):
|
||||||
"""Test that FormattingCompletionEngine can be instantiated."""
|
"""Test that FormattingCompletionEngine can be instantiated."""
|
||||||
engine = FormattingCompletionEngine(provider, "app.orders")
|
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)
|
context = engine.detect_context(text, cursor, scope)
|
||||||
|
|
||||||
assert context == Context.STYLE_ARGS
|
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
|
||||||
|
|||||||
@@ -47,6 +47,30 @@ class TestNumberFormatter:
|
|||||||
)
|
)
|
||||||
assert resolver.resolve(formatter, value) == expected
|
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):
|
def test_number_none_value(self):
|
||||||
resolver = FormatterResolver()
|
resolver = FormatterResolver()
|
||||||
formatter = NumberFormatter(precision=2)
|
formatter = NumberFormatter(precision=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user