feat(ui/scripts): shape-aware Monaco features for script calls
Now that the form holds parameter + return shapes for declared
parameters, sibling scripts (template Scripts tab), and shared
scripts (via SharedScriptCatalog), the editor leverages them four
ways:
1. Snippet expansion on accept.
Picking a CallShared or CallScript completion inserts the full
call template with tabstops, e.g. `Greet", ${1:name})`. The JS
provider extends the completion range over Monaco's auto-closed
`")` so the snippet replaces the closing pair cleanly. Items
carry insertTextRules=4 (InsertAsSnippet) and a command to
immediately trigger parameter hints after acceptance.
2. Hover info.
Hovering the script name token inside CallShared("X") or
CallScript("Y") shows a markdown tooltip with the call signature
and return type. New endpoint POST /api/script-analysis/hover.
3. Signature help.
Inside CallShared(...) / CallScript(...) Monaco shows the
parameter strip with the active parameter highlighted. The
service walks up from the cursor to the nearest enclosing
InvocationExpression and resolves which argument index the
cursor is on. New endpoint POST /api/script-analysis/signature-help.
4. Argument-count diagnostic (SCADA004) and unknown-Parameters-key
diagnostic (SCADA003). The Diagnose pipeline now consults the
declared parameters and sibling/shared shapes to flag:
- Parameters["typo"] when "typo" isn't on the form (warn)
- CallScript("Calc", 1) when Calc declares 2 required args (err)
- CallShared("Greet", 1, 2, 3) when Greet declares 1 arg (err)
Optional parameters relax the required-count bound.
Contract changes:
- ScriptShape / ParameterShape records
- ISharedScriptCatalog.GetShapesAsync (replaces GetNamesAsync)
- new HoverRequest/Response, SignatureHelpRequest/Response
- CompletionsRequest.SiblingScripts: string[] -> ScriptShape[]
- DiagnoseRequest gains DeclaredParameters + SiblingScripts
- CompletionItem gains InsertTextRules (Monaco snippet rule)
Form wiring:
- TemplateEdit passes ScriptShapeParser.Parse(...) per sibling
- MonacoEditor surfaces SiblingScripts: IReadOnlyList<ScriptShape>
- GetContext returns shapes to JS on each completion/hover/sig
request
10 new ScriptAnalysisServiceTests covering all four features plus
optional-parameter edge cases. Existing tests updated for the
contract changes. Total: 113 -> 139.
Browser-verified via direct curl + Monaco marker readback:
- SCADA003 squiggle on Parameters["typo"]
- Snippet item Greet", ${1:name}) with insertTextRules=4
- Hover markdown shape signature
- Signature help parameter strip
This commit is contained in:
@@ -10,12 +10,20 @@ public class ScriptAnalysisServiceTests
|
||||
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
|
||||
private readonly ScriptAnalysisService _svc;
|
||||
|
||||
private static ScriptShape Shape(string name, params ParameterShape[] ps) =>
|
||||
new(name, ps, null);
|
||||
|
||||
private static ParameterShape Param(string name, string type = "String", bool required = true) =>
|
||||
new(name, type, required);
|
||||
|
||||
public ScriptAnalysisServiceTests()
|
||||
{
|
||||
_catalog.GetNamesAsync().Returns(Array.Empty<string>());
|
||||
_catalog.GetShapesAsync().Returns(Array.Empty<ScriptShape>());
|
||||
_svc = new ScriptAnalysisService(_catalog, _cache);
|
||||
}
|
||||
|
||||
// ── Diagnose ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void EmptyCode_NoMarkers()
|
||||
{
|
||||
@@ -65,7 +73,6 @@ public class ScriptAnalysisServiceTests
|
||||
[Fact]
|
||||
public void UserIdentifierNamedFile_DoesNotFalsePositive()
|
||||
{
|
||||
// No System.IO import; user defines their own 'File' local.
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
"var File = \"hello\"; return File.Length;"));
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA002");
|
||||
@@ -85,8 +92,6 @@ public class ScriptAnalysisServiceTests
|
||||
var req = new DiagnoseRequest("using System.IO;");
|
||||
var first = _svc.Diagnose(req);
|
||||
var second = _svc.Diagnose(req);
|
||||
|
||||
// Same instance reference indicates the cache returned the prior result.
|
||||
Assert.Same(first, second);
|
||||
}
|
||||
|
||||
@@ -98,6 +103,75 @@ public class ScriptAnalysisServiceTests
|
||||
Assert.NotSame(a, b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownParameterKey_RaisesSCADA003()
|
||||
{
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var x = Parameters[\"typo\"];",
|
||||
DeclaredParameters: new[] { "name", "temperature" }));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA003" && m.Message.Contains("'typo'"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclaredParameterKey_NoMarker()
|
||||
{
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var x = Parameters[\"name\"];",
|
||||
DeclaredParameters: new[] { "name", "temperature" }));
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA003");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArgumentCountTooFew_RaisesSCADA004()
|
||||
{
|
||||
var siblings = new[] { Shape("Calc", Param("x"), Param("y")) };
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var r = CallScript(\"Calc\", 1);",
|
||||
SiblingScripts: siblings));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA004" && m.Message.Contains("expects 2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArgumentCountTooMany_RaisesSCADA004()
|
||||
{
|
||||
var siblings = new[] { Shape("Ping") };
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var r = CallScript(\"Ping\", 1, 2);",
|
||||
SiblingScripts: siblings));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA004" && m.Message.Contains("got 2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArgumentCountCorrect_NoMarker()
|
||||
{
|
||||
var siblings = new[] { Shape("Calc", Param("x"), Param("y")) };
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var r = CallScript(\"Calc\", 1, 2);",
|
||||
SiblingScripts: siblings));
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA004");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionalParameter_AcceptsBothOmittedAndPresent()
|
||||
{
|
||||
var siblings = new[]
|
||||
{
|
||||
Shape("Calc", Param("x"), Param("y", required: false))
|
||||
};
|
||||
// Required only (1) — OK.
|
||||
var with1 = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var r = CallScript(\"Calc\", 1);",
|
||||
SiblingScripts: siblings));
|
||||
Assert.DoesNotContain(with1.Markers, m => m.Code == "SCADA004");
|
||||
// Both passed (2) — OK.
|
||||
var with2 = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var r = CallScript(\"Calc\", 1, 2);",
|
||||
SiblingScripts: siblings));
|
||||
Assert.DoesNotContain(with2.Markers, m => m.Code == "SCADA004");
|
||||
}
|
||||
|
||||
// ── Completions ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ParametersStringLiteral_ReturnsDeclaredParameterNames()
|
||||
{
|
||||
@@ -114,24 +188,31 @@ public class ScriptAnalysisServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallScriptStringLiteral_ReturnsSiblingNames()
|
||||
public async Task CallScriptStringLiteral_ReturnsSiblingNamesWithSnippet()
|
||||
{
|
||||
var siblings = new[] { Shape("SiblingA", Param("x")) };
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = CallScript(\"",
|
||||
Line: 1,
|
||||
Column: 21,
|
||||
SiblingScripts: new[] { "SiblingA", "SiblingB" });
|
||||
SiblingScripts: siblings);
|
||||
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
|
||||
Assert.Contains(resp.Items, i => i.Label == "SiblingA" && i.Detail == "sibling script");
|
||||
Assert.Contains(resp.Items, i => i.Label == "SiblingB");
|
||||
var item = Assert.Single(resp.Items, i => i.Label == "SiblingA");
|
||||
Assert.Equal(4, item.InsertTextRules);
|
||||
Assert.Contains("${1:x}", item.InsertText);
|
||||
Assert.Contains("sibling script", item.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallSharedStringLiteral_ResolvesViaCatalog()
|
||||
public async Task CallSharedStringLiteral_ResolvesViaCatalogWithShapes()
|
||||
{
|
||||
_catalog.GetNamesAsync().Returns(new[] { "GetWeather", "Greet" });
|
||||
_catalog.GetShapesAsync().Returns(new[]
|
||||
{
|
||||
Shape("GetWeather"),
|
||||
Shape("Greet", Param("name"))
|
||||
});
|
||||
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = CallShared(\"",
|
||||
@@ -140,17 +221,72 @@ public class ScriptAnalysisServiceTests
|
||||
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
|
||||
Assert.Contains(resp.Items, i => i.Label == "GetWeather" && i.Detail == "shared script");
|
||||
Assert.Contains(resp.Items, i => i.Label == "Greet");
|
||||
Assert.Contains(resp.Items, i => i.Label == "GetWeather");
|
||||
var greet = Assert.Single(resp.Items, i => i.Label == "Greet");
|
||||
Assert.Contains("${1:name}", greet.InsertText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GeneralCompletion_ReturnsInScopeSymbols()
|
||||
{
|
||||
// At file scope of a script, ScriptHost members + the System namespace are visible.
|
||||
var req = new CompletionsRequest("var x = ", 1, 9);
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
Assert.Contains(resp.Items, i => i.Label == "Parameters");
|
||||
Assert.Contains(resp.Items, i => i.Label == "CallShared");
|
||||
}
|
||||
|
||||
// ── Hover ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Hover_OnSiblingName_ReturnsSignature()
|
||||
{
|
||||
var siblings = new[] { Shape("Calc", Param("x", "Integer"), Param("y", "Float")) };
|
||||
var resp = _svc.Hover(new HoverRequest(
|
||||
CodeText: "var r = CallScript(\"Calc\", 1, 2);",
|
||||
Line: 1,
|
||||
Column: 23,
|
||||
SiblingScripts: siblings));
|
||||
Assert.NotNull(resp.Markdown);
|
||||
Assert.Contains("Calc", resp.Markdown);
|
||||
Assert.Contains("x: Integer", resp.Markdown);
|
||||
Assert.Contains("y: Float", resp.Markdown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hover_OnUnrelatedToken_ReturnsNull()
|
||||
{
|
||||
var resp = _svc.Hover(new HoverRequest(
|
||||
CodeText: "var r = 1 + 2;",
|
||||
Line: 1,
|
||||
Column: 5));
|
||||
Assert.Null(resp.Markdown);
|
||||
}
|
||||
|
||||
// ── Signature help ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SignatureHelp_InsideCallScript_ReturnsParameterStrip()
|
||||
{
|
||||
var siblings = new[] { Shape("Calc", Param("x", "Integer"), Param("y", "Float")) };
|
||||
var resp = _svc.SignatureHelp(new SignatureHelpRequest(
|
||||
CodeText: "var r = CallScript(\"Calc\", 1, ",
|
||||
Line: 1,
|
||||
Column: 31,
|
||||
SiblingScripts: siblings));
|
||||
Assert.NotNull(resp.Label);
|
||||
Assert.Equal(2, resp.Parameters!.Count);
|
||||
Assert.Equal("x: Integer", resp.Parameters[0].Label);
|
||||
Assert.Equal("y: Float", resp.Parameters[1].Label);
|
||||
Assert.Equal(1, resp.ActiveParameter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignatureHelp_OutsideCall_ReturnsNull()
|
||||
{
|
||||
var resp = _svc.SignatureHelp(new SignatureHelpRequest(
|
||||
CodeText: "var r = 1 + 2;",
|
||||
Line: 1,
|
||||
Column: 5));
|
||||
Assert.Null(resp.Label);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user