From 3dd23d890b3b3c8b6773b974679a24578199074e Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Sun, 12 Oct 2025 23:01:03 +1000 Subject: [PATCH] tooltip hover refinement --- .../Features/Analysis/RoslynAnalysis.cs | 22 ++++----- .../Features/CodeEditor/SharpIdeCodeEdit.cs | 45 ++++++++++++++----- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 164302a..1f51594 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -14,6 +14,7 @@ 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.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using SharpIDE.Application.Features.Analysis.FixLoaders; @@ -476,14 +477,14 @@ public static class RoslynAnalysis return changedFilesWithText; } - public static async Task LookupSymbol(SharpIdeFile fileModel, LinePosition linePosition) + public static async Task<(ISymbol?, LinePositionSpan?)> LookupSymbol(SharpIdeFile fileModel, LinePosition linePosition) { await _solutionLoadedTcs.Task; - var symbol = fileModel.IsRazorFile ? await LookupSymbolInRazor(fileModel, linePosition) : await LookupSymbolInCs(fileModel, linePosition); - return symbol; + var (symbol, linePositionSpan) = fileModel.IsRazorFile ? await LookupSymbolInRazor(fileModel, linePosition) : await LookupSymbolInCs(fileModel, linePosition); + return (symbol, linePositionSpan); } - private static async Task LookupSymbolInRazor(SharpIdeFile fileModel, LinePosition linePosition, CancellationToken cancellationToken = default) + private static async Task<(ISymbol? symbol, LinePositionSpan? linePositionSpan)> LookupSymbolInRazor(SharpIdeFile fileModel, LinePosition linePosition, CancellationToken cancellationToken = default) { var sharpIdeProjectModel = ((IChildSharpIdeNode) fileModel).GetNearestProjectNode()!; var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == sharpIdeProjectModel!.FilePath); @@ -502,11 +503,11 @@ public static class RoslynAnalysis var mappedPosition = MapRazorLinePositionToGeneratedCSharpAbsolutePosition(razorCSharpDocument, razorText, linePosition); var semanticModelAsync = await generatedDocument.GetSemanticModelAsync(cancellationToken); - var symbol = GetSymbolAtPosition(semanticModelAsync!, generatedDocSyntaxRoot!, mappedPosition!.Value); - return symbol; + var (symbol, linePositionSpan) = GetSymbolAtPosition(semanticModelAsync!, generatedDocSyntaxRoot!, mappedPosition!.Value); + return (symbol, linePositionSpan); } - private static async Task LookupSymbolInCs(SharpIdeFile fileModel, LinePosition linePosition) + private static async Task<(ISymbol? symbol, LinePositionSpan? linePositionSpan)> LookupSymbolInCs(SharpIdeFile fileModel, LinePosition linePosition) { var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath); var document = project.Documents.Single(s => s.FilePath == fileModel.Path); @@ -519,18 +520,19 @@ public static class RoslynAnalysis return GetSymbolAtPosition(semanticModel, syntaxRoot!, position); } - private static ISymbol? GetSymbolAtPosition(SemanticModel semanticModel, SyntaxNode root, int position) + private static (ISymbol? symbol, LinePositionSpan? linePositionSpan) GetSymbolAtPosition(SemanticModel semanticModel, SyntaxNode root, int position) { var node = root.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; + return (null, null); } + var linePositionSpan = root.SyntaxTree.GetLineSpan(node.Span).Span; Console.WriteLine($"Symbol found: {symbol.Name} ({symbol.Kind}) - {symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}"); - return symbol; + return (symbol, linePositionSpan); } private static int? MapRazorLinePositionToGeneratedCSharpAbsolutePosition(RazorCSharpDocument razorCSharpDocument, SourceText razorText, LinePosition razorLinePosition) diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index b413d05..8d688ec 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -88,16 +88,40 @@ public partial class SharpIdeCodeEdit : CodeEdit SetSymbolLookupWordAsValid(true); } + // This method is a bit of a disaster - we create an additional invisible Window, so that the tooltip window doesn't disappear while the mouse is over the hovered symbol private async void OnSymbolHovered(string symbol, long line, long column) { if (HasFocus() is false) return; // only show if we have focus, every tab is currently listening for this event, maybe find a better way + var globalMousePosition = GetGlobalMousePosition(); // don't breakpoint before this, else your mouse position will be wrong + var lineHeight = GetLineHeight(); 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) + var (roslynSymbol, linePositionSpan) = await RoslynAnalysis.LookupSymbol(_currentFile, new LinePosition((int)line, (int)column)); + if (roslynSymbol is null || linePositionSpan is null) { return; } + + var symbolNameHoverWindow = new Window(); + symbolNameHoverWindow.WrapControls = true; + symbolNameHoverWindow.Unresizable = true; + symbolNameHoverWindow.Transparent = true; + symbolNameHoverWindow.Borderless = true; + symbolNameHoverWindow.PopupWMHint = true; + symbolNameHoverWindow.MinimizeDisabled = true; + symbolNameHoverWindow.MaximizeDisabled = true; + + var startSymbolCharRect = GetRectAtLineColumn(linePositionSpan.Value.Start.Line, linePositionSpan.Value.Start.Character + 1); + var endSymbolCharRect = GetRectAtLineColumn(linePositionSpan.Value.End.Line, linePositionSpan.Value.End.Character + 1); + symbolNameHoverWindow.Size = new Vector2I(endSymbolCharRect.End.X - startSymbolCharRect.Position.X, lineHeight); + + var globalPosition = GetGlobalPosition(); + var startSymbolCharGlobalPos = startSymbolCharRect.Position + globalPosition; + var endSymbolCharGlobalPos = endSymbolCharRect.Position + globalPosition; + + AddChild(symbolNameHoverWindow); + symbolNameHoverWindow.Position = new Vector2I((int)startSymbolCharGlobalPos.X, (int)endSymbolCharGlobalPos.Y); + symbolNameHoverWindow.Popup(); var tooltipWindow = new Window(); tooltipWindow.WrapControls = true; @@ -108,12 +132,18 @@ public partial class SharpIdeCodeEdit : CodeEdit tooltipWindow.MinimizeDisabled = true; tooltipWindow.MaximizeDisabled = true; - var timer = new Timer { WaitTime = 0.5f, OneShot = true, Autostart = false }; + var timer = new Timer { WaitTime = 0.05f, OneShot = true, Autostart = false }; tooltipWindow.AddChild(timer); - timer.Timeout += () => tooltipWindow.QueueFree(); + timer.Timeout += () => + { + tooltipWindow.QueueFree(); + symbolNameHoverWindow.QueueFree(); + }; tooltipWindow.MouseExited += () => timer.Start(); tooltipWindow.MouseEntered += () => timer.Stop(); + symbolNameHoverWindow.MouseExited += () => timer.Start(); + symbolNameHoverWindow.MouseEntered += () => timer.Stop(); var styleBox = new StyleBoxFlat { @@ -154,17 +184,12 @@ public partial class SharpIdeCodeEdit : CodeEdit panel.AddChild(symbolInfoNode); var vboxContainer = new VBoxContainer(); vboxContainer.AddThemeConstantOverride("separation", 0); - vboxContainer.AddChild(new Control { CustomMinimumSize = new Vector2I(0, GetLineHeight()) }); vboxContainer.AddChild(panel); tooltipWindow.AddChild(vboxContainer); tooltipWindow.ChildControlsChanged(); AddChild(tooltipWindow); - var globalSymbolPosition = GetRectAtLineColumn((int)line, (int)column).Position + GetGlobalPosition(); - - var globalMousePosition = GetGlobalMousePosition(); - // -1 so that the mouse is inside the popup, otherwise the mouse exit event won't occur if the cursor moves away immediately - tooltipWindow.Position = new Vector2I((int)globalMousePosition.X - 1, (int)globalSymbolPosition.Y); + tooltipWindow.Position = new Vector2I((int)globalMousePosition.X, (int)startSymbolCharGlobalPos.Y + lineHeight); tooltipWindow.Popup(); }