tooltip hover refinement

This commit is contained in:
Matt Parker
2025-10-12 23:01:03 +10:00
parent 7a290d4254
commit 3dd23d890b
2 changed files with 47 additions and 20 deletions

View File

@@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Razor.SemanticTokens; using Microsoft.CodeAnalysis.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens; using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
using SharpIDE.Application.Features.Analysis.FixLoaders; using SharpIDE.Application.Features.Analysis.FixLoaders;
@@ -476,14 +477,14 @@ public static class RoslynAnalysis
return changedFilesWithText; return changedFilesWithText;
} }
public static async Task<ISymbol?> LookupSymbol(SharpIdeFile fileModel, LinePosition linePosition) public static async Task<(ISymbol?, LinePositionSpan?)> LookupSymbol(SharpIdeFile fileModel, LinePosition linePosition)
{ {
await _solutionLoadedTcs.Task; await _solutionLoadedTcs.Task;
var symbol = fileModel.IsRazorFile ? await LookupSymbolInRazor(fileModel, linePosition) : await LookupSymbolInCs(fileModel, linePosition); var (symbol, linePositionSpan) = fileModel.IsRazorFile ? await LookupSymbolInRazor(fileModel, linePosition) : await LookupSymbolInCs(fileModel, linePosition);
return symbol; return (symbol, linePositionSpan);
} }
private static async Task<ISymbol?> 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 sharpIdeProjectModel = ((IChildSharpIdeNode) fileModel).GetNearestProjectNode()!;
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == sharpIdeProjectModel!.FilePath); 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 mappedPosition = MapRazorLinePositionToGeneratedCSharpAbsolutePosition(razorCSharpDocument, razorText, linePosition);
var semanticModelAsync = await generatedDocument.GetSemanticModelAsync(cancellationToken); var semanticModelAsync = await generatedDocument.GetSemanticModelAsync(cancellationToken);
var symbol = GetSymbolAtPosition(semanticModelAsync!, generatedDocSyntaxRoot!, mappedPosition!.Value); var (symbol, linePositionSpan) = GetSymbolAtPosition(semanticModelAsync!, generatedDocSyntaxRoot!, mappedPosition!.Value);
return symbol; return (symbol, linePositionSpan);
} }
private static async Task<ISymbol?> 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 project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath);
var document = project.Documents.Single(s => s.FilePath == fileModel.Path); var document = project.Documents.Single(s => s.FilePath == fileModel.Path);
@@ -519,18 +520,19 @@ public static class RoslynAnalysis
return GetSymbolAtPosition(semanticModel, syntaxRoot!, position); 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 node = root.FindToken(position).Parent!;
var symbol = semanticModel.GetSymbolInfo(node).Symbol ?? semanticModel.GetDeclaredSymbol(node); var symbol = semanticModel.GetSymbolInfo(node).Symbol ?? semanticModel.GetDeclaredSymbol(node);
if (symbol is null) if (symbol is null)
{ {
Console.WriteLine("No symbol found at position"); 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)}"); 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) private static int? MapRazorLinePositionToGeneratedCSharpAbsolutePosition(RazorCSharpDocument razorCSharpDocument, SourceText razorText, LinePosition razorLinePosition)

View File

@@ -88,17 +88,41 @@ public partial class SharpIdeCodeEdit : CodeEdit
SetSymbolLookupWordAsValid(true); 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) 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 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}"); GD.Print($"Symbol hovered: {symbol} at line {line}, column {column}");
var roslynSymbol = await RoslynAnalysis.LookupSymbol(_currentFile, new LinePosition((int)line, (int)column)); var (roslynSymbol, linePositionSpan) = await RoslynAnalysis.LookupSymbol(_currentFile, new LinePosition((int)line, (int)column));
if (roslynSymbol is null) if (roslynSymbol is null || linePositionSpan is null)
{ {
return; 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(); var tooltipWindow = new Window();
tooltipWindow.WrapControls = true; tooltipWindow.WrapControls = true;
tooltipWindow.Unresizable = true; tooltipWindow.Unresizable = true;
@@ -108,12 +132,18 @@ public partial class SharpIdeCodeEdit : CodeEdit
tooltipWindow.MinimizeDisabled = true; tooltipWindow.MinimizeDisabled = true;
tooltipWindow.MaximizeDisabled = 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); tooltipWindow.AddChild(timer);
timer.Timeout += () => tooltipWindow.QueueFree(); timer.Timeout += () =>
{
tooltipWindow.QueueFree();
symbolNameHoverWindow.QueueFree();
};
tooltipWindow.MouseExited += () => timer.Start(); tooltipWindow.MouseExited += () => timer.Start();
tooltipWindow.MouseEntered += () => timer.Stop(); tooltipWindow.MouseEntered += () => timer.Stop();
symbolNameHoverWindow.MouseExited += () => timer.Start();
symbolNameHoverWindow.MouseEntered += () => timer.Stop();
var styleBox = new StyleBoxFlat var styleBox = new StyleBoxFlat
{ {
@@ -154,17 +184,12 @@ public partial class SharpIdeCodeEdit : CodeEdit
panel.AddChild(symbolInfoNode); panel.AddChild(symbolInfoNode);
var vboxContainer = new VBoxContainer(); var vboxContainer = new VBoxContainer();
vboxContainer.AddThemeConstantOverride("separation", 0); vboxContainer.AddThemeConstantOverride("separation", 0);
vboxContainer.AddChild(new Control { CustomMinimumSize = new Vector2I(0, GetLineHeight()) });
vboxContainer.AddChild(panel); vboxContainer.AddChild(panel);
tooltipWindow.AddChild(vboxContainer); tooltipWindow.AddChild(vboxContainer);
tooltipWindow.ChildControlsChanged(); tooltipWindow.ChildControlsChanged();
AddChild(tooltipWindow); AddChild(tooltipWindow);
var globalSymbolPosition = GetRectAtLineColumn((int)line, (int)column).Position + GetGlobalPosition(); tooltipWindow.Position = new Vector2I((int)globalMousePosition.X, (int)startSymbolCharGlobalPos.Y + lineHeight);
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.Popup(); tooltipWindow.Popup();
} }