Skip to content

Commit 7958280

Browse files
committed
feat: defer script-step value completion to calc provider
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.
1 parent 36f571b commit 7958280

3 files changed

Lines changed: 137 additions & 13 deletions

File tree

src/SharpFM/Scripting/Editor/FmScriptCompletionProvider.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ public enum CompletionContext
1111
StepName,
1212
ParamLabel,
1313
ParamValue,
14+
/// <summary>
15+
/// Caret is inside <c>[ ... ]</c> on a freeform value position — the
16+
/// calculation provider is supplying functions, control forms,
17+
/// constants, etc. Anchored at the identifier prefix.
18+
/// </summary>
19+
CalcExpression,
20+
/// <summary>
21+
/// Same as <see cref="CalcExpression"/> but the calc provider matched
22+
/// a <see cref="FmCalcCompletionProvider"/> <c>FunctionParam</c>
23+
/// context — i.e. enum keywords for the current arg
24+
/// (<c>Get(...)</c>, <c>JSONSetElement</c>'s <c>type</c>, etc.).
25+
/// Distinguished so the controller can pop on <c>(</c> and <c>;</c>
26+
/// triggers without spamming bare identifier lists.
27+
/// </summary>
28+
CalcParamValue,
1429
None,
1530
}
1631

@@ -49,6 +64,17 @@ public static (CompletionContext Context, IList<ICompletionData> Items) GetCompl
4964
return (CompletionContext.StepName, items);
5065
}
5166

67+
// Inside an unmatched `(...)` the `;` separators belong to that
68+
// call, not to the step's bracket-param list. Hand off to the calc
69+
// provider before we try to slice currentSegment on flat `;` —
70+
// otherwise the script logic mis-attributes the call's args to
71+
// step params and we never reach the labeled-value fallback.
72+
if (FmCalcCompletionProvider.DetectEnclosingCall(lineText, caretColumn) != null)
73+
{
74+
var deferredEarly = DeferToCalc(lineText, caretColumn);
75+
if (deferredEarly.Items.Count > 0) return deferredEarly;
76+
}
77+
5278
var afterStepName = forLookup.Substring(stepName.Length);
5379
var lastSemicolon = afterStepName.LastIndexOf(';');
5480
var currentSegment = lastSemicolon >= 0
@@ -67,6 +93,14 @@ public static (CompletionContext Context, IList<ICompletionData> Items) GetCompl
6793
var items = GetParamValueCompletions(matchingParam);
6894
if (items.Count > 0)
6995
return (CompletionContext.ParamValue, items);
96+
97+
// Param exists but has no enum values — the user is typing
98+
// a freeform calculation expression (e.g. `Value: Length(`,
99+
// `Value: $myVar`, `Value: Get(AccountName)`). Hand off to
100+
// the calc provider so functions, control forms, constants
101+
// and per-arg keywords all light up here too.
102+
var deferred = DeferToCalc(lineText, caretColumn);
103+
if (deferred.Items.Count > 0) return deferred;
70104
}
71105
}
72106

@@ -259,4 +293,24 @@ private static IList<ICompletionData> GetPositionalValueCompletions(
259293

260294
return items;
261295
}
296+
297+
/// <summary>
298+
/// Hand off completion to the calc provider for the same line/caret
299+
/// position. Returns a script-side context so the controller can
300+
/// distinguish identifier-prefix completions (anchored at the prefix)
301+
/// from function-param keyword completions (whose triggers also
302+
/// include <c>(</c> and <c>;</c>).
303+
/// </summary>
304+
private static (CompletionContext Context, IList<ICompletionData> Items) DeferToCalc(
305+
string lineText, int caretColumn)
306+
{
307+
var (calcCtx, calcItems) = FmCalcCompletionProvider.GetCompletions(lineText, caretColumn);
308+
if (calcCtx == CalcCompletionContext.None || calcItems.Count == 0)
309+
return (CompletionContext.None, Array.Empty<ICompletionData>());
310+
311+
var scriptCtx = calcCtx == CalcCompletionContext.FunctionParam
312+
? CompletionContext.CalcParamValue
313+
: CompletionContext.CalcExpression;
314+
return (scriptCtx, calcItems);
315+
}
262316
}

src/SharpFM/Scripting/Editor/ScriptEditorController.cs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,19 @@ private void OnPointerMoved(object? sender, PointerEventArgs e)
162162
private void OnTextEntered(object? sender, TextInputEventArgs e)
163163
{
164164
if (_completionWindow != null) return;
165-
166-
// Only auto-trigger on identifier-starting characters. Without this
167-
// gate, every space, semicolon, bracket, etc. spawns a fresh
168-
// CompletionWindow build — measurably laggy for catalogs of any
169-
// size. Mirrors the calculation editor's gating logic.
170-
if (string.IsNullOrEmpty(e.Text) || !IsTriggerChar(e.Text[0])) return;
171-
172-
TryShowCompletions();
165+
if (string.IsNullOrEmpty(e.Text)) return;
166+
167+
var ch = e.Text[0];
168+
var isIdentifierTrigger = IsTriggerChar(ch);
169+
// Argument-boundary characters: ( and ; should pop the menu *only*
170+
// when we end up in a CalcParamValue context (e.g. Get( →
171+
// selectors, JSONSetElement(j;k;v; → JSON types). Without this,
172+
// every ( and ; would spam the window with the full identifier
173+
// catalog. Mirrors the calculation editor's gating logic.
174+
var isArgBoundary = ch == '(' || ch == ';';
175+
if (!isIdentifierTrigger && !isArgBoundary) return;
176+
177+
TryShowCompletions(isArgBoundary);
173178
}
174179

175180
private static bool IsTriggerChar(char c) =>
@@ -178,24 +183,35 @@ private static bool IsTriggerChar(char c) =>
178183
private static bool IsIdentifierChar(char c) =>
179184
char.IsLetterOrDigit(c) || c == '_';
180185

181-
private void TryShowCompletions()
186+
private void TryShowCompletions(bool isArgBoundaryTrigger = false)
182187
{
183188
var caret = _editor.TextArea.Caret;
184189
var line = _editor.Document.GetLineByNumber(caret.Line);
185190
var lineText = _editor.Document.GetText(line.Offset, line.Length);
186191
var col = caret.Column - 1;
187192

188-
// Require AutoCompleteMinPrefix consecutive identifier characters
189-
// immediately before the caret before showing. Skips the
190-
// 60-item-empty-prefix popup on the very first keystroke.
193+
// Walk back over the identifier prefix immediately before the
194+
// caret. We need this in two places: the min-prefix gate below
195+
// and the StartOffset anchor for calc-context completions.
191196
var prefixStart = col;
192197
while (prefixStart > 0 && IsIdentifierChar(lineText[prefixStart - 1]))
193198
prefixStart--;
194-
if (col - prefixStart < AutoCompleteMinPrefix) return;
199+
200+
// Require AutoCompleteMinPrefix consecutive identifier characters
201+
// before showing — skips the empty-prefix popup on the first
202+
// keystroke. Skipped for ( / ; triggers because those are
203+
// argument-boundary characters where the prefix is intentionally
204+
// empty (e.g. `Get(` should pop the selector list immediately).
205+
if (!isArgBoundaryTrigger && col - prefixStart < AutoCompleteMinPrefix) return;
195206

196207
var (context, items) = FmScriptCompletionProvider.GetCompletions(lineText, col);
197208
if (context == CompletionContext.None || items.Count == 0) return;
198209

210+
// Only commit to opening the window on ( or ; triggers when the
211+
// result is a function-param keyword list — anything else and the
212+
// user just typed a separator and doesn't want a popup.
213+
if (isArgBoundaryTrigger && context != CompletionContext.CalcParamValue) return;
214+
199215
_completionWindow = new CompletionWindow(_editor.TextArea);
200216

201217
if (context == CompletionContext.StepName)
@@ -207,6 +223,14 @@ private void TryShowCompletions()
207223
{
208224
_completionWindow.StartOffset = _editor.CaretOffset;
209225
}
226+
else if (context == CompletionContext.CalcExpression
227+
|| context == CompletionContext.CalcParamValue)
228+
{
229+
// Anchor at the start of the identifier prefix so accepting
230+
// an item replaces the partial prefix rather than appending
231+
// after it.
232+
_completionWindow.StartOffset = line.Offset + prefixStart;
233+
}
210234

211235
foreach (var item in items)
212236
_completionWindow.CompletionList.CompletionData.Add(item);

tests/SharpFM.Tests/Scripting/CompletionProviderTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,50 @@ public void StepNameCompletion_ZeroParamStep_InsertsBareName()
147147

148148
Assert.Equal("Beep", snippet);
149149
}
150+
151+
[Fact]
152+
public void SetVariable_AfterValueColon_DefersToCalcCompletion()
153+
{
154+
// Set Variable's Value param is freeform calc — script provider
155+
// hands off to FmCalcCompletionProvider so the user gets functions,
156+
// control forms, constants etc. mid-bracket.
157+
var line = "Set Variable [ $x ; Value: Le";
158+
var (context, items) = FmScriptCompletionProvider.GetCompletions(line, line.Length);
159+
Assert.Equal(CompletionContext.CalcExpression, context);
160+
Assert.Contains(items, i => i.Text == "Length");
161+
Assert.Contains(items, i => i.Text == "Let");
162+
// Sanity: param labels (Repetition) should not bleed into calc results.
163+
Assert.DoesNotContain(items, i => i.Text == "Repetition: ");
164+
}
165+
166+
[Fact]
167+
public void SetVariable_GetCallInsideValue_DefersToFunctionParam()
168+
{
169+
// Inside Get(... and the script provider should recognise the calc
170+
// sub-context as FunctionParam, surfacing Get's selector keywords.
171+
var line = "Set Variable [ $x ; Value: Get(";
172+
var (context, items) = FmScriptCompletionProvider.GetCompletions(line, line.Length);
173+
Assert.Equal(CompletionContext.CalcParamValue, context);
174+
Assert.Contains(items, i => i.Text == "AccountName");
175+
Assert.Contains(items, i => i.Text == "CurrentDate");
176+
}
177+
178+
[Fact]
179+
public void SetVariable_JsonSetElementType_DefersToFunctionParam()
180+
{
181+
var line = "Set Variable [ $x ; Value: JSONSetElement(j;\"k\";v;";
182+
var (context, items) = FmScriptCompletionProvider.GetCompletions(line, line.Length);
183+
Assert.Equal(CompletionContext.CalcParamValue, context);
184+
Assert.Contains(items, i => i.Text == "JSONString");
185+
}
186+
187+
[Fact]
188+
public void SetVariable_BeforeAnyColon_StillOffersLabelCompletions()
189+
{
190+
// Calc deferral should fire only after a labeled colon. Pre-colon
191+
// mid-bracket positions keep the existing label/positional logic.
192+
var line = "Set Variable [ ";
193+
var (context, _) = FmScriptCompletionProvider.GetCompletions(line, line.Length);
194+
Assert.Equal(CompletionContext.ParamLabel, context);
195+
}
150196
}

0 commit comments

Comments
 (0)