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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Script</label>
|
<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>
|
</div>
|
||||||
|
|
||||||
@if (_formError != null)
|
@if (_formError != null)
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small">Code</label>
|
<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>
|
</div>
|
||||||
@if (_formError != null)
|
@if (_formError != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -654,7 +654,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Code</label>
|
<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>
|
</div>
|
||||||
@if (_scriptFormError != null)
|
@if (_scriptFormError != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,18 @@
|
|||||||
[Parameter] public string Height { get; set; } = "320px";
|
[Parameter] public string Height { get; set; } = "320px";
|
||||||
[Parameter] public bool ReadOnly { get; set; } = false;
|
[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 ElementReference _hostRef;
|
||||||
private DotNetObjectReference<MonacoEditor>? _dotNetRef;
|
private DotNetObjectReference<MonacoEditor>? _dotNetRef;
|
||||||
private readonly string _id = Guid.NewGuid().ToString("N");
|
private readonly string _id = Guid.NewGuid().ToString("N");
|
||||||
@@ -52,12 +64,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnValueChanged(string newValue)
|
public Task OnValueChanged(string newValue)
|
||||||
{
|
{
|
||||||
_lastSentValue = 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()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_initialized)
|
if (_initialized)
|
||||||
@@ -66,4 +87,6 @@
|
|||||||
}
|
}
|
||||||
_dotNetRef?.Dispose();
|
_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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,12 @@ public record DiagnosticMarker(
|
|||||||
string Message,
|
string Message,
|
||||||
string Code);
|
string Code);
|
||||||
|
|
||||||
public record CompletionsRequest(string CodeText, int Line, int Column);
|
public record CompletionsRequest(
|
||||||
|
string CodeText,
|
||||||
|
int Line,
|
||||||
|
int Column,
|
||||||
|
IReadOnlyList<string>? DeclaredParameters = null,
|
||||||
|
IReadOnlyList<string>? SiblingScripts = null);
|
||||||
|
|
||||||
public record CompletionsResponse(IReadOnlyList<CompletionItem> Items);
|
public record CompletionsResponse(IReadOnlyList<CompletionItem> Items);
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ public static class ScriptAnalysisEndpoints
|
|||||||
group.MapPost("/diagnostics", (DiagnoseRequest req, ScriptAnalysisService svc) =>
|
group.MapPost("/diagnostics", (DiagnoseRequest req, ScriptAnalysisService svc) =>
|
||||||
Results.Ok(svc.Diagnose(req)));
|
Results.Ok(svc.Diagnose(req)));
|
||||||
|
|
||||||
group.MapPost("/completions", (CompletionsRequest req, ScriptAnalysisService svc) =>
|
group.MapPost("/completions", async (CompletionsRequest req, ScriptAnalysisService svc) =>
|
||||||
Results.Ok(svc.Complete(req)));
|
Results.Ok(await svc.CompleteAsync(req)));
|
||||||
|
|
||||||
return endpoints;
|
return endpoints;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp;
|
|||||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
using Microsoft.CodeAnalysis.Scripting;
|
using Microsoft.CodeAnalysis.Scripting;
|
||||||
|
using ScadaLink.TemplateEngine;
|
||||||
|
|
||||||
namespace ScadaLink.CentralUI.ScriptAnalysis;
|
namespace ScadaLink.CentralUI.ScriptAnalysis;
|
||||||
|
|
||||||
@@ -11,6 +12,12 @@ namespace ScadaLink.CentralUI.ScriptAnalysis;
|
|||||||
/// <see cref="ScriptHost"/> globals and surfaces diagnostics + completions
|
/// <see cref="ScriptHost"/> globals and surfaces diagnostics + completions
|
||||||
/// in the shape Monaco's provider APIs expect. Lightweight — no caching;
|
/// in the shape Monaco's provider APIs expect. Lightweight — no caching;
|
||||||
/// each request rebuilds the script. Acceptable for human-paced edits.
|
/// each request rebuilds the script. Acceptable for human-paced edits.
|
||||||
|
///
|
||||||
|
/// Beyond plain C# analysis, layers SCADA-specific extensions:
|
||||||
|
/// - In-string completion of Parameters["..."] keys (from the request's
|
||||||
|
/// DeclaredParameters), CallShared("...") names (from SharedScriptService),
|
||||||
|
/// and CallScript("...") names (from the request's SiblingScripts).
|
||||||
|
/// - Forbidden-API diagnostic for the documented script trust model.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ScriptAnalysisService
|
public class ScriptAnalysisService
|
||||||
{
|
{
|
||||||
@@ -28,6 +35,34 @@ public class ScriptAnalysisService
|
|||||||
"System.Text",
|
"System.Text",
|
||||||
"System.Threading.Tasks");
|
"System.Threading.Tasks");
|
||||||
|
|
||||||
|
// Namespaces and types banned by the script trust model.
|
||||||
|
// Tasks live under System.Threading.Tasks and remain allowed.
|
||||||
|
private static readonly string[] ForbiddenNamespacePrefixes =
|
||||||
|
{
|
||||||
|
"System.IO",
|
||||||
|
"System.Diagnostics",
|
||||||
|
"System.Reflection",
|
||||||
|
"System.Net",
|
||||||
|
"System.Threading.Thread",
|
||||||
|
"System.Threading.Tasks.Sources",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ForbiddenTypeNames = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
"File", "Directory", "Path", "StreamReader", "StreamWriter", "FileStream",
|
||||||
|
"Process", "ProcessStartInfo",
|
||||||
|
"Assembly", "Type", "MethodInfo", "PropertyInfo", "FieldInfo",
|
||||||
|
"Socket", "TcpClient", "UdpClient", "TcpListener",
|
||||||
|
"Thread", "ThreadPool", "Mutex", "Semaphore",
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly SharedScriptService _sharedScripts;
|
||||||
|
|
||||||
|
public ScriptAnalysisService(SharedScriptService sharedScripts)
|
||||||
|
{
|
||||||
|
_sharedScripts = sharedScripts;
|
||||||
|
}
|
||||||
|
|
||||||
public DiagnoseResponse Diagnose(DiagnoseRequest request)
|
public DiagnoseResponse Diagnose(DiagnoseRequest request)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(request.Code))
|
if (string.IsNullOrEmpty(request.Code))
|
||||||
@@ -53,10 +88,16 @@ public class ScriptAnalysisService
|
|||||||
.Select(ToMarker)
|
.Select(ToMarker)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var tree = compilation.SyntaxTrees.FirstOrDefault();
|
||||||
|
if (tree != null)
|
||||||
|
{
|
||||||
|
markers.AddRange(FindForbiddenApiUsages(tree));
|
||||||
|
}
|
||||||
|
|
||||||
return new DiagnoseResponse(markers);
|
return new DiagnoseResponse(markers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletionsResponse Complete(CompletionsRequest request)
|
public async Task<CompletionsResponse> CompleteAsync(CompletionsRequest request)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(request.CodeText))
|
if (string.IsNullOrEmpty(request.CodeText))
|
||||||
return new CompletionsResponse(Array.Empty<CompletionItem>());
|
return new CompletionsResponse(Array.Empty<CompletionItem>());
|
||||||
@@ -82,7 +123,13 @@ public class ScriptAnalysisService
|
|||||||
var root = tree.GetRoot();
|
var root = tree.GetRoot();
|
||||||
var token = root.FindToken(Math.Max(0, position - 1));
|
var token = root.FindToken(Math.Max(0, position - 1));
|
||||||
|
|
||||||
// Dot completion: look up members of the type on the left of the dot.
|
// SCADA-specific string-literal completions take priority over plain C#
|
||||||
|
// because they're the actually useful suggestions inside those literals.
|
||||||
|
var stringMatches = await TryStringLiteralCompletions(token, request);
|
||||||
|
if (stringMatches != null)
|
||||||
|
return new CompletionsResponse(stringMatches);
|
||||||
|
|
||||||
|
// Dot completion: members of the type on the left of the dot.
|
||||||
var dotMembers = TryGetDotMembers(token, semanticModel);
|
var dotMembers = TryGetDotMembers(token, semanticModel);
|
||||||
if (dotMembers != null)
|
if (dotMembers != null)
|
||||||
return new CompletionsResponse(dotMembers);
|
return new CompletionsResponse(dotMembers);
|
||||||
@@ -99,10 +146,62 @@ public class ScriptAnalysisService
|
|||||||
return new CompletionsResponse(scoped);
|
return new CompletionsResponse(scoped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<CompletionItem>?> TryStringLiteralCompletions(
|
||||||
|
SyntaxToken token, CompletionsRequest request)
|
||||||
|
{
|
||||||
|
// The token at the cursor must be (or be adjacent to) a string literal.
|
||||||
|
var literal = token.IsKind(SyntaxKind.StringLiteralToken)
|
||||||
|
? token
|
||||||
|
: token.GetPreviousToken().IsKind(SyntaxKind.StringLiteralToken)
|
||||||
|
? token.GetPreviousToken()
|
||||||
|
: default;
|
||||||
|
if (literal == default) return null;
|
||||||
|
|
||||||
|
// Token tree shape: StringLiteralToken → LiteralExpression → Argument →
|
||||||
|
// (ArgumentList | BracketedArgumentList) → invocation or element-access.
|
||||||
|
var argument = literal.Parent?.Parent as ArgumentSyntax;
|
||||||
|
var argumentList = argument?.Parent;
|
||||||
|
var owner = argumentList?.Parent;
|
||||||
|
|
||||||
|
// Parameters["..."]
|
||||||
|
if (owner is ElementAccessExpressionSyntax elem
|
||||||
|
&& elem.Expression is IdentifierNameSyntax id
|
||||||
|
&& id.Identifier.ValueText == "Parameters")
|
||||||
|
{
|
||||||
|
return (request.DeclaredParameters ?? Array.Empty<string>())
|
||||||
|
.Distinct()
|
||||||
|
.Select(n => new CompletionItem(n, n, "declared parameter", "Variable"))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallShared("...") / CallScript("...")
|
||||||
|
if (owner is InvocationExpressionSyntax inv)
|
||||||
|
{
|
||||||
|
var calleeName = (inv.Expression as IdentifierNameSyntax)?.Identifier.ValueText
|
||||||
|
?? (inv.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.ValueText;
|
||||||
|
|
||||||
|
if (calleeName == "CallShared")
|
||||||
|
{
|
||||||
|
var scripts = await _sharedScripts.GetAllSharedScriptsAsync();
|
||||||
|
return scripts
|
||||||
|
.Select(s => new CompletionItem(s.Name, s.Name, "shared script", "Method"))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calleeName == "CallScript")
|
||||||
|
{
|
||||||
|
return (request.SiblingScripts ?? Array.Empty<string>())
|
||||||
|
.Distinct()
|
||||||
|
.Select(n => new CompletionItem(n, n, "sibling script", "Method"))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<CompletionItem>? TryGetDotMembers(SyntaxToken token, SemanticModel model)
|
private static List<CompletionItem>? TryGetDotMembers(SyntaxToken token, SemanticModel model)
|
||||||
{
|
{
|
||||||
// The cursor may be positioned right after a '.'; resolve the
|
|
||||||
// member-access node and look up the left-hand side's type members.
|
|
||||||
var memberAccess = token.Parent as MemberAccessExpressionSyntax
|
var memberAccess = token.Parent as MemberAccessExpressionSyntax
|
||||||
?? token.GetPreviousToken().Parent as MemberAccessExpressionSyntax;
|
?? token.GetPreviousToken().Parent as MemberAccessExpressionSyntax;
|
||||||
if (memberAccess == null) return null;
|
if (memberAccess == null) return null;
|
||||||
@@ -121,6 +220,57 @@ public class ScriptAnalysisService
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<DiagnosticMarker> FindForbiddenApiUsages(SyntaxTree tree)
|
||||||
|
{
|
||||||
|
var root = tree.GetRoot();
|
||||||
|
|
||||||
|
// Banned using directives.
|
||||||
|
foreach (var u in root.DescendantNodes().OfType<UsingDirectiveSyntax>())
|
||||||
|
{
|
||||||
|
var name = u.Name?.ToString() ?? "";
|
||||||
|
if (ForbiddenNamespacePrefixes.Any(p => name == p || name.StartsWith(p + ".")))
|
||||||
|
{
|
||||||
|
var span = u.GetLocation().GetLineSpan().Span;
|
||||||
|
yield return new DiagnosticMarker(
|
||||||
|
Severity: 8,
|
||||||
|
StartLineNumber: span.Start.Line + 1,
|
||||||
|
StartColumn: span.Start.Character + 1,
|
||||||
|
EndLineNumber: span.End.Line + 1,
|
||||||
|
EndColumn: span.End.Character + 1,
|
||||||
|
Message: $"Forbidden namespace '{name}' is not allowed in scripts (script trust model).",
|
||||||
|
Code: "SCADA001");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Banned type identifiers (e.g., new Process(), File.ReadAllText, etc.).
|
||||||
|
// Note: this is a name-based heuristic — false positives are possible for
|
||||||
|
// user identifiers that happen to share names with forbidden types.
|
||||||
|
foreach (var ident in root.DescendantNodes().OfType<IdentifierNameSyntax>())
|
||||||
|
{
|
||||||
|
var name = ident.Identifier.ValueText;
|
||||||
|
if (ForbiddenTypeNames.Contains(name))
|
||||||
|
{
|
||||||
|
// Filter: only flag when used as a type or as a member-access target.
|
||||||
|
var parent = ident.Parent;
|
||||||
|
var isTypeOrAccess =
|
||||||
|
parent is MemberAccessExpressionSyntax m && m.Expression == ident ||
|
||||||
|
parent is QualifiedNameSyntax ||
|
||||||
|
parent is ObjectCreationExpressionSyntax;
|
||||||
|
if (!isTypeOrAccess) continue;
|
||||||
|
|
||||||
|
var span = ident.GetLocation().GetLineSpan().Span;
|
||||||
|
yield return new DiagnosticMarker(
|
||||||
|
Severity: 8,
|
||||||
|
StartLineNumber: span.Start.Line + 1,
|
||||||
|
StartColumn: span.Start.Character + 1,
|
||||||
|
EndLineNumber: span.End.Line + 1,
|
||||||
|
EndColumn: span.End.Character + 1,
|
||||||
|
Message: $"Type '{name}' is forbidden in scripts (script trust model).",
|
||||||
|
Code: "SCADA002");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static CompletionItem ToCompletionItem(ISymbol symbol)
|
private static CompletionItem ToCompletionItem(ISymbol symbol)
|
||||||
{
|
{
|
||||||
var kind = symbol.Kind switch
|
var kind = symbol.Kind switch
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IDialogService, DialogService>();
|
services.AddScoped<IDialogService, DialogService>();
|
||||||
|
|
||||||
// Roslyn-backed C# analysis for the Monaco script editor.
|
// Roslyn-backed C# analysis for the Monaco script editor.
|
||||||
services.AddSingleton<ScriptAnalysisService>();
|
// Scoped because SharedScriptService (a dependency) is scoped.
|
||||||
|
services.AddScoped<ScriptAnalysisService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,25 @@
|
|||||||
triggerCharacters: [".", "(", "\""],
|
triggerCharacters: [".", "(", "\""],
|
||||||
provideCompletionItems: async function (model, position) {
|
provideCompletionItems: async function (model, position) {
|
||||||
try {
|
try {
|
||||||
|
// Find which editor instance owns this model so we can ask
|
||||||
|
// the Blazor side for the latest form context.
|
||||||
|
// Blazor JS interop serializes records as PascalCase; we
|
||||||
|
// normalize to camelCase here.
|
||||||
|
let ctx = { declaredParameters: [], siblingScripts: [] };
|
||||||
|
for (const key in editors) {
|
||||||
|
if (editors[key].editor.getModel() === model) {
|
||||||
|
try {
|
||||||
|
const got = await editors[key].dotNetRef.invokeMethodAsync("GetContext");
|
||||||
|
if (got) {
|
||||||
|
ctx = {
|
||||||
|
declaredParameters: got.DeclaredParameters || got.declaredParameters || [],
|
||||||
|
siblingScripts: got.SiblingScripts || got.siblingScripts || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) { /* fall through */ }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
const resp = await fetch("/api/script-analysis/completions", {
|
const resp = await fetch("/api/script-analysis/completions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
@@ -48,7 +67,9 @@
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
codeText: model.getValue(),
|
codeText: model.getValue(),
|
||||||
line: position.lineNumber,
|
line: position.lineNumber,
|
||||||
column: position.column
|
column: position.column,
|
||||||
|
declaredParameters: ctx.declaredParameters,
|
||||||
|
siblingScripts: ctx.siblingScripts
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (!resp.ok) return { suggestions: [] };
|
if (!resp.ok) return { suggestions: [] };
|
||||||
|
|||||||
Reference in New Issue
Block a user