From f9235bf67a53b570f0b3e6bdacf2b3714b2792dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ko=C4=8Dvara?= Date: Thu, 11 Dec 2025 14:15:07 +0100 Subject: [PATCH 1/2] Add Roslyn lookup based on metadata --- .../Features/Analysis/RoslynAnalysis.cs | 99 +++++++++++++++++++ .../Features/CodeEditor/SharpIdeCodeEdit.cs | 2 +- .../SharpIdeCodeEdit_SymbolLookup.cs | 19 +++- 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 395bf1cb..f7633749 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Composition.Hosting; using System.Diagnostics; +using System.Reflection.Metadata.Ecma335; using System.Text; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Razor.Language; @@ -13,14 +14,18 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; 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.Rename; +using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.FileSystemGlobbing; @@ -938,6 +943,100 @@ public async Task> FindAllSymbolReferences(ISym return (symbol, linePositionSpan, semanticInfo); } + public async Task<(SharpIdeFile? metadataFile, FileLinePositionSpan? fileLinePositionSpan)> LookupMetadataDefinition(SharpIdeFile fileModel, LinePosition linePosition, CancellationToken cancellationToken = default) + { + // TODO: + // 1. We should add a cache and look in the cache first, right now we just keep generating files for the same symbol over and over + // 2. Clean after ourselves. On start of the application, we should clear (remove) all the cached files. We wanna do it on start, because closing the app + // can be inconsistent -> sudden crash, process kill, regular close request, etc + // 3. Would be cool to add option like "IsReadOnly" to the SharpIdeFile so we can force immutability of a source generated file (because it will have 0 effect if changed) + var project = GetProjectForSharpIdeFile(fileModel); + var document = project.Documents.Single(s => s.FilePath == fileModel.Path); + Guard.Against.Null(document, nameof(document)); + + var sourceText = await document.GetTextAsync(cancellationToken); + var position = sourceText.GetPosition(linePosition); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + Guard.Against.Null(semanticModel, nameof(semanticModel)); + + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken); + var (symbol, _) = GetSymbolAtPosition(semanticModel, syntaxRoot!, position); + + if (symbol is null) + return (null, null); + + var compilation = await project.GetCompilationAsync(cancellationToken) + ?? throw new NullReferenceException("Project compilation is null."); + + // Create a temporary host document for metadata generation + var tempMetadataDocId = DocumentId.CreateNewId(project.Id); + + var newSolution = project.Solution.AddDocument( + tempMetadataDocId, + name: $"__MetadataHost_{Guid.NewGuid():N}.cs", + text: "", + folders: ["Metadata"] + ); + + // Retrieve the document from the new temporary solution + var tempMetadataDoc = newSolution.GetDocument(tempMetadataDocId) + ?? throw new NullReferenceException("Temporary project document is null."); + + var metadataService = project.Services.GetRequiredService(); + + var generatedDoc = await metadataService.AddSourceToAsync( + tempMetadataDoc, + compilation, + symbol, + new SyntaxFormattingOptions { WrappingColumn = 120 }, + cancellationToken + ); + + // Write metadata file to disk so IDE can load it + // TODO: This could be maybe done only with memory buffer, although this approach also can work as a cache for current session + var tempFolder = Path.Combine(Path.GetTempPath(), "SharpIDEMetadata"); + Directory.CreateDirectory(tempFolder); + + var safeFileName = $"{generatedDoc.Name}_{Guid.NewGuid():N}.cs"; + var tempFilePath = Path.Combine(tempFolder, safeFileName); + + var generatedText = await generatedDoc.GetTextAsync(cancellationToken); + await File.WriteAllTextAsync(tempFilePath, generatedText.ToString(), cancellationToken); + + // TODO: Theoretically we could create some kind of "Metadata" solution under the hood just for this session + // Create metadata IDE file model + var newFile = new SharpIdeFile( + tempFilePath, + generatedDoc.Name, + ".cs", + // TODO: I think we have no parent for this use-case? + null!, + [] + ); + + var metadataSemanticModel = await generatedDoc.GetSemanticModelAsync(cancellationToken) + ?? throw new NullReferenceException("Generated temporary document is null."); + + var metadataSymbol = metadataSemanticModel.Compilation.GetSymbolsWithName( + symbol.Name, + SymbolFilter.Member, + cancellationToken + ).FirstOrDefault(s => s.MetadataToken == symbol.MetadataToken); + + if (metadataSymbol == null) + return (newFile, null); + + var loc = metadataSymbol.Locations.FirstOrDefault(l => l.IsInSource); + if (loc is null) + return (newFile, null); + + var lineSpan = loc.GetMappedLineSpan(); + + return (newFile, lineSpan); + } + + + 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 287cb222..7366b5c7 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -609,4 +609,4 @@ await this.InvokeAsync(() => var caretLine = GetCaretLine(); return (caretLine, caretColumn); } -} \ No newline at end of file +} diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs index 06f09cc2..45be711c 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; +using Ardalis.GuardClauses; using Godot; using Microsoft.CodeAnalysis.Text; using SharpIDE.Application.Features.Analysis; +using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Godot.Features.SymbolLookup; namespace SharpIDE.Godot.Features.CodeEditor; @@ -82,6 +84,21 @@ await this.InvokeAsync(() => // Lets jump to the definition var definitionLocation = locations[0]; var definitionLineSpan = definitionLocation.GetMappedLineSpan(); + if (string.IsNullOrWhiteSpace(definitionLineSpan.Path)) + { + // This flow happens when we are trying to reach methods non-existing in the current project (mostly because they are from library or framework) + // When this happens, we are trying to generate temporary metadata definition file with Roslyn. This works for built-in framework/language methods. + var (doc, defLinePositionSpan) = await _roslynAnalysis.LookupMetadataDefinition(_currentFile, new LinePosition((int)line, (int)column)); + + if (doc is null || defLinePositionSpan is null) + { + GD.PrintErr($"Failed to generate metadata document for symbol: {symbol.Name}"); + return; + } + + await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(doc, new SharpIdeFileLinePosition(defLinePositionSpan.Value.Span.Start.Line, defLinePositionSpan.Value.Span.Start.Character)); + return; + } var sharpIdeFile = Solution!.AllFiles.GetValueOrDefault(definitionLineSpan.Path); if (sharpIdeFile is null) { @@ -98,4 +115,4 @@ await this.InvokeAsync(() => } }); } -} \ No newline at end of file +} From c26fa453a2f54dada88f2dc81d48a4553f8d3719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ko=C4=8Dvara?= Date: Thu, 11 Dec 2025 14:35:39 +0100 Subject: [PATCH 2/2] Removed forgotten usings --- src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs | 3 --- .../Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index f7633749..6e923b67 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -1,7 +1,6 @@ using System.Collections.Immutable; using System.Composition.Hosting; using System.Diagnostics; -using System.Reflection.Metadata.Ecma335; using System.Text; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Razor.Language; @@ -18,14 +17,12 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.MSBuild; -using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; 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.Rename; -using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.FileSystemGlobbing; diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs index 45be711c..a7765de3 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs @@ -1,9 +1,7 @@ using System.Collections.Immutable; -using Ardalis.GuardClauses; using Godot; using Microsoft.CodeAnalysis.Text; using SharpIDE.Application.Features.Analysis; -using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Godot.Features.SymbolLookup; namespace SharpIDE.Godot.Features.CodeEditor;