feat(adminui): scope + dot-member script completions
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
|
||||
@@ -141,8 +142,63 @@ public sealed class ScriptAnalysisService
|
||||
if (code[i] == '\n') { line++; col = 1; } else col++;
|
||||
return (line, col);
|
||||
}
|
||||
public Task<CompletionsResponse> CompleteAsync(CompletionsRequest req)
|
||||
=> Task.FromResult(new CompletionsResponse(Array.Empty<CompletionItem>())); // Tasks 4,6
|
||||
public async Task<CompletionsResponse> CompleteAsync(CompletionsRequest req)
|
||||
{
|
||||
if (string.IsNullOrEmpty(req.CodeText)) return new CompletionsResponse(Array.Empty<CompletionItem>());
|
||||
try
|
||||
{
|
||||
var code = Normalize(req.CodeText);
|
||||
var (tree, _, model, preambleLength) = Analyze(code);
|
||||
var position = OffsetInWrapped(code, req.Line, req.Column, preambleLength);
|
||||
var root = await tree.GetRootAsync();
|
||||
// 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("…")).
|
||||
|
||||
var dot = TryGetDotMembers(token, model);
|
||||
if (dot != null) return new CompletionsResponse(dot);
|
||||
|
||||
var scoped = model.LookupSymbols(position)
|
||||
.Where(s => !s.IsImplicitlyDeclared && !string.IsNullOrEmpty(s.Name))
|
||||
.GroupBy(s => s.Name).Select(g => g.First())
|
||||
.Select(ToCompletionItem).Take(200).ToList();
|
||||
return new CompletionsResponse(scoped);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Script completion failed; returning none.");
|
||||
return new CompletionsResponse(Array.Empty<CompletionItem>());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CompletionItem>? TryGetDotMembers(SyntaxToken token, SemanticModel model)
|
||||
{
|
||||
var memberAccess = token.Parent as MemberAccessExpressionSyntax
|
||||
?? token.GetPreviousToken().Parent as MemberAccessExpressionSyntax;
|
||||
if (memberAccess == null) return null;
|
||||
var typeInfo = model.GetTypeInfo(memberAccess.Expression);
|
||||
var type = typeInfo.Type ?? typeInfo.ConvertedType;
|
||||
if (type == null) return null;
|
||||
return type.GetMembers()
|
||||
.Where(m => m.CanBeReferencedByName && !m.IsImplicitlyDeclared)
|
||||
// NotApplicable covers members (e.g. some BCL/special members) that carry no explicit accessibility but are referenceable.
|
||||
.Where(m => m.DeclaredAccessibility == Accessibility.Public || m.DeclaredAccessibility == Accessibility.NotApplicable)
|
||||
.GroupBy(m => m.Name).Select(g => g.First())
|
||||
.Select(ToCompletionItem).Take(200).ToList();
|
||||
}
|
||||
|
||||
private static CompletionItem ToCompletionItem(ISymbol symbol)
|
||||
{
|
||||
var kind = symbol.Kind switch
|
||||
{
|
||||
SymbolKind.Method => "Method", SymbolKind.Property => "Property", SymbolKind.Field => "Field",
|
||||
SymbolKind.Event => "Event", SymbolKind.NamedType => "Class", SymbolKind.Local => "Variable",
|
||||
SymbolKind.Parameter => "Variable", SymbolKind.Namespace => "Module", _ => "Text"
|
||||
};
|
||||
return new CompletionItem(symbol.Name, symbol.Name,
|
||||
symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), kind);
|
||||
}
|
||||
public HoverResponse Hover(HoverRequest req) => new((string?)null); // Task 7
|
||||
public SignatureHelpResponse SignatureHelp(SignatureHelpRequest req) => new(null, null, 0); // Task 7
|
||||
public FormatResponse Format(FormatRequest req) => new(req.Code); // Task 8
|
||||
|
||||
Reference in New Issue
Block a user