feat(ui/scripts): editor support for self/child/parent accessors
Phases 3+4 of the script-scope rollout. Wires the runtime accessors
landed in efba01d through to Monaco completion, diagnostics, and
hover.
New analyzer surface in ScriptAnalysisService:
String-literal completion contexts (added to TryStringLiteralCompletions):
Attributes["..."] -> SelfAttributes
Children["..."] -> composition names
Children["X"].Attributes["..."] -> child template's attributes
Children["X"].CallScript("...") -> child template's scripts
Parent.Attributes["..."] -> parent template's attributes
Parent.CallScript("...") -> parent template's scripts
Diagnostics:
SCADA006 Attribute "Typo" is not declared on {this template,
child composition 'X', the parent}. (warning)
SCADA007 Composition "Unknown" is not declared on this template.
(warning)
CallShared / CallScript snippet-expansion now routes through the
child / parent shape catalogs when invoked on Children["X"] /
Parent — picking a child script accepts `Sample", ${1:count})`.
Contract additions:
- AttributeShape (Name, Type) record
- CompositionContext (Name, Attributes, Scripts) record
- SelfAttributes / Children / Parent fields on DiagnoseRequest,
CompletionsRequest, HoverRequest, SignatureHelpRequest
ScriptHost (analyzer-side globals) gains stub AttributeBag /
ChildrenBag / CompositionBag types so Roslyn doesn't emit CS0103 on
Attributes / Children / Parent. The stubs are never invoked — only
their signatures are read by the analyzer's compilation pass.
MonacoEditor.razor exposes SelfAttributes / Children / Parent
parameters; GetContext returns them; monaco-init.js forwards all
three on completion / hover / signature-help / diagnostics requests.
TemplateEdit fetches each composition's resolved child template
shape via GetTemplateWithChildrenAsync, and queries GetAllTemplatesAsync
for any single parent that composes the open template. Multi-parent
or no-parent → Parent is suppressed.
11 new xUnit tests on the new completion / diagnostic paths. Total:
149 -> 159.
Browser-verified via curl:
- Children["..."] suggests composition names
- Attributes["..."] suggests attributes with type detail
- Attributes["Typo"] squiggles SCADA006
- Children["Unknown"] squiggles SCADA007
- No spurious CS0103 on the new accessors
Hover, signature help, and inlay hints for the new accessors keep
working because they reuse the same dispatch logic.
This commit is contained in:
@@ -382,6 +382,128 @@ public class ScriptAnalysisServiceTests
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA005");
|
||||
}
|
||||
|
||||
// ── Self / Children / Parent attribute completions ────────────────────
|
||||
|
||||
private static AttributeShape Attr(string name, string type = "String") => new(name, type);
|
||||
private static CompositionContext Comp(string name, AttributeShape[]? attrs = null, ScriptShape[]? scripts = null)
|
||||
=> new(name, attrs ?? Array.Empty<AttributeShape>(), scripts ?? Array.Empty<ScriptShape>());
|
||||
|
||||
[Fact]
|
||||
public async Task SelfAttribute_Literal_ReturnsSelfAttributeNames()
|
||||
{
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = Attributes[\"",
|
||||
Line: 1,
|
||||
Column: 21,
|
||||
SelfAttributes: new[] { Attr("Temperature"), Attr("Setpoint", "Float") });
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
Assert.Contains(resp.Items, i => i.Label == "Temperature");
|
||||
Assert.Contains(resp.Items, i => i.Label == "Setpoint" && i.Detail.Contains("Float"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChildAttribute_Literal_ReturnsChildAttributeNames()
|
||||
{
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = Children[\"TempSensor\"].Attributes[\"",
|
||||
Line: 1,
|
||||
Column: 44,
|
||||
Children: new[] { Comp("TempSensor", attrs: new[] { Attr("Temperature"), Attr("Humidity") }) });
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
Assert.Contains(resp.Items, i => i.Label == "Temperature");
|
||||
Assert.Contains(resp.Items, i => i.Label == "Humidity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParentAttribute_Literal_ReturnsParentAttributeNames()
|
||||
{
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = Parent.Attributes[\"",
|
||||
Line: 1,
|
||||
Column: 28,
|
||||
Parent: Comp("Motor", attrs: new[] { Attr("SpeedRPM") }));
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
Assert.Contains(resp.Items, i => i.Label == "SpeedRPM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChildrenLiteral_ReturnsCompositionNames()
|
||||
{
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = Children[\"",
|
||||
Line: 1,
|
||||
Column: 19,
|
||||
Children: new[] { Comp("TempSensor"), Comp("PressureSensor") });
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
Assert.Contains(resp.Items, i => i.Label == "TempSensor" && i.Detail == "composition");
|
||||
Assert.Contains(resp.Items, i => i.Label == "PressureSensor");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownSelfAttribute_RaisesSCADA006()
|
||||
{
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var x = Attributes[\"Typo\"];",
|
||||
SelfAttributes: new[] { Attr("Temperature") }));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA006" && m.Message.Contains("Typo"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownSelfAttribute_NoMarker()
|
||||
{
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var x = Attributes[\"Temperature\"];",
|
||||
SelfAttributes: new[] { Attr("Temperature") }));
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA006");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownChildAttribute_RaisesSCADA006()
|
||||
{
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var x = Children[\"TempSensor\"].Attributes[\"Typo\"];",
|
||||
Children: new[] { Comp("TempSensor", attrs: new[] { Attr("Temperature") }) }));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA006" && m.Message.Contains("TempSensor"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownComposition_RaisesSCADA007()
|
||||
{
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "var x = Children[\"Unknown\"].Attributes[\"X\"];",
|
||||
Children: new[] { Comp("TempSensor") }));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA007" && m.Message.Contains("Unknown"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChildrenCallScript_ReturnsChildScripts()
|
||||
{
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = Children[\"TempSensor\"].CallScript(\"",
|
||||
Line: 1,
|
||||
Column: 44,
|
||||
Children: new[]
|
||||
{
|
||||
Comp("TempSensor", scripts: new[] { Shape("Sample", Param("count", "Integer")) })
|
||||
});
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
var sample = Assert.Single(resp.Items, i => i.Label == "Sample");
|
||||
Assert.Contains("script on TempSensor", sample.Detail);
|
||||
Assert.Contains("${1:count}", sample.InsertText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParentCallScript_ReturnsParentScripts()
|
||||
{
|
||||
var req = new CompletionsRequest(
|
||||
CodeText: "var x = Parent.CallScript(\"",
|
||||
Line: 1,
|
||||
Column: 28,
|
||||
Parent: Comp("Motor", scripts: new[] { Shape("Trip") }));
|
||||
var resp = await _svc.CompleteAsync(req);
|
||||
Assert.Contains(resp.Items, i => i.Label == "Trip" && i.Detail.Contains("parent script"));
|
||||
}
|
||||
|
||||
// ── Hover on Parameters["name"] ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user