go to usage/declaration

This commit is contained in:
Matt Parker
2025-10-26 23:43:10 +10:00
parent e0da4e22b2
commit 1670837d21
8 changed files with 377 additions and 3 deletions

View File

@@ -10,12 +10,14 @@ using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Razor.Remote;
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.Text;
using Microsoft.Extensions.Logging;
using Roslyn.LanguageServer.Protocol;
@@ -586,7 +588,7 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
public async Task<(string updatedText, SharpIdeFileLinePosition sharpIdeFileLinePosition)> GetCompletionApplyChanges(SharpIdeFile file, CompletionItem completionItem, CancellationToken cancellationToken = default)
{
var documentId = _workspace!.CurrentSolution.GetDocumentIdsWithFilePath(file.Path).Single();
var document = _workspace.CurrentSolution.GetRequiredDocument(documentId);
var document = SolutionExtensions.GetRequiredDocument(_workspace.CurrentSolution, documentId);
var completionService = CompletionService.GetService(document) ?? throw new InvalidOperationException("Completion service is not available for the document.");
var completionChange = await completionService.GetChangeAsync(document, completionItem, commitCharacter: '.', cancellationToken: cancellationToken);
@@ -661,6 +663,36 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
return changedFilesWithText;
}
public async Task<ISymbol?> GetEnclosingSymbolForReferenceLocation(ReferenceLocation referenceLocation)
{
var semanticModel = await referenceLocation.Document.GetSemanticModelAsync();
if (semanticModel is null) return null;
var enclosingSymbol = ReferenceLocationExtensions.GetEnclosingMethodOrPropertyOrField(semanticModel, referenceLocation);
return enclosingSymbol;
}
public async Task<ImmutableArray<ReferencedSymbol>> FindAllSymbolReferences(ISymbol symbol, CancellationToken cancellationToken = default)
{
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(FindAllSymbolReferences)}");
await _solutionLoadedTcs.Task;
var solution = _workspace!.CurrentSolution;
var references = await SymbolFinder.FindReferencesAsync(symbol, solution, cancellationToken);
return references.AsImmutable();
}
public async Task<(ISymbol?, LinePositionSpan?, TokenSemanticInfo?)> LookupSymbolSemanticInfo(SharpIdeFile fileModel, LinePosition linePosition)
{
await _solutionLoadedTcs.Task;
var (symbol, linePositionSpan, semanticInfo) = fileModel switch
{
{ IsRazorFile: true } => await LookupSymbolSemanticInfoInRazor(fileModel, linePosition),
{ IsCsharpFile: true } => await LookupSymbolSemanticInfoInCs(fileModel, linePosition),
_ => (null, null, null)
};
return (symbol, linePositionSpan, semanticInfo);
}
public async Task<(ISymbol?, LinePositionSpan?)> LookupSymbol(SharpIdeFile fileModel, LinePosition linePosition)
{
await _solutionLoadedTcs.Task;
@@ -709,6 +741,46 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
return GetSymbolAtPosition(semanticModel, syntaxRoot!, position);
}
private async Task<(ISymbol? symbol, LinePositionSpan? linePositionSpan, TokenSemanticInfo? semanticInfo)> LookupSymbolSemanticInfoInCs(SharpIdeFile fileModel, LinePosition linePosition, CancellationToken cancellationToken = default)
{
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 semanticInfo = await SymbolFinder.GetSemanticInfoAtPositionAsync(semanticModel, position, document.Project.Solution.Services, cancellationToken).ConfigureAwait(false);
var (symbol, linePositionSpan) = GetSymbolAtPosition(semanticModel, syntaxRoot!, position);
return (symbol, linePositionSpan, semanticInfo);
}
private async Task<(ISymbol? symbol, LinePositionSpan? linePositionSpan, TokenSemanticInfo? semanticInfo)> LookupSymbolSemanticInfoInRazor(SharpIdeFile fileModel, LinePosition linePosition, CancellationToken cancellationToken = default)
{
var sharpIdeProjectModel = ((IChildSharpIdeNode) fileModel).GetNearestProjectNode()!;
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == sharpIdeProjectModel!.FilePath);
var additionalDocument = project.AdditionalDocuments.Single(s => s.FilePath == fileModel.Path);
var razorProjectSnapshot = _snapshotManager!.GetSnapshot(project);
var documentSnapshot = razorProjectSnapshot.GetDocument(additionalDocument);
var razorCodeDocument = await razorProjectSnapshot.GetRequiredCodeDocumentAsync(documentSnapshot, cancellationToken);
var razorCSharpDocument = razorCodeDocument.GetRequiredCSharpDocument();
var generatedDocument = await razorProjectSnapshot.GetRequiredGeneratedDocumentAsync(documentSnapshot, cancellationToken);
var generatedDocSyntaxRoot = await generatedDocument.GetSyntaxRootAsync(cancellationToken);
var razorText = await additionalDocument.GetTextAsync(cancellationToken);
var mappedPosition = MapRazorLinePositionToGeneratedCSharpAbsolutePosition(razorCSharpDocument, razorText, linePosition);
var semanticModel = await generatedDocument.GetSemanticModelAsync(cancellationToken);
var (symbol, linePositionSpan) = GetSymbolAtPosition(semanticModel!, generatedDocSyntaxRoot!, mappedPosition!.Value);
var semanticInfo = await SymbolFinder.GetSemanticInfoAtPositionAsync(semanticModel!, mappedPosition.Value, generatedDocument.Project.Solution.Services, cancellationToken).ConfigureAwait(false);
return (symbol, linePositionSpan, semanticInfo);
}
private (ISymbol? symbol, LinePositionSpan? linePositionSpan) GetSymbolAtPosition(SemanticModel semanticModel, SyntaxNode root, int position)
{
var node = root.FindToken(position).Parent!;

View File

@@ -3,6 +3,8 @@ using Godot;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Rename.ConflictEngine;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using SharpIDE.Application;
@@ -15,6 +17,7 @@ using SharpIDE.Application.Features.Run;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.Problems;
using SharpIDE.Godot.Features.SymbolLookup;
using SharpIDE.RazorAccess;
using Task = System.Threading.Tasks.Task;
using Timer = Godot.Timer;
@@ -153,9 +156,85 @@ public partial class SharpIdeCodeEdit : CodeEdit
GD.Print($"Breakpoint {(breakpointAdded ? "added" : "removed")} at line {lineForDebugger}");
}
private void OnSymbolLookup(string symbol, long line, long column)
private readonly PackedScene _symbolUsagePopupScene = ResourceLoader.Load<PackedScene>("uid://dq7ss2ha5rk44");
private void OnSymbolLookup(string symbolString, long line, long column)
{
GD.Print($"Symbol lookup requested: {symbol} at line {line}, column {column}");
GD.Print($"Symbol lookup requested: {symbolString} at line {line}, column {column}");
_ = Task.GodotRun(async () =>
{
var (symbol, linePositionSpan, semanticInfo) = await _roslynAnalysis.LookupSymbolSemanticInfo(_currentFile, new LinePosition((int)line, (int)column));
if (symbol is null) return;
//var locations = symbol.Locations;
if (semanticInfo is null) return;
if (semanticInfo.Value.DeclaredSymbol is not null)
{
GD.Print($"Symbol is declared here: {symbolString}");
// TODO: Lookup references instead
var references = await _roslynAnalysis.FindAllSymbolReferences(semanticInfo.Value.DeclaredSymbol);
if (references.Length is 1)
{
var reference = references[0];
var locations = reference.LocationsArray;
if (locations.Length is 1)
{
// Lets jump to the definition
var referenceLocation = locations[0];
var referenceLineSpan = referenceLocation.Location.GetMappedLineSpan();
var sharpIdeFile = Solution!.AllFiles.SingleOrDefault(f => f.Path == referenceLineSpan.Path);
if (sharpIdeFile is null)
{
GD.Print($"Reference file not found in solution: {referenceLineSpan.Path}");
return;
}
await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(sharpIdeFile, new SharpIdeFileLinePosition(referenceLineSpan.Span.Start.Line, referenceLineSpan.Span.Start.Character));
}
else
{
// Show popup to select which reference to go to
var scene = _symbolUsagePopupScene.Instantiate<SymbolLookupPopup>();
var locationsAndFiles = locations.Select(s =>
{
var lineSpan = s.Location.GetMappedLineSpan();
var file = Solution!.AllFiles.SingleOrDefault(f => f.Path == lineSpan.Path);
return (s, file);
}).Where(t => t.file is not null).ToImmutableArray();
scene.Locations = locations;
scene.LocationsAndFiles = locationsAndFiles!;
scene.Symbol = semanticInfo.Value.DeclaredSymbol;
await this.InvokeAsync(() =>
{
AddChild(scene);
scene.PopupCenteredClamped();
});
}
}
}
else if (semanticInfo.Value.ReferencedSymbols.Length is not 0)
{
var referencedSymbol = semanticInfo.Value.ReferencedSymbols.Single(); // Handle more than one when I run into it
var locations = referencedSymbol.Locations;
if (locations.Length is 1)
{
// Lets jump to the definition
var definitionLocation = locations[0];
var definitionLineSpan = definitionLocation.GetMappedLineSpan();
var sharpIdeFile = Solution!.AllFiles.SingleOrDefault(f => f.Path == definitionLineSpan.Path);
if (sharpIdeFile is null)
{
GD.Print($"Definition file not found in solution: {definitionLineSpan.Path}");
return;
}
await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(sharpIdeFile, new SharpIdeFileLinePosition(definitionLineSpan.Span.Start.Line, definitionLineSpan.Span.Start.Character));
}
else
{
// TODO: Show a popup to select which definition location to go to
}
}
});
}
private void OnSymbolValidate(string symbol)

View File

@@ -0,0 +1,42 @@
using System.Collections.Immutable;
using Godot;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using SharpIDE.Application.Features.SolutionDiscovery;
namespace SharpIDE.Godot.Features.SymbolLookup;
public partial class SymbolLookupPopup : PopupPanel
{
private Label _symbolNameLabel = null!;
private VBoxContainer _usagesContainer = null!;
public ImmutableArray<ReferenceLocation> Locations { get; set; }
public ImmutableArray<(ReferenceLocation location, SharpIdeFile file)> LocationsAndFiles { get; set; }
public ISymbol Symbol { get; set; } = null!;
private readonly PackedScene _symbolUsageScene = ResourceLoader.Load<PackedScene>("uid://dokm0dyac2enh");
public override void _Ready()
{
_symbolNameLabel = GetNode<Label>("%SymbolNameLabel");
_symbolNameLabel.Text = "";
_usagesContainer = GetNode<VBoxContainer>("%UsagesVBoxContainer");
_usagesContainer.GetChildren().ToList().ForEach(s => s.QueueFree());
AboutToPopup += OnAboutToPopup;
_usagesContainer.GetChildren().ToList().ForEach(s => s.QueueFree());
foreach (var (location, file) in LocationsAndFiles)
{
var resultNode = _symbolUsageScene.Instantiate<SymbolUsageComponent>();
resultNode.Location = location;
resultNode.File = file;
resultNode.ParentSearchWindow = this;
_usagesContainer.AddChild(resultNode);
}
_symbolNameLabel.Text = $"'{Symbol.Name}'";
}
private void OnAboutToPopup()
{
}
}

View File

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

View File

@@ -0,0 +1,64 @@
[gd_scene load_steps=4 format=3 uid="uid://dq7ss2ha5rk44"]
[ext_resource type="Script" uid="uid://cxxo2sex03ox5" path="res://Features/SymbolLookup/SymbolLookupPopup.cs" id="1_f5udm"]
[ext_resource type="PackedScene" uid="uid://dokm0dyac2enh" path="res://Features/SymbolLookup/SymbolUsageComponent.tscn" id="1_k5g0h"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cuaw5"]
bg_color = Color(0.1764706, 0.1764706, 0.1764706, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
shadow_color = Color(0, 0, 0, 0.11764706)
shadow_size = 4
[node name="SymbolLookupPopup" type="PopupPanel"]
oversampling_override = 1.0
size = Vector2i(505, 340)
visible = true
theme_override_styles/panel = SubResource("StyleBoxFlat_cuaw5")
script = ExtResource("1_f5udm")
[node name="MarginContainer" type="MarginContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 4.0
offset_right = -4.0
offset_bottom = -4.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 15
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 15
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 43.615)
layout_mode = 2
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Usages of"
[node name="SymbolNameLabel" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "'UseWebApi()'"
[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
follow_focus = true
[node name="UsagesVBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="SymbolUsageComponent" parent="MarginContainer/VBoxContainer/ScrollContainer/UsagesVBoxContainer" instance=ExtResource("1_k5g0h")]
layout_mode = 2

View File

@@ -0,0 +1,53 @@
using Godot;
using Microsoft.CodeAnalysis.FindSymbols;
using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.SolutionDiscovery;
namespace SharpIDE.Godot.Features.SymbolLookup;
public partial class SymbolUsageComponent : MarginContainer
{
private Label _enclosingSymbolLabel = null!;
private Label _fileNameLabel = null!;
private Label _lineNumberLabel = null!;
private Button _button = null!;
public SymbolLookupPopup ParentSearchWindow { get; set; } = null!;
public ReferenceLocation? Location { get; set; }
public SharpIdeFile File { get; set; } = null!;
[Inject] private readonly RoslynAnalysis _roslynAnalysis = null!;
public override void _Ready()
{
_button = GetNode<Button>("Button");
_enclosingSymbolLabel = GetNode<Label>("%EnclosingSymbolLabel");
_fileNameLabel = GetNode<Label>("%FileNameLabel");
_lineNumberLabel = GetNode<Label>("%LineNumberLabel");
SetValue(Location);
_button.Pressed += OnButtonPressed;
}
private void OnButtonPressed()
{
var mappedLineSpan = Location!.Value.Location.GetMappedLineSpan();
var fileLinePosition = new SharpIdeFileLinePosition { Line = mappedLineSpan.StartLinePosition.Line, Column = mappedLineSpan.StartLinePosition.Character };
GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(File, fileLinePosition);
ParentSearchWindow.Hide();
}
private void SetValue(ReferenceLocation? result)
{
if (result is null) return;
var mappedLineSpan = result.Value.Location.GetMappedLineSpan();
_fileNameLabel.Text = File.Name;
_lineNumberLabel.Text = (mappedLineSpan.StartLinePosition.Line + 1).ToString();
_ = Task.GodotRun(async () =>
{
var enclosingSymbol = await _roslynAnalysis.GetEnclosingSymbolForReferenceLocation(result.Value);
await this.InvokeAsync(() => _enclosingSymbolLabel.Text = enclosingSymbol?.Name);
});
}
}

View File

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

View File

@@ -0,0 +1,62 @@
[gd_scene load_steps=4 format=3 uid="uid://dokm0dyac2enh"]
[ext_resource type="Script" uid="uid://ct037eoc26o04" path="res://Features/SymbolLookup/SymbolUsageComponent.cs" id="1_f7dno"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6ov2c"]
draw_center = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dtmd4"]
bg_color = Color(0.18039216, 0.2627451, 0.43137255, 1)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[node name="SymbolUsageComponent" type="MarginContainer"]
anchors_preset = 14
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_top = -4.0
offset_bottom = 4.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_f7dno")
[node name="Button" type="Button" parent="."]
custom_minimum_size = Vector2(0, 26)
layout_mode = 2
theme_override_styles/normal = SubResource("StyleBoxFlat_6ov2c")
theme_override_styles/focus = SubResource("StyleBoxFlat_dtmd4")
[node name="MarginContainer" type="MarginContainer" parent="Button"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_right = 5
[node name="HBoxContainer" type="HBoxContainer" parent="Button/MarginContainer"]
layout_mode = 2
[node name="EnclosingSymbolLabel" type="Label" parent="Button/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Containing Symbol Name"
text_overrun_behavior = 3
[node name="FileNameLabel" type="Label" parent="Button/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 0.7411765)
text = "FileName.cs"
[node name="LineNumberLabel" type="Label" parent="Button/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 0.7411765)
text = "24"