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:
Joseph Doherty
2026-05-12 05:53:13 -04:00
parent efba01d10a
commit 0b24b4537d
7 changed files with 506 additions and 18 deletions

View File

@@ -38,7 +38,10 @@
// Look up the SCADA context for a model by walking the editors map. Blazor
// JS interop serializes records as PascalCase; we normalize to camelCase.
async function lookupContext(model) {
const empty = { declaredParameters: [], siblingScripts: [], declaredParameterShapes: [] };
const empty = {
declaredParameters: [], siblingScripts: [], declaredParameterShapes: [],
selfAttributes: [], children: [], parent: null
};
for (const key in editors) {
if (editors[key].editor.getModel() === model) {
try {
@@ -47,7 +50,10 @@
return {
declaredParameters: got.DeclaredParameters || got.declaredParameters || [],
siblingScripts: got.SiblingScripts || got.siblingScripts || [],
declaredParameterShapes: got.DeclaredParameterShapes || got.declaredParameterShapes || []
declaredParameterShapes: got.DeclaredParameterShapes || got.declaredParameterShapes || [],
selfAttributes: got.SelfAttributes || got.selfAttributes || [],
children: got.Children || got.children || [],
parent: got.Parent || got.parent || null
};
}
} catch (e) { /* fall through */ }
@@ -73,7 +79,10 @@
line: position.lineNumber,
column: position.column,
declaredParameters: ctx.declaredParameters,
siblingScripts: ctx.siblingScripts
siblingScripts: ctx.siblingScripts,
selfAttributes: ctx.selfAttributes,
children: ctx.children,
parent: ctx.parent
})
});
if (!resp.ok) return { suggestions: [] };
@@ -141,7 +150,10 @@
line: position.lineNumber,
column: position.column,
siblingScripts: ctx.siblingScripts,
declaredParameters: ctx.declaredParameterShapes
declaredParameters: ctx.declaredParameterShapes,
selfAttributes: ctx.selfAttributes,
children: ctx.children,
parent: ctx.parent
})
});
if (!resp.ok) return null;
@@ -186,6 +198,8 @@
code: model.getValue(),
siblingScripts: ctx.siblingScripts
})
// Note: inlay hints don't yet read children/parent shapes
// because they only label CallShared/CallScript args today.
});
if (!resp.ok) return { hints: [], dispose: function () {} };
const data = await resp.json();
@@ -219,7 +233,9 @@
codeText: model.getValue(),
line: position.lineNumber,
column: position.column,
siblingScripts: ctx.siblingScripts
siblingScripts: ctx.siblingScripts,
children: ctx.children,
parent: ctx.parent
})
});
if (!resp.ok) return null;