RobotNet/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpExtensions.cs
2025-10-15 15:15:53 +07:00

170 lines
6.7 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using RobotNet.WebApp.Scripts.Monaco.Documentation;
namespace RobotNet.WebApp.Scripts.Monaco.Languages;
public static class SignatureHelpExtensions
{
public static async Task<SignatureHelpResult?> GetSignatureHelpAsync(this Document document, int line, int column)
{
var invocation = await GetInvocation(document, line - 1, column - 1);
if (invocation is null) return null;
var response = new SignatureHelp();
foreach (var comma in invocation.Separators)
{
if (comma.Span.Start > invocation.Position)
{
break;
}
response.ActiveParameter += 1;
}
var signaturesSet = new HashSet<SignatureInformation>();
var bestScore = int.MinValue;
SignatureInformation? bestScoredItem = null;
var types = invocation.ArgumentTypes;
ISymbol? throughSymbol = null;
ISymbol? throughType = null;
var methodGroup = invocation.SemanticModel.GetMemberGroup(invocation.Receiver).OfType<IMethodSymbol>();
if (invocation.Receiver is MemberAccessExpressionSyntax syntax)
{
var throughExpression = syntax.Expression;
throughSymbol = invocation.SemanticModel.GetSpeculativeSymbolInfo(invocation.Position, throughExpression, SpeculativeBindingOption.BindAsExpression).Symbol;
throughType = invocation.SemanticModel.GetSpeculativeTypeInfo(invocation.Position, throughExpression, SpeculativeBindingOption.BindAsTypeOrNamespace).Type;
var includeInstance = throughSymbol != null && throughSymbol is not ITypeSymbol ||
throughExpression is LiteralExpressionSyntax ||
throughExpression is TypeOfExpressionSyntax;
var includeStatic = throughSymbol is INamedTypeSymbol || throughType != null;
methodGroup = methodGroup.Where(m => m.IsStatic && includeStatic || !m.IsStatic && includeInstance);
}
else if (invocation.Receiver is SimpleNameSyntax && invocation.IsInStaticContext)
{
methodGroup = methodGroup.Where(m => m.IsStatic || m.MethodKind == MethodKind.LocalFunction);
}
foreach (var methodOverload in methodGroup)
{
var signature = BuildSignature(methodOverload);
signaturesSet.Add(signature);
var score = InvocationScore(methodOverload, types);
if (score > bestScore)
{
bestScore = score;
bestScoredItem = signature;
}
}
var signaturesList = signaturesSet.ToList();
response.Signatures = [.. signaturesList];
if(bestScoredItem == null)
{
response.ActiveSignature = -1;
}
else
{
response.ActiveSignature = signaturesList.IndexOf((SignatureInformation)bestScoredItem);
}
return new SignatureHelpResult()
{
Value = response,
};
}
private static async Task<InvocationContext?> GetInvocation(Document document, int line, int column)
{
var sourceText = await document.GetTextAsync();
var position = sourceText.Lines.GetPosition(new LinePosition(line, column));
var tree = await document.GetSyntaxTreeAsync();
if (tree is null) return null;
var root = await tree.GetRootAsync();
var node = root.FindToken(position).Parent;
// Walk up until we find a node that we're interested in.
while (node != null)
{
if (node is InvocationExpressionSyntax invocation && invocation.ArgumentList.Span.Contains(position))
{
var semanticModel = await document.GetSemanticModelAsync();
return semanticModel is null ? null : new InvocationContext(semanticModel, position, invocation.Expression, invocation.ArgumentList, invocation.IsInStaticContext());
}
if (node is BaseObjectCreationExpressionSyntax objectCreation && (objectCreation.ArgumentList?.Span.Contains(position) ?? false))
{
var semanticModel = await document.GetSemanticModelAsync();
return semanticModel is null ? null : new InvocationContext(semanticModel, position, objectCreation, objectCreation.ArgumentList, objectCreation.IsInStaticContext());
}
if (node is AttributeSyntax attributeSyntax && (attributeSyntax.ArgumentList?.Span.Contains(position) ?? false))
{
var semanticModel = await document.GetSemanticModelAsync();
return semanticModel is null ? null : new InvocationContext(semanticModel, position, attributeSyntax, attributeSyntax.ArgumentList, attributeSyntax.IsInStaticContext());
}
node = node.Parent;
}
return null;
}
private static int InvocationScore(IMethodSymbol symbol, IEnumerable<TypeInfo> types)
{
var parameters = symbol.Parameters;
if (parameters.Length < types.Count())
{
return int.MinValue;
}
var score = 0;
var invocationEnum = types.GetEnumerator();
var definitionEnum = parameters.GetEnumerator();
while (invocationEnum.MoveNext() && definitionEnum.MoveNext())
{
if (invocationEnum.Current.ConvertedType == null)
{
// 1 point for having a parameter
score += 1;
}
else if (SymbolEqualityComparer.Default.Equals(invocationEnum.Current.ConvertedType, definitionEnum.Current.Type))
{
// 2 points for having a parameter and being
// the same type
score += 2;
}
}
return score;
}
private static SignatureInformation BuildSignature(IMethodSymbol symbol)
{
var StructuredDocumentation = DocumentationConverter.GetStructuredDocumentation(symbol);
return new SignatureInformation
{
Documentation = new MarkdownString()
{
Value = StructuredDocumentation?.SummaryText ?? "",
},
Label = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Parameters = [..symbol.Parameters.Select(parameter => new ParameterInformation()
{
Label = parameter.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Documentation = new MarkdownString()
{
Value = StructuredDocumentation?.GetParameterText(parameter.Name) ?? string.Empty,
},
})],
ActiveParameter = null,
};
}
}