feat(adminui): tag-path completion inside ctx.GetTag/SetVirtualTag literals

This commit is contained in:
Joseph Doherty
2026-06-09 14:53:15 -04:00
parent d1434933b4
commit 521fb61e44
2 changed files with 71 additions and 2 deletions
@@ -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