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

@@ -92,6 +92,9 @@
private MonacoEditor? _scriptEditor;
private IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.DiagnosticMarker> _scriptMarkers
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.DiagnosticMarker>();
private IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorChildren
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? _editorParent;
private bool _showCompForm;
private int _compComposedTemplateId;
@@ -126,6 +129,13 @@
_scripts = (await TemplateEngineRepository.GetScriptsByTemplateIdAsync(Id)).ToList();
_compositions = (await TemplateEngineRepository.GetCompositionsByTemplateIdAsync(Id)).ToList();
// Editor metadata: child compositions + parent (if exactly one).
// Powers Attributes["X"] / Children["Y"].Attributes["Z"] /
// Parent.Attributes["W"] completion + SCADA006 / SCADA007 diagnostics
// in the Monaco editor.
_editorChildren = await BuildChildContextsAsync(_compositions);
_editorParent = await TryGetParentContextAsync(Id);
_validationResult = null;
}
catch (Exception ex)
@@ -662,6 +672,9 @@
DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)"
DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_scriptParameters)"
SiblingScripts="@(_scripts.Select(s => ScadaLink.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition)).ToArray())"
SelfAttributes="@(_attributes.Select(a => new ScadaLink.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType))).ToArray())"
Children="@_editorChildren"
Parent="@_editorParent"
MarkersChanged="@(m => { _scriptMarkers = m; StateHasChanged(); })" />
<ProblemsPanel Markers="@_scriptMarkers" OnNavigate="@(m => _scriptEditor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
</div>
@@ -970,4 +983,54 @@
}
else { _toast.ShowError(result.Error); }
}
// ---- Editor metadata builders ----
private async Task<IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>> BuildChildContextsAsync(
IReadOnlyList<ScadaLink.Commons.Entities.Templates.TemplateComposition> comps)
{
var result = new List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
foreach (var comp in comps)
{
var composed = await TemplateEngineRepository.GetTemplateWithChildrenAsync(comp.ComposedTemplateId);
if (composed == null) continue;
result.Add(BuildCompositionContext(comp.InstanceName, composed));
}
return result;
}
private async Task<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext?> TryGetParentContextAsync(int templateId)
{
var all = await TemplateEngineRepository.GetAllTemplatesAsync();
var parents = all.Where(t => t.Compositions.Any(c => c.ComposedTemplateId == templateId)).ToList();
if (parents.Count != 1) return null; // ambiguous or root — suppress Parent assistance
var p = await TemplateEngineRepository.GetTemplateWithChildrenAsync(parents[0].Id);
return p == null ? null : BuildCompositionContext(p.Name, p);
}
private static ScadaLink.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext(
string label,
ScadaLink.Commons.Entities.Templates.Template t)
{
var attrs = t.Attributes
.Select(a => new ScadaLink.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType)))
.ToList();
var scripts = t.Scripts
.Select(s => ScadaLink.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(
s.Name, s.ParameterDefinitions, s.ReturnDefinition))
.ToList();
return new ScadaLink.CentralUI.ScriptAnalysis.CompositionContext(label, attrs, scripts);
}
private static string MapDataType(ScadaLink.Commons.Types.Enums.DataType dt) => dt switch
{
ScadaLink.Commons.Types.Enums.DataType.Boolean => "Boolean",
ScadaLink.Commons.Types.Enums.DataType.Int32 => "Integer",
ScadaLink.Commons.Types.Enums.DataType.Float => "Float",
ScadaLink.Commons.Types.Enums.DataType.Double => "Float",
ScadaLink.Commons.Types.Enums.DataType.String => "String",
ScadaLink.Commons.Types.Enums.DataType.DateTime => "String",
ScadaLink.Commons.Types.Enums.DataType.Binary => "Object",
_ => "Object"
};
}

View File

@@ -48,6 +48,26 @@
/// </summary>
[Parameter] public IReadOnlyList<ScriptAnalysis.ScriptShape>? SiblingScripts { get; set; }
/// <summary>
/// Attributes declared on the current template. Surfaced inside
/// <c>Attributes["..."]</c> for completion and SCADA006 diagnostics.
/// </summary>
[Parameter] public IReadOnlyList<ScriptAnalysis.AttributeShape>? SelfAttributes { get; set; }
/// <summary>
/// Child compositions on the current template, each with its template's
/// attributes and scripts. Surfaced for <c>Children["X"].Attributes</c>,
/// <c>Children["X"].CallScript</c>, and SCADA007 diagnostics.
/// </summary>
[Parameter] public IReadOnlyList<ScriptAnalysis.CompositionContext>? Children { get; set; }
/// <summary>
/// Parent template when the current template is composed inside exactly
/// one other template. <c>null</c> at the root or when multiple parents
/// exist. Surfaced for <c>Parent.Attributes</c> / <c>Parent.CallScript</c>.
/// </summary>
[Parameter] public ScriptAnalysis.CompositionContext? Parent { get; set; }
/// <summary>
/// Fires whenever Monaco's marker set updates (after the 500 ms diagnostic
/// debounce). Hosts can render a <see cref="ProblemsPanel"/> with the same
@@ -125,7 +145,10 @@
SiblingScripts?.ToArray() ?? Array.Empty<ScriptAnalysis.ScriptShape>(),
DeclaredParameterShapes?.ToArray()
?? DeclaredParameters?.Select(n => new ScriptAnalysis.ParameterShape(n, "Object", true)).ToArray()
?? Array.Empty<ScriptAnalysis.ParameterShape>());
?? Array.Empty<ScriptAnalysis.ParameterShape>(),
SelfAttributes?.ToArray() ?? Array.Empty<ScriptAnalysis.AttributeShape>(),
Children?.ToArray() ?? Array.Empty<ScriptAnalysis.CompositionContext>(),
Parent);
private async Task FormatAsync()
{
@@ -163,5 +186,8 @@
public record ScadaContext(
string[] DeclaredParameters,
ScriptAnalysis.ScriptShape[] SiblingScripts,
ScriptAnalysis.ParameterShape[] DeclaredParameterShapes);
ScriptAnalysis.ParameterShape[] DeclaredParameterShapes,
ScriptAnalysis.AttributeShape[] SelfAttributes,
ScriptAnalysis.CompositionContext[] Children,
ScriptAnalysis.CompositionContext? Parent);
}

View File

@@ -3,7 +3,10 @@ namespace ScadaLink.CentralUI.ScriptAnalysis;
public record DiagnoseRequest(
string Code,
IReadOnlyList<string>? DeclaredParameters = null,
IReadOnlyList<ScriptShape>? SiblingScripts = null);
IReadOnlyList<ScriptShape>? SiblingScripts = null,
IReadOnlyList<AttributeShape>? SelfAttributes = null,
IReadOnlyList<CompositionContext>? Children = null,
CompositionContext? Parent = null);
public record DiagnoseResponse(IReadOnlyList<DiagnosticMarker> Markers);
@@ -25,7 +28,10 @@ public record CompletionsRequest(
int Line,
int Column,
IReadOnlyList<string>? DeclaredParameters = null,
IReadOnlyList<ScriptShape>? SiblingScripts = null);
IReadOnlyList<ScriptShape>? SiblingScripts = null,
IReadOnlyList<AttributeShape>? SelfAttributes = null,
IReadOnlyList<CompositionContext>? Children = null,
CompositionContext? Parent = null);
public record CompletionsResponse(IReadOnlyList<CompletionItem> Items);
@@ -42,7 +48,10 @@ public record HoverRequest(
int Line,
int Column,
IReadOnlyList<ScriptShape>? SiblingScripts = null,
IReadOnlyList<ParameterShape>? DeclaredParameters = null);
IReadOnlyList<ParameterShape>? DeclaredParameters = null,
IReadOnlyList<AttributeShape>? SelfAttributes = null,
IReadOnlyList<CompositionContext>? Children = null,
CompositionContext? Parent = null);
public record HoverResponse(string? Markdown);
@@ -50,7 +59,9 @@ public record SignatureHelpRequest(
string CodeText,
int Line,
int Column,
IReadOnlyList<ScriptShape>? SiblingScripts = null);
IReadOnlyList<ScriptShape>? SiblingScripts = null,
IReadOnlyList<CompositionContext>? Children = null,
CompositionContext? Parent = null);
public record SignatureHelpResponse(
string? Label,
@@ -72,6 +83,24 @@ public record ScriptShape(
public record ParameterShape(string Name, string Type, bool Required);
/// <summary>
/// Attribute declared on a template: name + canonical SCADA type (Boolean,
/// Integer, Float, String, Object, List).
/// </summary>
public record AttributeShape(string Name, string Type);
/// <summary>
/// One end of a composition relationship — either a child (referenced by
/// composition instance name) or the parent (referenced by template name).
/// The shape carries the attributes and scripts at that scope so the editor
/// can complete <c>Children["X"].Attributes["Y"]</c> and
/// <c>Children["X"].CallScript("Z")</c> with the right metadata.
/// </summary>
public record CompositionContext(
string Name,
IReadOnlyList<AttributeShape> Attributes,
IReadOnlyList<ScriptShape> Scripts);
public record FormatRequest(string Code);
public record FormatResponse(string Code);

View File

@@ -107,6 +107,8 @@ public class ScriptAnalysisService
markers.AddRange(FindUnknownParameterKeys(tree, request.DeclaredParameters));
markers.AddRange(FindArgumentCountMismatches(tree, request.SiblingScripts));
markers.AddRange(FindArgumentTypeMismatches(tree, request.SiblingScripts));
markers.AddRange(FindUnknownAttributeKeys(tree, request));
markers.AddRange(FindUnknownChildren(tree, request.Children));
}
return Cache(cacheKey, new DiagnoseResponse(markers));
@@ -205,11 +207,48 @@ public class ScriptAnalysisService
.ToList();
}
// CallShared("...") / CallScript("...")
// Attributes["..."] / Children["X"].Attributes["..."] / Parent.Attributes["..."]
if (owner is ElementAccessExpressionSyntax attrElem)
{
var ctx = ClassifyAttributeContext(
attrElem,
request.Children ?? Array.Empty<CompositionContext>(),
request.Parent);
if (ctx.Kind != AttributeContextKind.None)
{
IReadOnlyList<AttributeShape> source = ctx.Kind switch
{
AttributeContextKind.Self => request.SelfAttributes ?? Array.Empty<AttributeShape>(),
AttributeContextKind.Child => ctx.Composition!.Attributes,
AttributeContextKind.Parent => request.Parent?.Attributes ?? Array.Empty<AttributeShape>(),
_ => Array.Empty<AttributeShape>()
};
var label = ctx.Kind switch
{
AttributeContextKind.Self => "attribute",
AttributeContextKind.Child => $"attribute on {ctx.Composition!.Name}",
AttributeContextKind.Parent => "parent attribute",
_ => "attribute"
};
return source.Select(a =>
new CompletionItem(a.Name, a.Name, $"{label}: {a.Type}", "Field")).ToList();
}
// Children["..."] → suggest composition names
if (attrElem.Expression is IdentifierNameSyntax cid && cid.Identifier.ValueText == "Children")
{
return (request.Children ?? Array.Empty<CompositionContext>())
.Select(c => new CompletionItem(c.Name, c.Name, "composition", "Class"))
.ToList();
}
}
// CallShared("...") / CallScript("...") / Children["X"].CallScript("...") / Parent.CallScript("...")
if (owner is InvocationExpressionSyntax inv)
{
var calleeName = (inv.Expression as IdentifierNameSyntax)?.Identifier.ValueText
?? (inv.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.ValueText;
var calleeIdName = (inv.Expression as IdentifierNameSyntax)?.Identifier.ValueText;
var calleeMa = inv.Expression as MemberAccessExpressionSyntax;
var calleeName = calleeIdName ?? calleeMa?.Name.Identifier.ValueText;
if (calleeName == "CallShared")
{
@@ -219,6 +258,36 @@ public class ScriptAnalysisService
if (calleeName == "CallScript")
{
// Children["X"].CallScript("..." or Parent.CallScript("...
if (calleeMa != null)
{
// Children["X"].CallScript
if (calleeMa.Expression is ElementAccessExpressionSyntax childElem
&& childElem.Expression is IdentifierNameSyntax cid
&& cid.Identifier.ValueText == "Children"
&& childElem.ArgumentList.Arguments.Count == 1
&& childElem.ArgumentList.Arguments[0].Expression is LiteralExpressionSyntax cLit
&& cLit.IsKind(SyntaxKind.StringLiteralExpression))
{
var compName = cLit.Token.ValueText;
var comp = (request.Children ?? Array.Empty<CompositionContext>())
.FirstOrDefault(c => c.Name == compName);
if (comp != null)
return comp.Scripts.Select(s => MakeCallCompletion(s, $"script on {compName}")).ToList();
return new List<CompletionItem>();
}
// Parent.CallScript
if (calleeMa.Expression is IdentifierNameSyntax pid
&& pid.Identifier.ValueText == "Parent"
&& request.Parent != null)
{
return request.Parent.Scripts
.Select(s => MakeCallCompletion(s, "parent script"))
.ToList();
}
}
// Plain CallScript("...") — siblings
return (request.SiblingScripts ?? Array.Empty<ScriptShape>())
.Select(s => MakeCallCompletion(s, "sibling script"))
.ToList();
@@ -559,6 +628,135 @@ public class ScriptAnalysisService
}
}
/// <summary>
/// SCADA006 — flag <c>Attributes["typo"]</c>,
/// <c>Children["X"].Attributes["typo"]</c>, and
/// <c>Parent.Attributes["typo"]</c> when the literal key isn't declared
/// at the relevant scope. Also SCADA007 — flag <c>Children["Unknown"]</c>
/// when the composition name isn't declared on the form.
/// </summary>
private IEnumerable<DiagnosticMarker> FindUnknownAttributeKeys(SyntaxTree tree, DiagnoseRequest request)
{
var root = tree.GetRoot();
var selfAttrs = request.SelfAttributes ?? Array.Empty<AttributeShape>();
var children = request.Children ?? Array.Empty<CompositionContext>();
var parent = request.Parent;
foreach (var elem in root.DescendantNodes().OfType<ElementAccessExpressionSyntax>())
{
if (elem.ArgumentList.Arguments.Count != 1) continue;
if (elem.ArgumentList.Arguments[0].Expression is not LiteralExpressionSyntax lit) continue;
if (!lit.IsKind(SyntaxKind.StringLiteralExpression)) continue;
var key = lit.Token.ValueText;
if (string.IsNullOrEmpty(key)) continue;
var ctx = ClassifyAttributeContext(elem, children, parent);
if (ctx.Kind == AttributeContextKind.None) continue;
IReadOnlyList<AttributeShape> known = ctx.Kind switch
{
AttributeContextKind.Self => selfAttrs,
AttributeContextKind.Child => ctx.Composition!.Attributes,
AttributeContextKind.Parent => parent?.Attributes ?? Array.Empty<AttributeShape>(),
_ => Array.Empty<AttributeShape>()
};
if (known.Count == 0) continue; // No metadata — don't false-alarm.
if (known.Any(a => a.Name == key)) continue;
var span = lit.GetLocation().GetLineSpan().Span;
var scopeLabel = ctx.Kind switch
{
AttributeContextKind.Self => "this template",
AttributeContextKind.Child => $"child composition '{ctx.Composition!.Name}'",
AttributeContextKind.Parent => "the parent",
_ => "unknown"
};
yield return new DiagnosticMarker(
Severity: 4,
StartLineNumber: span.Start.Line + 1,
StartColumn: span.Start.Character + 1,
EndLineNumber: span.End.Line + 1,
EndColumn: span.End.Character + 1,
Message: $"Attribute '{key}' is not declared on {scopeLabel}.",
Code: "SCADA006");
}
}
/// <summary>SCADA007 — <c>Children["UnknownComposition"]</c>.</summary>
private static IEnumerable<DiagnosticMarker> FindUnknownChildren(SyntaxTree tree, IReadOnlyList<CompositionContext>? children)
{
var known = (children ?? Array.Empty<CompositionContext>())
.Select(c => c.Name)
.ToHashSet(StringComparer.Ordinal);
if (known.Count == 0) yield break;
var root = tree.GetRoot();
foreach (var elem in root.DescendantNodes().OfType<ElementAccessExpressionSyntax>())
{
if (elem.Expression is not IdentifierNameSyntax id || id.Identifier.ValueText != "Children")
continue;
if (elem.ArgumentList.Arguments.Count != 1) continue;
if (elem.ArgumentList.Arguments[0].Expression is not LiteralExpressionSyntax lit) continue;
if (!lit.IsKind(SyntaxKind.StringLiteralExpression)) continue;
var key = lit.Token.ValueText;
if (string.IsNullOrEmpty(key) || known.Contains(key)) continue;
var span = lit.GetLocation().GetLineSpan().Span;
yield return new DiagnosticMarker(
Severity: 4,
StartLineNumber: span.Start.Line + 1,
StartColumn: span.Start.Character + 1,
EndLineNumber: span.End.Line + 1,
EndColumn: span.End.Character + 1,
Message: $"Composition '{key}' is not declared on this template.",
Code: "SCADA007");
}
}
private enum AttributeContextKind { None, Self, Child, Parent }
private record AttributeContext(AttributeContextKind Kind, CompositionContext? Composition);
/// <summary>
/// Classifies an element-access expression as one of the scope-aware
/// attribute contexts. Recognized shapes:
/// Attributes["..."] Self
/// Children["X"].Attributes["..."] Child (composition X)
/// Parent.Attributes["..."] Parent
/// </summary>
private static AttributeContext ClassifyAttributeContext(
ElementAccessExpressionSyntax elem,
IReadOnlyList<CompositionContext> children,
CompositionContext? parent)
{
// Attributes[".."]
if (elem.Expression is IdentifierNameSyntax id && id.Identifier.ValueText == "Attributes")
return new(AttributeContextKind.Self, null);
// (something).Attributes[".."]
if (elem.Expression is MemberAccessExpressionSyntax ma && ma.Name.Identifier.ValueText == "Attributes")
{
// Children["X"].Attributes[".."]
if (ma.Expression is ElementAccessExpressionSyntax childElem
&& childElem.Expression is IdentifierNameSyntax cid
&& cid.Identifier.ValueText == "Children"
&& childElem.ArgumentList.Arguments.Count == 1
&& childElem.ArgumentList.Arguments[0].Expression is LiteralExpressionSyntax cLit
&& cLit.IsKind(SyntaxKind.StringLiteralExpression))
{
var compName = cLit.Token.ValueText;
var comp = children.FirstOrDefault(c => c.Name == compName);
if (comp != null) return new(AttributeContextKind.Child, comp);
return new(AttributeContextKind.None, null);
}
// Parent.Attributes[".."]
if (ma.Expression is IdentifierNameSyntax pid && pid.Identifier.ValueText == "Parent")
return parent != null ? new(AttributeContextKind.Parent, parent) : new(AttributeContextKind.None, null);
}
return new(AttributeContextKind.None, null);
}
private IEnumerable<DiagnosticMarker> FindArgumentTypeMismatches(SyntaxTree tree, IReadOnlyList<ScriptShape>? siblings)
{
var root = tree.GetRoot();

View File

@@ -1,10 +1,11 @@
namespace ScadaLink.CentralUI.ScriptAnalysis;
/// <summary>
/// Globals type seen by user scripts. Mirrors the surface the runtime exposes
/// today: Parameters bag plus CallShared / CallScript stubs. The methods here
/// are never invoked — Roslyn only reads their signatures to know what's in
/// scope while compiling for diagnostics + completions.
/// Globals type seen by user scripts during analysis. Mirrors the surface
/// the runtime exposes (see ScadaLink.SiteRuntime.Scripts.ScriptGlobals).
/// The methods and indexers here are never invoked — Roslyn only reads
/// their signatures to know what's in scope while compiling for diagnostics
/// and completions.
/// </summary>
public class ScriptHost
{
@@ -16,4 +17,37 @@ public class ScriptHost
/// <summary>Invokes another script on the same template and returns its result.</summary>
public object? CallScript(string name, params object?[] args) => null;
// Scope-aware accessors. SCADA-specific completion + diagnostics live in
// ScriptAnalysisService; these stubs exist so the bare Roslyn pass doesn't
// produce CS0103 errors on Attributes / Children / Parent.
public AttributeBag Attributes { get; } = new();
public ChildrenBag Children { get; } = new();
public CompositionBag? Parent { get; } = new();
public class AttributeBag
{
public object? this[string name]
{
get => null;
set { /* no-op for analyzer */ }
}
public System.Threading.Tasks.Task<object?> GetAsync(string name) =>
System.Threading.Tasks.Task.FromResult<object?>(null);
public System.Threading.Tasks.Task SetAsync(string name, object? value) =>
System.Threading.Tasks.Task.CompletedTask;
}
public class CompositionBag
{
public AttributeBag Attributes { get; } = new();
public System.Threading.Tasks.Task<object?> CallScript(string name, params object?[] args) =>
System.Threading.Tasks.Task.FromResult<object?>(null);
}
public class ChildrenBag
{
public CompositionBag this[string compositionName] => new();
}
}

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;