hover method symbol v1

This commit is contained in:
Matt Parker
2025-10-11 12:46:50 +10:00
parent fff3f1c544
commit 9e968fbf60
8 changed files with 380 additions and 17 deletions

View File

@@ -14,8 +14,8 @@ using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using NuGet.Packaging;
using SharpIDE.Application.Features.Analysis.FixLoaders;
using SharpIDE.Application.Features.Analysis.Razor;
using SharpIDE.Application.Features.SolutionDiscovery;
@@ -476,6 +476,37 @@ public static class RoslynAnalysis
return changedFilesWithText;
}
public static async Task<ISymbol?> LookupSymbol(SharpIdeFile fileModel, LinePosition linePosition)
{
await _solutionLoadedTcs.Task;
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath);
var document = project.Documents.Single(s => s.FilePath == fileModel.Path);
Guard.Against.Null(document, nameof(document));
var sourceText = await document.GetTextAsync();
var position = sourceText.GetPosition(linePosition);
var semanticModel = await document.GetSemanticModelAsync();
Guard.Against.Null(semanticModel, nameof(semanticModel));
var syntaxRoot = await document.GetSyntaxRootAsync();
var node = syntaxRoot!.FindToken(position).Parent!;
var symbol = semanticModel.GetSymbolInfo(node).Symbol ?? semanticModel.GetDeclaredSymbol(node);
if (symbol is null)
{
Console.WriteLine("No symbol found at position");
return null;
}
var documentationCommentXml = symbol.GetDocumentationCommentXml();
if (documentationCommentXml is not null)
{
var comment = DocumentationComment.FromXmlFragment(documentationCommentXml);
;
}
Console.WriteLine($"Symbol found: {symbol.Name} ({symbol.Kind}) - {symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}");
return symbol;
}
public static void UpdateDocument(SharpIdeFile fileModel, string newContent)
{
Guard.Against.Null(fileModel, nameof(fileModel));

View File

@@ -15,6 +15,8 @@
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.LanguageServer.Protocol" />
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.ExternalAccess.Razor.Features" />
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Razor.SemanticTokens" />
<IgnoresAccessChecksTo Include="Microsoft.CodeAnalysis.Workspaces" />
<IgnoresAccessChecksToExcludeTypeName Include="System.Linq.RoslynEnumerableExtensions" />
</ItemGroup>
<ItemGroup>

View File

@@ -146,9 +146,9 @@ public partial class CustomHighlighter : SyntaxHighlighter
"number" => CachedColors.NumberGreen,
// Types (User Types)
"class name" => CachedColors.ClassBlue,
"record class name" => CachedColors.ClassBlue,
"struct name" => CachedColors.ClassBlue,
"class name" => CachedColors.ClassGreen,
"record class name" => CachedColors.ClassGreen,
"struct name" => CachedColors.ClassGreen,
"interface name" => CachedColors.InterfaceGreen,
"enum name" => CachedColors.InterfaceGreen,
"namespace name" => CachedColors.White,
@@ -188,7 +188,7 @@ public partial class CustomHighlighter : SyntaxHighlighter
}
}
file static class CachedColors
public static class CachedColors
{
public static readonly Color Orange = new("f27718");
public static readonly Color White = new("dcdcdc");
@@ -198,7 +198,7 @@ file static class CachedColors
public static readonly Color LightOrangeBrown = new("d69d85");
public static readonly Color NumberGreen = new("b5cea8");
public static readonly Color InterfaceGreen = new("b8d7a3");
public static readonly Color ClassBlue = new("4ec9b0");
public static readonly Color ClassGreen = new("4ec9b0");
public static readonly Color VariableBlue = new("9cdcfe");
public static readonly Color Gray = new("a9a9a9");

View File

@@ -0,0 +1,9 @@
[gd_resource type="FontVariation" load_steps=2 format=3 uid="uid://cctwlwcoycek7"]
[ext_resource type="FontFile" uid="uid://7jc0nj310cu6" path="res://Features/CodeEditor/Resources/CascadiaCode.ttf" id="1_ba3y1"]
[resource]
base_font = ExtResource("1_ba3y1")
spacing_top = 3
spacing_bottom = 2
baseline_offset = 0.05

View File

@@ -82,12 +82,62 @@ public partial class SharpIdeCodeEdit : CodeEdit
private void OnSymbolValidate(string symbol)
{
GD.Print($"Symbol validating: {symbol}");
//var valid = symbol.Contains(' ') is false;
//SetSymbolLookupWordAsValid(valid);
SetSymbolLookupWordAsValid(true);
}
private void OnSymbolHovered(string symbol, long line, long column)
private async void OnSymbolHovered(string symbol, long line, long column)
{
GD.Print($"Symbol hovered: {symbol}");
if (HasFocus() is false) return; // only show if we have focus, every tab is currently listening for this event, maybe find a better way
GD.Print($"Symbol hovered: {symbol} at line {line}, column {column}");
var roslynSymbol = await RoslynAnalysis.LookupSymbol(_currentFile, new LinePosition((int)line, (int)column));
if (roslynSymbol is null)
{
return;
}
var popupPanel = new PopupPanel();
popupPanel.Unfocusable = true; // may need to change eventually for navigating to other symbols
popupPanel.MouseExited += () => popupPanel.QueueFree();
// set background color
var styleBox = new StyleBoxFlat
{
BgColor = new Color("2b2d30"),
BorderColor = new Color("3e4045"),
BorderWidthTop = 1,
BorderWidthBottom = 1,
BorderWidthLeft = 1,
BorderWidthRight = 1,
CornerRadiusBottomLeft = 4,
CornerRadiusBottomRight = 4,
CornerRadiusTopLeft = 4,
CornerRadiusTopRight = 4,
ShadowSize = 2,
ShadowColor = new Color(0, 0, 0, 0.5f),
ContentMarginTop = 10,
ContentMarginBottom = 10,
ContentMarginLeft = 10,
ContentMarginRight = 10
};
popupPanel.AddThemeStyleboxOverride("panel", styleBox);
var symbolInfoNode = roslynSymbol switch
{
IMethodSymbol methodSymbol => SymbolInfoComponents.GetMethodSymbolInfo(methodSymbol),
_ => new Control()
};
popupPanel.AddChild(symbolInfoNode);
AddChild(popupPanel);
var globalSymbolPosition = GetRectAtLineColumn((int)line, (int)column).Position + GetGlobalPosition();
globalSymbolPosition.Y += GetLineHeight();
var globalMousePosition = GetGlobalMousePosition();
popupPanel.Position = new Vector2I((int)globalMousePosition.X, (int)globalSymbolPosition.Y);
popupPanel.Popup();
}
private void OnCaretChanged()

View File

@@ -1,14 +1,8 @@
[gd_scene load_steps=6 format=3 uid="uid://cinaqbdghcvoi"]
[gd_scene load_steps=5 format=3 uid="uid://cinaqbdghcvoi"]
[ext_resource type="FontFile" uid="uid://7jc0nj310cu6" path="res://Features/CodeEditor/Resources/CascadiaCode.ttf" id="1_s7ira"]
[ext_resource type="FontVariation" uid="uid://cctwlwcoycek7" path="res://Features/CodeEditor/Resources/CascadiaFontVariation.tres" id="1_s7ira"]
[ext_resource type="Script" uid="uid://du2lt7r1p1qfy" path="res://Features/CodeEditor/SharpIdeCodeEdit.cs" id="2_kp2fd"]
[sub_resource type="FontVariation" id="FontVariation_y3aoi"]
base_font = ExtResource("1_s7ira")
spacing_top = 3
spacing_bottom = 2
baseline_offset = 0.05
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v06ln"]
content_margin_left = 4.0
content_margin_top = 4.0
@@ -26,7 +20,7 @@ corner_detail = 5
[node name="SharpIdeCodeEdit" type="CodeEdit"]
theme_override_colors/current_line_color = Color(0.0588235, 0.0588235, 0.0588235, 1)
theme_override_fonts/font = SubResource("FontVariation_y3aoi")
theme_override_fonts/font = ExtResource("1_s7ira")
theme_override_font_sizes/font_size = 18
theme_override_styles/normal = SubResource("StyleBoxFlat_v06ln")
theme_override_styles/focus = SubResource("StyleBoxEmpty_7ptyn")

View File

@@ -0,0 +1,276 @@
using Godot;
using Microsoft.CodeAnalysis;
namespace SharpIDE.Godot.Features.CodeEditor;
public static class SymbolInfoComponents
{
private static readonly FontVariation MonospaceFont = ResourceLoader.Load<FontVariation>("uid://cctwlwcoycek7");
public static RichTextLabel GetMethodSymbolInfo(IMethodSymbol methodSymbol)
{
var label = new RichTextLabel();
label.FitContent = true;
label.AutowrapMode = TextServer.AutowrapMode.Off;
label.SetAnchorsPreset(Control.LayoutPreset.FullRect);
label.PushFont(MonospaceFont);
label.PushColor(CachedColors.KeywordBlue);
label.AddText(methodSymbol.DeclaredAccessibility.GetAccessibilityString());
label.Pop();
label.AddText(" ");
label.AddStaticModifier(methodSymbol);
label.AddText(" ");
label.AddMethodReturnType(methodSymbol);
label.AddText(" ");
label.AddMethodName(methodSymbol);
label.AddTypeParameters(methodSymbol);
label.AddText("(");
label.AddParameters(methodSymbol);
label.AddText(")");
label.Newline();
label.AddText("in class ");
label.AddContainingNamespaceAndClass(methodSymbol);
label.Newline(); // TODO: Make this only 1.5 lines high
label.Newline(); //
label.AddTypeParameterArguments(methodSymbol);
label.AddHr(100, 1, CachedColors.Gray);
label.Newline();
label.Pop(); // font
label.AddText("docs");
return label;
}
private static string GetAccessibilityString(this Accessibility accessibility) => accessibility switch
{
Accessibility.Public => "public",
Accessibility.Private => "private",
Accessibility.Protected => "protected",
Accessibility.Internal => "internal",
Accessibility.ProtectedOrInternal => "protected internal",
Accessibility.ProtectedAndInternal => "private protected",
_ => "unknown"
};
private static void AddStaticModifier(this RichTextLabel label, IMethodSymbol methodSymbol)
{
if (methodSymbol.IsStatic || methodSymbol.ReducedFrom?.IsStatic is true)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText("static");
label.Pop();
}
}
private static void AddMethodReturnType(this RichTextLabel label, IMethodSymbol methodSymbol)
{
if (methodSymbol.ReturnsVoid)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText("void");
label.Pop();
return;
}
label.PushColor(CachedColors.ClassGreen);
label.AddText(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
label.Pop();
}
private static void AddMethodName(this RichTextLabel label, IMethodSymbol methodSymbol)
{
label.PushColor(CachedColors.Yellow);
label.AddText(methodSymbol.Name);
label.Pop();
}
private static void AddTypeParameters(this RichTextLabel label, IMethodSymbol methodSymbol)
{
if (methodSymbol.TypeParameters.Length == 0) return;
label.PushColor(CachedColors.White);
label.AddText("<");
label.Pop();
foreach (var (index, typeParameter) in methodSymbol.TypeParameters.Index())
{
label.PushColor(CachedColors.ClassGreen);
label.AddText(typeParameter.Name);
label.Pop();
if (index < methodSymbol.TypeParameters.Length - 1)
{
label.AddText(", ");
}
}
label.PushColor(CachedColors.White);
label.AddText(">");
label.Pop();
}
private static void AddParameters(this RichTextLabel label, IMethodSymbol methodSymbol)
{
if (methodSymbol.IsExtensionMethod)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText("this");
label.Pop();
label.AddText(" ");
}
foreach (var (index, parameterSymbol) in methodSymbol.Parameters.Index())
{
var attributes = parameterSymbol.GetAttributes();
if (attributes.Length is not 0)
{
foreach (var (attrIndex, attribute) in attributes.Index())
{
label.AddText("[");
label.PushColor(CachedColors.ClassGreen);
var displayString = attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
if (displayString?.EndsWith("Attribute") is true) displayString = displayString[..^9]; // remove last 9 chars
label.AddText(displayString ?? "unknown");
label.Pop();
label.AddText("]");
label.AddText(" ");
}
}
if (parameterSymbol.RefKind != RefKind.None) // ref, in, out
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText(parameterSymbol.RefKind.ToString().ToLower());
label.Pop();
label.AddText(" ");
}
else if (parameterSymbol.IsParams)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText("params");
label.Pop();
label.AddText(" ");
}
label.PushColor(parameterSymbol.Type.GetSymbolColourByType());
label.AddText(parameterSymbol.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
label.Pop();
label.AddText(" ");
label.PushColor(CachedColors.VariableBlue);
label.AddText(parameterSymbol.Name);
label.Pop();
// default value
if (parameterSymbol.HasExplicitDefaultValue)
{
label.AddText(" = ");
if (parameterSymbol.ExplicitDefaultValue is null)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText("null");
label.Pop();
}
else if (parameterSymbol.Type.TypeKind == TypeKind.Enum)
{
var explicitDefaultValue = parameterSymbol.ExplicitDefaultValue;
// Find the enum field with the same constant value
var enumMember = parameterSymbol.Type.GetMembers()
.OfType<IFieldSymbol>()
.FirstOrDefault(f => f.HasConstantValue && Equals(f.ConstantValue, explicitDefaultValue));
if (enumMember != null)
{
label.PushColor(CachedColors.InterfaceGreen);
label.AddText(parameterSymbol.Type.Name);
label.Pop();
label.PushColor(CachedColors.White);
label.AddText(".");
label.Pop();
label.PushColor(CachedColors.White);
label.AddText(enumMember.Name);
label.Pop();
}
else
{
label.PushColor(CachedColors.InterfaceGreen);
label.AddText(parameterSymbol.Type.Name);
label.Pop();
label.AddText($"({explicitDefaultValue})");
}
}
else if (parameterSymbol.ExplicitDefaultValue is string str)
{
label.PushColor(CachedColors.LightOrangeBrown);
label.AddText($"""
"{str}"
""");
label.Pop();
}
else if (parameterSymbol.ExplicitDefaultValue is bool b)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText(b ? "true" : "false");
label.Pop();
}
else
{
label.AddText(parameterSymbol.ExplicitDefaultValue.ToString() ?? "unknown");
}
}
if (index < methodSymbol.Parameters.Length - 1)
{
label.AddText(", ");
}
}
}
private static void AddContainingNamespaceAndClass(this RichTextLabel label, IMethodSymbol methodSymbol)
{
if (methodSymbol.ContainingNamespace is null || methodSymbol.ContainingNamespace.IsGlobalNamespace) return;
var namespaces = methodSymbol.ContainingNamespace.ToDisplayString().Split('.');
foreach (var ns in namespaces)
{
label.PushColor(CachedColors.KeywordBlue);
label.AddText(ns);
label.Pop();
label.AddText(".");
}
label.PushColor(CachedColors.ClassGreen);
label.AddText(methodSymbol.ContainingType.Name);
label.Pop();
}
private static void AddTypeParameterArguments(this RichTextLabel label, IMethodSymbol methodSymbol)
{
if (methodSymbol.TypeArguments.Length == 0) return;
var typeParameters = methodSymbol.TypeParameters;
var typeArguments = methodSymbol.TypeArguments;
if (typeParameters.Length != typeArguments.Length) throw new Exception("Type parameters and type arguments length mismatch.");
foreach (var (index, (typeArgument, typeParameter)) in methodSymbol.TypeArguments.Zip(typeParameters).Index())
{
label.PushColor(CachedColors.ClassGreen);
label.AddText(typeParameter.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
label.Pop();
label.AddText(" is ");
label.PushColor(typeArgument.GetSymbolColourByType());
label.AddText(typeArgument.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
label.Pop();
if (index < methodSymbol.TypeArguments.Length - 1)
{
label.Newline();
}
}
}
// TODO: handle arrays etc, where there are multiple colours in one type
private static Color GetSymbolColourByType(this ITypeSymbol symbol)
{
Color colour = symbol switch
{
{SpecialType: not SpecialType.None} => CachedColors.KeywordBlue,
INamedTypeSymbol namedTypeSymbol => namedTypeSymbol.TypeKind switch
{
TypeKind.Class => CachedColors.ClassGreen,
TypeKind.Interface => CachedColors.InterfaceGreen,
TypeKind.Struct => CachedColors.ClassGreen,
TypeKind.Enum => CachedColors.InterfaceGreen,
TypeKind.Delegate => CachedColors.ClassGreen,
_ => CachedColors.Orange
},
_ => CachedColors.Orange
};
return colour;
}
}

View File

@@ -0,0 +1 @@
uid://c48r2nff1ckv