diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 86fbff9..128f945 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -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 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)); diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index 2798462..c516fef 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -15,6 +15,8 @@ + + diff --git a/src/SharpIDE.Godot/Features/CodeEditor/CustomSyntaxHighlighter.cs b/src/SharpIDE.Godot/Features/CodeEditor/CustomSyntaxHighlighter.cs index e878e05..8640d01 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/CustomSyntaxHighlighter.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/CustomSyntaxHighlighter.cs @@ -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"); diff --git a/src/SharpIDE.Godot/Features/CodeEditor/Resources/CascadiaFontVariation.tres b/src/SharpIDE.Godot/Features/CodeEditor/Resources/CascadiaFontVariation.tres new file mode 100644 index 0000000..c1667a9 --- /dev/null +++ b/src/SharpIDE.Godot/Features/CodeEditor/Resources/CascadiaFontVariation.tres @@ -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 diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index 8503874..5068111 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -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() diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.tscn b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.tscn index a928367..b8542b4 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.tscn +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.tscn @@ -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") diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SymbolInfoComponents.cs b/src/SharpIDE.Godot/Features/CodeEditor/SymbolInfoComponents.cs new file mode 100644 index 0000000..f25a553 --- /dev/null +++ b/src/SharpIDE.Godot/Features/CodeEditor/SymbolInfoComponents.cs @@ -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("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() + .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; + } +} \ No newline at end of file diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SymbolInfoComponents.cs.uid b/src/SharpIDE.Godot/Features/CodeEditor/SymbolInfoComponents.cs.uid new file mode 100644 index 0000000..9487be6 --- /dev/null +++ b/src/SharpIDE.Godot/Features/CodeEditor/SymbolInfoComponents.cs.uid @@ -0,0 +1 @@ +uid://c48r2nff1ckv