feat(adminui): script hover + signature help
This commit is contained in:
@@ -227,8 +227,104 @@ public sealed class ScriptAnalysisService
|
||||
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 HoverResponse Hover(HoverRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.CodeText)) return new HoverResponse(null);
|
||||
try
|
||||
{
|
||||
var code = Normalize(req.CodeText);
|
||||
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));
|
||||
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
|
||||
// member-access (e.g. hovering the `GetTag` identifier — the IdentifierName itself
|
||||
// carries no symbol info, but its parent member-access binds to the method).
|
||||
var symbol = ResolveSymbol(model, node);
|
||||
if (symbol is null) return new HoverResponse(null);
|
||||
var display = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
var summary = TryGetXmlSummary(symbol);
|
||||
var md = "```csharp\n" + display + "\n```" + (summary is null ? "" : "\n\n" + summary);
|
||||
return new HoverResponse(md);
|
||||
}
|
||||
catch (Exception ex) { _logger?.LogWarning(ex, "Script hover failed."); return new HoverResponse(null); }
|
||||
}
|
||||
|
||||
private static ISymbol? ResolveSymbol(SemanticModel model, SyntaxNode node)
|
||||
{
|
||||
foreach (var candidate in ResolutionCandidates(node))
|
||||
{
|
||||
var info = model.GetSymbolInfo(candidate);
|
||||
var symbol = info.Symbol ?? info.CandidateSymbols.FirstOrDefault();
|
||||
if (symbol is not null) return symbol;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// The node under the caret, then — only if it is the name of a member access — that member access,
|
||||
// and then its enclosing invocation. Bounded so a hover never resolves an unrelated ancestor symbol.
|
||||
private static IEnumerable<SyntaxNode> ResolutionCandidates(SyntaxNode node)
|
||||
{
|
||||
yield return node;
|
||||
if (node is IdentifierNameSyntax
|
||||
&& node.Parent is MemberAccessExpressionSyntax ma
|
||||
&& ma.Name == node)
|
||||
{
|
||||
yield return ma;
|
||||
if (ma.Parent is InvocationExpressionSyntax inv && inv.Expression == ma)
|
||||
yield return inv;
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort: returns the <summary> text if the symbol's XML doc is available, else null.
|
||||
private static string? TryGetXmlSummary(ISymbol symbol)
|
||||
{
|
||||
var xml = symbol.GetDocumentationCommentXml();
|
||||
if (string.IsNullOrWhiteSpace(xml)) return null;
|
||||
try
|
||||
{
|
||||
var doc = System.Xml.Linq.XDocument.Parse(xml);
|
||||
var summary = doc.Descendants("summary").FirstOrDefault()?.Value.Trim();
|
||||
return string.IsNullOrWhiteSpace(summary) ? null : summary;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
public SignatureHelpResponse SignatureHelp(SignatureHelpRequest req)
|
||||
{
|
||||
var empty = new SignatureHelpResponse(null, null, 0);
|
||||
if (string.IsNullOrWhiteSpace(req.CodeText)) return empty;
|
||||
try
|
||||
{
|
||||
var code = Normalize(req.CodeText);
|
||||
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));
|
||||
|
||||
InvocationExpressionSyntax? inv = null;
|
||||
for (var n = token.Parent; n is not null; n = n.Parent)
|
||||
if (n is InvocationExpressionSyntax found) { inv = found; break; }
|
||||
if (inv is null) return empty;
|
||||
|
||||
var info = model.GetSymbolInfo(inv);
|
||||
var method = (info.Symbol ?? info.CandidateSymbols.FirstOrDefault()) as IMethodSymbol;
|
||||
if (method is null) return empty;
|
||||
|
||||
var ps = method.Parameters
|
||||
.Select(p => new SignatureHelpParameter(
|
||||
$"{p.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)} {p.Name}", null))
|
||||
.ToList();
|
||||
var label = $"{method.Name}(" + string.Join(", ", ps.Select(p => p.Label)) + ")";
|
||||
|
||||
int active = 0;
|
||||
foreach (var a in inv.ArgumentList.Arguments) { if (a.Span.End < position) active++; else break; }
|
||||
active = Math.Clamp(active, 0, Math.Max(0, ps.Count - 1));
|
||||
|
||||
return new SignatureHelpResponse(label, ps, active);
|
||||
}
|
||||
catch (Exception ex) { _logger?.LogWarning(ex, "Script signature help failed."); return empty; }
|
||||
}
|
||||
public FormatResponse Format(FormatRequest req) => new(req.Code); // Task 8
|
||||
public InlayHintsResponse InlayHints(InlayHintsRequest req) => new(Array.Empty<InlayHint>()); // Task 8 (stays empty)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user