Skip to content

feat: intellisense for the FileMaker calculation editor#198

Merged
fuzzzerd merged 10 commits intomasterfrom
fuzzz/fmcalc-feature
Apr 26, 2026
Merged

feat: intellisense for the FileMaker calculation editor#198
fuzzzerd merged 10 commits intomasterfrom
fuzzz/fmcalc-feature

Conversation

@fuzzzerd
Copy link
Copy Markdown
Owner

@fuzzzerd fuzzzerd commented Apr 26, 2026

Summary

  • Adds a runtime-built TextMate grammar for FileMaker calculations, sourced from FmCalcCatalog so highlighting and completion stay in sync with a single catalog of functions, parameters, and enum values.
  • Adds intellisense to the calculation editor: function-name completion, parameter snippets with tab-through, enum-keyword completions for arguments, and context-aware popup triggers on ( and ;.
  • Routes script-step value completion through the calc provider so the script editor and calc editor share the same completion data and UX.

Test plan

  • dotnet test — 1244 tests passing, including new coverage for FmCalcCatalog, FmCalcCompletionProvider, FmCalcSignatureParser, CalcCompletionContextProvider, and grammar tokenization.
  • Manually exercise the calc editor: function-name completion, tab-through parameter snippets, enum keywords inside argument lists, and popup triggers on (/;.
  • Manually verify script-step value completion still pops correctly via the unified provider.

fuzzzerd added 10 commits April 26, 2026 18:22
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.
@github-actions
Copy link
Copy Markdown

Test Results

✔️ Tests 1244 / 1244 - passed in 11.3s
✔️ Coverage 79.45% - passed with 70% threshold
📏 14301 / 16511 lines covered 🌿 4853 / 7596 branches covered
🔍 click here for more details

✏️ updated for commit 7958280

@fuzzzerd fuzzzerd enabled auto-merge (rebase) April 26, 2026 23:31
@fuzzzerd fuzzzerd merged commit f201e52 into master Apr 26, 2026
6 checks passed
@fuzzzerd fuzzzerd deleted the fuzzz/fmcalc-feature branch April 26, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant