feat(adminui): tag-path completion inside ctx.GetTag/SetVirtualTag literals
This commit is contained in:
@@ -29,8 +29,13 @@ public sealed class ScriptAnalysisService
|
||||
OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release,
|
||||
allowUnsafe: false, warningLevel: 4, nullableContextOptions: NullableContextOptions.Enable);
|
||||
|
||||
private readonly IScriptTagCatalog? _catalog;
|
||||
private readonly ILogger<ScriptAnalysisService>? _logger;
|
||||
public ScriptAnalysisService(ILogger<ScriptAnalysisService>? logger = null) => _logger = logger;
|
||||
public ScriptAnalysisService(IScriptTagCatalog? catalog = null, ILogger<ScriptAnalysisService>? logger = null)
|
||||
{
|
||||
_catalog = catalog;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// Mirrors ScriptEvaluator's wrapper EXACTLY (usings from the sandbox + the
|
||||
// Run(ScriptGlobals<VirtualTagContext>) shape), except the analysis return type is
|
||||
@@ -154,7 +159,12 @@ public sealed class ScriptAnalysisService
|
||||
// position is the offset just AFTER the caret; -1 finds the token the caret sits at/just-after (e.g. the dot in "ctx.").
|
||||
var token = root.FindToken(Math.Max(0, position - 1));
|
||||
|
||||
// Task 6 inserts tag-path string-literal completion here (inside ctx.GetTag("…")/ctx.SetVirtualTag("…")).
|
||||
if (_catalog != null && TryGetTagPathLiteral(token, out var pathPrefix))
|
||||
{
|
||||
var paths = await _catalog.GetPathsAsync(pathPrefix, CancellationToken.None);
|
||||
return new CompletionsResponse(
|
||||
paths.Select(p => new CompletionItem(p, p, "tag path", "Field")).ToList());
|
||||
}
|
||||
|
||||
var dot = TryGetDotMembers(token, model);
|
||||
if (dot != null) return new CompletionsResponse(dot);
|
||||
@@ -188,6 +198,24 @@ public sealed class ScriptAnalysisService
|
||||
.Select(ToCompletionItem).Take(200).ToList();
|
||||
}
|
||||
|
||||
private static bool TryGetTagPathLiteral(SyntaxToken token, out string prefix)
|
||||
{
|
||||
prefix = "";
|
||||
var literal = token.Parent as LiteralExpressionSyntax
|
||||
?? token.GetPreviousToken().Parent as LiteralExpressionSyntax;
|
||||
if (literal is null || !literal.IsKind(SyntaxKind.StringLiteralExpression)) return false;
|
||||
if (literal.Parent is not ArgumentSyntax arg) return false;
|
||||
if (arg.Parent is not ArgumentListSyntax argList) return false;
|
||||
if (argList.Parent is not InvocationExpressionSyntax inv) return false;
|
||||
if (inv.Expression is not MemberAccessExpressionSyntax ma) return false;
|
||||
var method = ma.Name.Identifier.ValueText;
|
||||
if (method is not ("GetTag" or "SetVirtualTag")) return false;
|
||||
// only the FIRST argument is the path
|
||||
if (argList.Arguments.Count > 0 && argList.Arguments[0] != arg) return false;
|
||||
prefix = literal.Token.ValueText ?? "";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CompletionItem ToCompletionItem(ISymbol symbol)
|
||||
{
|
||||
var kind = symbol.Kind switch
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
|
||||
|
||||
public sealed class TagPathCompletionTests
|
||||
{
|
||||
private sealed class FakeCatalog : IScriptTagCatalog
|
||||
{
|
||||
public Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<string>>(new[] { "Motor.Speed", "Motor.Temp" });
|
||||
}
|
||||
|
||||
private static readonly ScriptAnalysisService Svc = new(new FakeCatalog());
|
||||
|
||||
private static async Task<IReadOnlyList<string>> Labels(string code, int line, int col)
|
||||
=> (await Svc.CompleteAsync(new CompletionsRequest(code, line, col))).Items.Select(i => i.Label).ToList();
|
||||
|
||||
[Fact] public async Task Empty_literal_in_GetTag_offers_tag_paths()
|
||||
{
|
||||
// caret between the quotes of ctx.GetTag("") — verify/adjust the column so the caret is inside the literal.
|
||||
(await Labels("""ctx.GetTag("")""", 1, 13)).ShouldContain("Motor.Speed");
|
||||
}
|
||||
|
||||
[Fact] public async Task Partial_literal_in_GetTag_offers_tag_paths()
|
||||
{
|
||||
(await Labels("""ctx.GetTag("Mot")""", 1, 15)).ShouldContain("Motor.Speed");
|
||||
}
|
||||
|
||||
[Fact] public async Task SetVirtualTag_literal_also_offers_tag_paths()
|
||||
{
|
||||
(await Labels("""ctx.SetVirtualTag("")""", 1, 20)).ShouldContain("Motor.Speed");
|
||||
}
|
||||
|
||||
[Fact] public async Task Outside_a_tag_literal_does_not_offer_tag_paths()
|
||||
{
|
||||
// a string literal NOT inside GetTag/SetVirtualTag → no tag-path suggestions
|
||||
(await Labels("""var s = "";""", 1, 10)).ShouldNotContain("Motor.Speed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user