feat(ui/scripts): SCADA-specific Monaco extensions

Wave 3 of the Monaco/Roslyn integration. Adds the four extensions
agreed in the design Q&A:

  1. Parameters["..."] keys — when the cursor is inside a string
     literal that's the index of a Parameters[] element-access,
     completions return the parameter names declared in the form's
     ParameterListEditor.
  2. CallShared("...") names — when the cursor is inside a string
     literal argument to a CallShared(...) invocation, completions
     return the names of all shared scripts (resolved server-side
     via SharedScriptService).
  3. CallScript("...") names — same shape, but uses sibling-script
     names passed from the form (TemplateEdit's _scripts list).
  4. Forbidden-API diagnostic — squiggles uses of the documented
     script trust model bans: System.IO / Diagnostics / Reflection /
     Net / Threading.Thread namespaces, plus the named types File,
     Directory, Process, Thread, Socket, etc. New diagnostic codes
     SCADA001 (using directive) and SCADA002 (type identifier).

ScriptAnalysisService gains a SharedScriptService dependency
(scoped, hence the analyzer is now scoped too); CompletionsRequest
carries DeclaredParameters and SiblingScripts; Complete is now async.

MonacoEditor.razor exposes DeclaredParameters / SiblingScripts
parameters plus a [JSInvokable] GetContext() so the JS side asks
for the latest form state on every completion request. The
provider in monaco-init.js looks up the owning editor from the
internal editors map and forwards the context.

ScriptParameterNames helper parses the ParameterListEditor JSON
into a name list — used by SharedScriptForm, ApiMethodForm, and
TemplateEdit's Add-Script form to populate the Monaco context.

Smoke-verified via direct fetch + Monaco trigger:
  - var x = Parameters["  →  popup: "name" (declared parameter)
  - var y = CallShared("  →  popup: GetWeather, Greet
  - using System.IO;      →  SCADA001 squiggle
  - Process.Start(...)    →  SCADA002 squiggle
  - File.ReadAllText(...) →  SCADA002 squiggle

Also fixed: ScriptAnalysisService scoped (was singleton, broke DI
because SharedScriptService is scoped); JS normalizes Pascal-case
context keys from Blazor's record serialization to camel-case for
the request body.
This commit is contained in:
Joseph Doherty
2026-05-12 04:56:56 -04:00
parent cf9548e9ed
commit 225817eac9
10 changed files with 250 additions and 14 deletions
@@ -12,6 +12,18 @@
[Parameter] public string Height { get; set; } = "320px";
[Parameter] public bool ReadOnly { get; set; } = false;
/// <summary>
/// Parameter names declared on the form (from the ParameterListEditor),
/// surfaced as completions inside Parameters["..."] literals.
/// </summary>
[Parameter] public IReadOnlyList<string>? DeclaredParameters { get; set; }
/// <summary>
/// Names of other scripts on the same template, surfaced as completions
/// inside CallScript("...") literals.
/// </summary>
[Parameter] public IReadOnlyList<string>? SiblingScripts { get; set; }
private ElementReference _hostRef;
private DotNetObjectReference<MonacoEditor>? _dotNetRef;
private readonly string _id = Guid.NewGuid().ToString("N");
@@ -52,12 +64,21 @@
}
[JSInvokable]
public async Task OnValueChanged(string newValue)
public Task OnValueChanged(string newValue)
{
_lastSentValue = newValue ?? "";
await ValueChanged.InvokeAsync(_lastSentValue);
return ValueChanged.InvokeAsync(_lastSentValue);
}
/// <summary>
/// Called from JS at completion-request time so the form's latest state is
/// passed through, not whatever was captured when the editor was created.
/// </summary>
[JSInvokable]
public ScadaContext GetContext() => new(
DeclaredParameters?.ToArray() ?? Array.Empty<string>(),
SiblingScripts?.ToArray() ?? Array.Empty<string>());
public async ValueTask DisposeAsync()
{
if (_initialized)
@@ -66,4 +87,6 @@
}
_dotNetRef?.Dispose();
}
public record ScadaContext(string[] DeclaredParameters, string[] SiblingScripts);
}