feat: intellisense for the FileMaker calculation editor#198
Merged
feat: intellisense for the FileMaker calculation editor#198
Conversation
Introduce source.fmcalc grammar covering comments, strings with escapes, numeric literals, $/$$ variables, Table::Field references, control forms (Let/Case/If/While/Choose), operators, and builtin functions grouped by category. Custom function calls scope as entity.name.function. Rename FmScriptRegistryOptions to FmLanguageRegistryOptions and serve both source.fmscript and source.fmcalc with per-scope Lazy caches. Point the field Calculation Editor at the new scope so it no longer mis-tokenizes the first identifier as a script step. Refactor fmscript bracket-params to include source.fmcalc rather than duplicating a hardcoded subset. Add tokenization tests asserting representative fixtures for both grammars, including cross-grammar resolution from script step brackets. Closes #193
Replace the hand-authored fmcalc.tmLanguage.json with FmCalcCatalog — a typed list of functions (name, category, signature, description), control forms (with snippet templates), constants, and word operators in SharpFM.Model. FmCalcGrammarBuilder turns the catalog into the TextMate grammar JSON; FmLanguageRegistryOptions feeds that JSON into TextMateSharp at first use and caches the result. The catalog is the single source of truth — the upcoming completion provider will read from the same lists, so grammar and intellisense can never drift. No build-time codegen, no committed grammar artifact: function-list edits are a one-line catalog change. Tests: catalog has no duplicates, every function has signature and description, builder emits valid JSON containing every function name, control-form snippets carry tab-stops.
Add an AvaloniaEdit completion window to CalculationEditorWindow, backed by FmCalcCompletionProvider reading from FmCalcCatalog. Items carry a signature + one-line description in their tooltip; control forms (Let, Case, If, While, Choose) expand from snippet templates with the first parameter pre-selected for editing. Detect what's being typed and switch sources: - Identifier prefix → built-in functions, control forms, constants - After $ / $$ → variables harvested from the document - After Table:: → fields of the current table - Inside a string or // comment → suppress completions CalcCompletionContextProvider supplies the document-derived data: $variables are scraped from the calculation text (bag-of-names, no Let-scope analysis — calcs are short enough that this covers the common case), and field names come from the FmTable passed in by TableEditorViewModel. Table enumeration is stubbed out until a schema container exists that can list table occurrences. Tests: 14 cases for the provider's context detection and filtering, 6 for the document-aware context provider.
Pre-build the static identifier completion lists once at type init and reuse them across every editor instance. Per-keystroke work is now a prefix-filtered copy of references — no FmScriptCompletionData allocations, no per-trigger snippet synthesis. AllBuiltinIdentifierCompletions on FmCalcCompletionProvider and AllStepNameCompletions on FmScriptCompletionProvider are public so future callers (e.g. embedded calc inside script step brackets) can share the same list. Gate OnTextEntered on identifier-starting characters so typing ( ; , space etc. no longer pops a fresh CompletionWindow with the entire catalog. Calc adds $ to the trigger set for variable references; scripts stick to letters and underscore. Existing windows still update as the user types (early return when one is already open), so no functional change to filter-as-you-type. Format completion descriptions as "signature — description" rather than two lines so the tooltip stays compact.
Generalise the catalog so any function parameter can declare a list
of accepted keywords. FmCalcFunction now carries an
IReadOnlyList<FmCalcFunctionParam>; each param can optionally hold an
IReadOnlyList<FmCalcEnumValue> (Name + optional Description). The
completion provider walks back from the caret to find the enclosing
function call and the current argument index, looks up that param's
values in a per-(function, argIndex) cache built once at type init,
and offers them filtered by prefix.
Filled enum data:
- Get(parameter) — ~120 selector keywords with descriptions
- JSONSetElement(...; type) — JSONString/Number/Object/Array/Boolean/
Null/Raw
- TextStyleAdd / TextStyleRemove (style) — Plain, Bold, Italic, …
- CryptDigest (algorithm) — MD5, SHA1, SHA256, …
Other functions leave Params empty and behave as before. Detection
handles nested calls (innermost wins), strings (semicolons inside
quotes don't count), and short-circuits on // line comments.
Tests: 12 cases covering each enum surface plus the call-detection
helper directly.
The trigger gate added in 4dbb435 only fired on identifier-starting characters, so typing Get( or JSONSetElement(j;k;v; never opened the completion menu — even though the param-value mechanism added in a654fea had keywords ready to show. Treat ( and ; as argument-boundary triggers but only commit to opening the window when GetCompletions reports FunctionParam — that keeps spurious pops on ( after non-enum functions (Length, Sum, …) suppressed.
Derive Params from each function's Signature string at catalog build
time so JSONGetElement, Length, Round, etc. all carry positional
param info — no per-function hand-authoring. Functions with explicit
Params (Get's selectors, JSONSetElement's type, TextStyleAdd's
style, CryptDigest's algorithm) keep their existing entries; the
Add helper only falls back to the parser when Params wasn't passed.
Generate a Monaco-style snippet for every function with params:
"Func ( ${1:p1} ; ${2:p2} )". Variadic markers ({; field...}) are
trimmed off — those become a single named stop. For params with
ValidValues, use the first value as the placeholder so users see a
real example (Get → AccountName) instead of the generic param name.
Switch FmScriptCompletionData.Complete to AvaloniaEdit's Snippet
engine. Each ${N:...} becomes a SnippetReplaceableTextElement, which
gives Tab navigation across all stops natively. $0 maps to a
SnippetCaretElement marking the post-snippet caret position.
Single-stop control forms (Let, Case, …) keep working — the engine
handles the degenerate case the same way the manual select/replace
code did.
Tests: 9 cases for the parser (positional, variadic, edge cases),
plus catalog-level guards that JSONGetElement gets parsed params and
Get's explicit ValidValues survive the auto-derivation.
Three small changes to make the script editor responsive on long scripts. Bisected via runtime perf flags; reverting any of these brings the lag back. 1. fmscript bracket-params no longer includes source.fmcalc. The embedded calc grammar expands to ~28 patterns including a 16-way builtin-function alternation (~150 names) — every line inside [...] tokenized through all of it on every layout. Restore the original 9 simple patterns (string, variable, semicolon, number, boolean-value, operator, function-call, field-reference, param-label). Calc highlighting inside step brackets stays functional via the lighter native rules; the calculation editor still uses source.fmcalc for full per-category coloring. 2. FmScriptCompletionData.Content caches its TextBlock instead of allocating one per property access. AvaloniaEdit's CompletionList re-reads Content during virtualization and filter narrowing — previously we were leaking a fresh TextBlock on each read. 3. Script editor only auto-pops the completion window once the identifier prefix is at least 2 characters. Avoids the 60-item-empty-prefix popup on the very first keystroke, which is the largest CompletionWindow build cost during normal typing. Tests: grammar tokenization tests updated to assert the .fmscript scopes that bracket-params now produces (entity.name.type.fmscript and friends) instead of the .fmcalc scopes the cross-grammar include used to surface.
The cached TextBlock introduced in f37e5a5 violates Avalonia's
single-parent rule when AvaloniaEdit's CompletionList reuses the
item across multiple ContentPresenter instances — produced an
InvalidOperationException ('already has a visual parent
ContentPresenter') the moment the completion window opened.
Return the bare Text string instead. ContentPresenter wraps it in
its own TextBlock automatically and owns that wrapper exclusively,
so reuse across virtualised rows is safe. Drops one allocation per
Content access compared to the original 'new TextBlock { Text = .. }'
factory pattern.
When the caret sits in a freeform value position inside a step's bracket params (e.g. Set Variable [ Value: Le... ]) hand completion off to FmCalcCompletionProvider so the user gets the same functions, control forms, constants, and per-arg keyword lists as the calculation editor. Two new contexts on the script side — CalcExpression (general identifier completion) and CalcParamValue (function-arg keywords). The controller anchors both at the identifier prefix start, and treats CalcParamValue as the only context worth popping on ( and ; triggers. Mirrors the calc editor's gating logic. Detection: when DetectEnclosingCall finds an unmatched ( ahead of the caret, defer immediately — those ; separators belong to the call, not to the step's flat param list (otherwise something like JSONSetElement(j;k;v; never reached the labeled-value fallback). A second deferral fires for labeled params with no enum values. Field-ref and $variable completions inside script step brackets are still empty — wiring a CalcCompletionContextProvider through the script editor (with document text and any current-table reference) is a follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FmCalcCatalogso highlighting and completion stay in sync with a single catalog of functions, parameters, and enum values.(and;.Test plan
dotnet test— 1244 tests passing, including new coverage forFmCalcCatalog,FmCalcCompletionProvider,FmCalcSignatureParser,CalcCompletionContextProvider, and grammar tokenization.(/;.