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:
@@ -38,7 +38,9 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Script</label>
|
||||
<MonacoEditor Value="@_script" ValueChanged="@(v => _script = v)" Language="csharp" Height="320px" />
|
||||
<MonacoEditor Value="@_script" ValueChanged="@(v => _script = v)"
|
||||
Language="csharp" Height="320px"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_params)" />
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Code</label>
|
||||
<MonacoEditor Value="@_formCode" ValueChanged="@(v => _formCode = v)" Language="csharp" Height="320px" />
|
||||
<MonacoEditor Value="@_formCode" ValueChanged="@(v => _formCode = v)"
|
||||
Language="csharp" Height="320px"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_formParameters)" />
|
||||
</div>
|
||||
@if (_formError != null)
|
||||
{
|
||||
|
||||
@@ -654,7 +654,10 @@
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Code</label>
|
||||
<MonacoEditor Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)" Language="csharp" Height="320px" />
|
||||
<MonacoEditor Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)"
|
||||
Language="csharp" Height="320px"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)"
|
||||
SiblingScripts="@(_scripts.Select(s => s.Name).ToArray())" />
|
||||
</div>
|
||||
@if (_scriptFormError != null)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ScadaLink.CentralUI.Components.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the parameter-definitions JSON written by ParameterListEditor and
|
||||
/// returns the declared parameter names. Used by script-edit pages to feed
|
||||
/// the Monaco editor's Parameters["..."] completion provider.
|
||||
/// </summary>
|
||||
public static class ScriptParameterNames
|
||||
{
|
||||
public static IReadOnlyList<string> Parse(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<string>();
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Array) return Array.Empty<string>();
|
||||
return doc.RootElement.EnumerateArray()
|
||||
.Select(e => e.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "")
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user