feat(adminui): tag-path hover (tag kind/type/driver inside ctx.GetTag literals)
v2-ci / build (push) Failing after 47s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 47s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
This commit is contained in:
@@ -15,8 +15,14 @@ public interface IScriptTagCatalog
|
||||
/// <param name="filter">Case-insensitive StartsWith prefix; <c>null</c>/empty returns all (bounded).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct);
|
||||
|
||||
/// <summary>Exact (case-sensitive, Ordinal) lookup of a resolvable tag path; null when not a known configured path.</summary>
|
||||
Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>Resolved info for one configured tag/virtual-tag path (for hover).</summary>
|
||||
public sealed record ScriptTagInfo(string Path, string Kind, string DataType, string? DriverInstanceId);
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="IScriptTagCatalog"/>. Returns ONLY the resolvable keys a script may pass to
|
||||
/// <c>ctx.GetTag</c> / <c>ctx.SetVirtualTag</c> — the driver <c>FullName</c> for equipment tags,
|
||||
@@ -69,44 +75,9 @@ public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> d
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var entries = await BuildEntriesAsync(ct);
|
||||
|
||||
// Only the resolvable keys are projected, so no UNS join is needed: the equipment-tag key is
|
||||
// the FullName from TagConfig, the SystemPlatform key is FolderPath/Name, and the virtual-tag
|
||||
// key is its own Name. Pull just those columns, untracked.
|
||||
var tagRows = await db.Tags
|
||||
.AsNoTracking()
|
||||
.Select(t => new { t.EquipmentId, t.Name, t.FolderPath, t.TagConfig })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var vtagRows = await db.VirtualTags
|
||||
.AsNoTracking()
|
||||
.Select(v => new { v.Name })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var t in tagRows)
|
||||
{
|
||||
if (t.EquipmentId is null)
|
||||
{
|
||||
// SystemPlatform tag — the Galaxy driver subscribes by the MXAccess dot-ref, which is
|
||||
// "FolderPath.Name" when a folder is set, else just "Name".
|
||||
paths.Add(string.IsNullOrWhiteSpace(t.FolderPath) ? t.Name : $"{t.FolderPath}.{t.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Equipment driver tag — the runtime GetTag key is the driver FullName from TagConfig.
|
||||
paths.Add(ExtractFullNameFromTagConfig(t.TagConfig));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var v in vtagRows)
|
||||
{
|
||||
// Virtual tag — best-effort: the live resolution of virtual-tag cascade/write targets is
|
||||
// unconfirmed, so emit the leaf Name only (no UNS browse path, which never resolves).
|
||||
paths.Add(v.Name);
|
||||
}
|
||||
var paths = new HashSet<string>(entries.Select(e => e.Path), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
IEnumerable<string> query = paths;
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
@@ -120,6 +91,63 @@ public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> d
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
|
||||
{
|
||||
var entries = await BuildEntriesAsync(ct);
|
||||
|
||||
// Exact, case-SENSITIVE (Ordinal) match: the live runtime resolves a ctx.GetTag("X") literal
|
||||
// via DependencyMuxActor keyed with StringComparer.Ordinal, so a case-mismatched path would
|
||||
// not resolve at runtime — hover must report it as "not a known configured tag path".
|
||||
return entries.FirstOrDefault(e => string.Equals(e.Path, path, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads Tags + VirtualTags (untracked) and projects one <see cref="ScriptTagInfo"/> per row —
|
||||
/// the SINGLE source of the per-category resolvable-key projection shared by
|
||||
/// <see cref="GetPathsAsync"/> (distinct paths, prefix-filtered) and
|
||||
/// <see cref="GetTagInfoAsync"/> (exact Ordinal lookup).
|
||||
/// </summary>
|
||||
private async Task<IReadOnlyList<ScriptTagInfo>> BuildEntriesAsync(CancellationToken ct)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Only the resolvable keys are projected, so no UNS join is needed: the equipment-tag key is
|
||||
// the FullName from TagConfig, the SystemPlatform key is FolderPath/Name, and the virtual-tag
|
||||
// key is its own Name. Pull just those columns, untracked.
|
||||
var tagRows = await db.Tags
|
||||
.AsNoTracking()
|
||||
.Select(t => new { t.EquipmentId, t.Name, t.FolderPath, t.DataType, t.DriverInstanceId, t.TagConfig })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var vtagRows = await db.VirtualTags
|
||||
.AsNoTracking()
|
||||
.Select(v => new { v.Name, v.DataType })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var entries = new List<ScriptTagInfo>(tagRows.Count + vtagRows.Count);
|
||||
|
||||
foreach (var t in tagRows)
|
||||
{
|
||||
var path = t.EquipmentId is null
|
||||
// SystemPlatform tag — the Galaxy driver subscribes by the MXAccess dot-ref, which is
|
||||
// "FolderPath.Name" when a folder is set, else just "Name".
|
||||
? (string.IsNullOrWhiteSpace(t.FolderPath) ? t.Name : $"{t.FolderPath}.{t.Name}")
|
||||
// Equipment driver tag — the runtime GetTag key is the driver FullName from TagConfig.
|
||||
: ExtractFullNameFromTagConfig(t.TagConfig);
|
||||
entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, t.DriverInstanceId));
|
||||
}
|
||||
|
||||
foreach (var v in vtagRows)
|
||||
{
|
||||
// Virtual tag — best-effort: the live resolution of virtual-tag cascade/write targets is
|
||||
// unconfirmed, so emit the leaf Name only (no UNS browse path, which never resolves).
|
||||
entries.Add(new ScriptTagInfo(v.Name, "Virtual tag", v.DataType, null));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the
|
||||
/// top-level <c>FullName</c> string every shipped driver stores. Mirrors
|
||||
|
||||
@@ -12,7 +12,7 @@ public static class ScriptAnalysisEndpoints
|
||||
var group = endpoints.MapGroup("/api/script-analysis").RequireAuthorization("FleetAdmin");
|
||||
group.MapPost("/diagnostics", (DiagnoseRequest r, ScriptAnalysisService s) => Results.Ok(s.Diagnose(r)));
|
||||
group.MapPost("/completions", async (CompletionsRequest r, ScriptAnalysisService s) => Results.Ok(await s.CompleteAsync(r)));
|
||||
group.MapPost("/hover", (HoverRequest r, ScriptAnalysisService s) => Results.Ok(s.Hover(r)));
|
||||
group.MapPost("/hover", async (HoverRequest r, ScriptAnalysisService s) => Results.Ok(await s.Hover(r)));
|
||||
group.MapPost("/signature-help", (SignatureHelpRequest r, ScriptAnalysisService s) => Results.Ok(s.SignatureHelp(r)));
|
||||
group.MapPost("/format", (FormatRequest r, ScriptAnalysisService s) => Results.Ok(s.Format(r)));
|
||||
group.MapPost("/inlay-hints", (InlayHintsRequest r, ScriptAnalysisService s) => Results.Ok(s.InlayHints(r)));
|
||||
|
||||
@@ -227,7 +227,7 @@ public sealed class ScriptAnalysisService
|
||||
return new CompletionItem(symbol.Name, symbol.Name,
|
||||
symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), kind);
|
||||
}
|
||||
public HoverResponse Hover(HoverRequest req)
|
||||
public async Task<HoverResponse> Hover(HoverRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.CodeText)) return new HoverResponse(null);
|
||||
try
|
||||
@@ -236,6 +236,21 @@ public sealed class ScriptAnalysisService
|
||||
var (tree, _, model, preambleLength) = Analyze(code);
|
||||
var position = OffsetInWrapped(code, req.Line, req.Column, preambleLength);
|
||||
var token = tree.GetRoot().FindToken(Math.Max(0, position - 1));
|
||||
|
||||
// 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))
|
||||
{
|
||||
var info = await _catalog.GetTagInfoAsync(tagPath, CancellationToken.None);
|
||||
static string Code(string s) => s.Replace("`", "\\`");
|
||||
var tagMd = info is null
|
||||
? $"**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)}`");
|
||||
return new HoverResponse(tagMd);
|
||||
}
|
||||
|
||||
var node = token.Parent;
|
||||
if (node is null) return new HoverResponse(null);
|
||||
// Resolve the token's node; if that yields nothing, climb to the enclosing
|
||||
|
||||
Reference in New Issue
Block a user