fix(adminui): ctx-receiver guard + truthful SetVirtualTag hover in script-editor completions

This commit is contained in:
Joseph Doherty
2026-06-18 02:39:12 -04:00
parent 74aad3bc87
commit ac3450d5f4
2 changed files with 70 additions and 3 deletions
@@ -161,7 +161,7 @@ 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));
if (_catalog != null && TryGetTagPathLiteral(token, out var pathPrefix))
if (_catalog != null && TryGetTagPathLiteral(token, out var pathPrefix, out _))
{
const string equipDot = EquipmentScriptPaths.EquipToken + "."; // "{{equip}}."
if (pathPrefix.StartsWith(equipDot, StringComparison.Ordinal))
@@ -210,9 +210,10 @@ public sealed class ScriptAnalysisService
.Select(ToCompletionItem).Take(200).ToList();
}
private static bool TryGetTagPathLiteral(SyntaxToken token, out string prefix)
private static bool TryGetTagPathLiteral(SyntaxToken token, out string prefix, out bool isSetVirtualTag)
{
prefix = "";
isSetVirtualTag = false;
var literal = token.Parent as LiteralExpressionSyntax
?? token.GetPreviousToken().Parent as LiteralExpressionSyntax;
if (literal is null || !literal.IsKind(SyntaxKind.StringLiteralExpression)) return false;
@@ -220,11 +221,16 @@ public sealed class ScriptAnalysisService
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;
// Receiver guard: only ctx.GetTag(...) / ctx.SetVirtualTag(...) are real tag-path calls. Mirrors the
// runtime harvest (EquipmentScriptPaths.GetTagRefRegex is syntactically `ctx`-anchored), so the editor
// offers tag completions/hover for exactly what Phase7Composer harvests — not an unrelated x.GetTag(...).
if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) 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 ?? "";
isSetVirtualTag = method == "SetVirtualTag";
return true;
}
@@ -252,7 +258,7 @@ public sealed class ScriptAnalysisService
// Tag-path hover takes priority over C# symbol resolution: when the caret sits on a
// ctx.GetTag("…")/ctx.SetVirtualTag("…") path literal, show the resolved tag's info
// (or note it's not a known configured tag path) instead of the string-literal symbol.
if (_catalog is not null && TryGetTagPathLiteral(token, out var tagPath) && !string.IsNullOrEmpty(tagPath))
if (_catalog is not null && TryGetTagPathLiteral(token, out var tagPath, out var isSetVirtualTag) && !string.IsNullOrEmpty(tagPath))
{
static string Code(string s) => s.Replace("`", "\\`");
// Equipment-relative literal (contains the {{equip}} token): it cannot resolve to a
@@ -270,6 +276,11 @@ public sealed class ScriptAnalysisService
? $"**Tag path** `{Code(tagPath)}`\n\n⚠ Not a known configured tag path."
: $"**Tag path** `{Code(info.Path)}`\n\n{info.Kind} · Type **{info.DataType}**"
+ (info.DriverInstanceId is null ? "" : $" · Driver `{Code(info.DriverInstanceId)}`");
// Truthfulness: ctx.SetVirtualTag(...) is a no-op in the live single-tag evaluator
// (RoslynVirtualTagEvaluator drops cross-tag writes; the cascade VirtualTagEngine is dormant).
if (isSetVirtualTag)
tagMd += "\n\n⚠ Cross-tag `SetVirtualTag` writes are currently dropped in single-tag mode " +
"(the cascade engine is not wired into the host); this write will not take effect at runtime.";
return new HoverResponse(tagMd);
}