diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 8ecfb12..21c6408 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -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 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 logger, BuildService buildSe return changedFilesWithText; } + public async Task 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> 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 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!; diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index ee568eb..54be094 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -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("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(); + 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) diff --git a/src/SharpIDE.Godot/Features/SymbolLookup/SymbolLookupPopup.cs b/src/SharpIDE.Godot/Features/SymbolLookup/SymbolLookupPopup.cs new file mode 100644 index 0000000..a88e2c7 --- /dev/null +++ b/src/SharpIDE.Godot/Features/SymbolLookup/SymbolLookupPopup.cs @@ -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 Locations { get; set; } + public ImmutableArray<(ReferenceLocation location, SharpIdeFile file)> LocationsAndFiles { get; set; } + public ISymbol Symbol { get; set; } = null!; + private readonly PackedScene _symbolUsageScene = ResourceLoader.Load("uid://dokm0dyac2enh"); + + public override void _Ready() + { + _symbolNameLabel = GetNode