diff --git a/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs b/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs index f12d364f73733..497a10596e097 100644 --- a/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs +++ b/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs @@ -47,17 +47,17 @@ public void ReportIncomplete() _searchCallback.ReportIncomplete(IncompleteReason.Parsing); } - public Task AddResultsAsync( + public async Task AddResultsAsync( ImmutableArray results, Document? activeDocument, CancellationToken cancellationToken) { // Convert roslyn pattern matches to the platform type. foreach (var result in results) { var matches = result.Matches.SelectAsArray(static m => new PatternMatch( - ConvertKind(m.Kind), - punctuationStripped: false, - m.IsCaseSensitive, - m.MatchedSpans.SelectAsArray(static s => s.ToSpan()))); + ConvertKind(m.Kind), + punctuationStripped: false, + m.IsCaseSensitive, + m.MatchedSpans.SelectAsArray(static s => s.ToSpan()))); // Weight the items based on the overall pattern matching weights. We want the items that have the best // pattern matches (low .Kind values) to have the highest float values (as higher is better for the VS @@ -72,13 +72,24 @@ public Task AddResultsAsync( result.Name, result.SecondarySort, matches, - result.NavigableItem.Document.FilePath, + await GetFilePathAsync(result).ConfigureAwait(false), perProviderItemPriority, project.Language, isActiveDocument: activeDocument != null && activeDocument.Id == result.NavigableItem.Document.Id)); } - return Task.CompletedTask; + async ValueTask GetFilePathAsync(INavigateToSearchResult result) + { + var document = result.NavigableItem.Document; + if (document.Id.IsSourceGenerated) + { + var foundDocument = await document.GetRequiredDocumentAsync(_solution, cancellationToken).ConfigureAwait(false); + if (foundDocument is SourceGeneratedDocument sourceGeneratedDocument) + return _provider._sourceGeneratedFileManager.MapSourceGeneratedDocumentToOpenableFilePath(sourceGeneratedDocument); + } + + return document.FilePath; + } } private static PatternMatchKind ConvertKind(PatternMatching.PatternMatchKind kind) diff --git a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSourceProvider.cs b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSourceProvider.cs index 227b5d168777e..5140d6119d602 100644 --- a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSourceProvider.cs +++ b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSourceProvider.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.LanguageServices.Implementation; using Microsoft.VisualStudio.Search.Data; using Microsoft.VisualStudio.Utilities; @@ -35,6 +36,7 @@ namespace Microsoft.CodeAnalysis.NavigateTo; internal sealed partial class RoslynSearchItemsSourceProvider : ISearchItemsSourceProvider { private readonly VisualStudioWorkspace _workspace; + private readonly SourceGeneratedFileManager _sourceGeneratedFileManager; private readonly IThreadingContext _threadingContext; private readonly IUIThreadOperationExecutor _threadOperationExecutor; private readonly IAsynchronousOperationListener _asyncListener; @@ -44,11 +46,13 @@ internal sealed partial class RoslynSearchItemsSourceProvider : ISearchItemsSour [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public RoslynSearchItemsSourceProvider( VisualStudioWorkspace workspace, + SourceGeneratedFileManager sourceGeneratedFileManager, IThreadingContext threadingContext, IUIThreadOperationExecutor threadOperationExecutor, IAsynchronousOperationListenerProvider listenerProvider) { _workspace = workspace; + _sourceGeneratedFileManager = sourceGeneratedFileManager; _threadingContext = threadingContext; _threadOperationExecutor = threadOperationExecutor; _asyncListener = listenerProvider.GetListener(FeatureAttribute.NavigateTo); diff --git a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs index 7049f12b968ef..93cb5c7e78da9 100644 --- a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs +++ b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs @@ -19,14 +19,9 @@ internal sealed partial class RoslynSearchItemsSourceProvider /// Implementation of the . Responsible for actually producing both the /// item presented in the search results list, and the async preview for that item. /// - private sealed class RoslynSearchResultViewFactory : ISearchResultViewFactory + private sealed class RoslynSearchResultViewFactory(RoslynSearchItemsSourceProvider provider) : ISearchResultViewFactory { - private readonly RoslynSearchItemsSourceProvider _provider; - - public RoslynSearchResultViewFactory(RoslynSearchItemsSourceProvider provider) - { - _provider = provider; - } + private readonly RoslynSearchItemsSourceProvider _provider = provider; public SearchResultViewBase CreateSearchResultView(SearchResult result) { @@ -59,43 +54,40 @@ public Task> GetPreviewPanelsAsync(S // fail, don't show any preview for this item. var document = roslynResult.SearchResult.NavigableItem.Document; - var filePath = document.FilePath; + + // RoslynNavigateToSearchCallback will have placed the file-path for this document in the .Location property + // of this search result. This will either be the true location of a real file in the workspace. Or a + // temporary dummy file placed on disk for source-generated documents. This dummy file will have been made + // in coordination with the SourceGeneratedFileManager. That way when the editor tries to open that file, + // SourceGeneratedFileManager will intercept, fetch the actual contents, and pressent those in the text + // buffer that the user sees. + var filePath = roslynResult.Location; if (filePath is null) return null; Uri? absoluteUri; - if (document.SourceGeneratedDocumentIdentity is not null) + try { - absoluteUri = SourceGeneratedDocumentUri.Create(document.SourceGeneratedDocumentIdentity.Value).GetRequiredParsedUri(); + absoluteUri = ProtocolConversions.CreateAbsoluteUri(filePath); } - else + catch (UriFormatException) { - try - { - absoluteUri = ProtocolConversions.CreateAbsoluteUri(filePath); - } - catch (UriFormatException) - { - // Unable to create an absolute URI for this path - return null; - } + // Unable to create an absolute URI for this path + return null; } var projectGuid = _provider._workspace.GetProjectGuid(document.Project.Id); if (projectGuid == Guid.Empty) return null; - return new SearchResultPreviewPanelBase[] - { - new RoslynSearchResultPreviewPanel( - _provider, - // Editor APIs require a parseable System.Uri instance - absoluteUri, - projectGuid, - roslynResult.SearchResult.NavigableItem.SourceSpan.ToSpan(), - searchResultView.Title.Text, - searchResultView.PrimaryIcon) - }; + return [new RoslynSearchResultPreviewPanel( + _provider, + // Editor APIs require a parseable System.Uri instance + absoluteUri, + projectGuid, + roslynResult.SearchResult.NavigableItem.SourceSpan.ToSpan(), + searchResultView.Title.Text, + searchResultView.PrimaryIcon)]; } } } diff --git a/src/VisualStudio/Core/Def/Workspace/SourceGeneratedFileManager.cs b/src/VisualStudio/Core/Def/Workspace/SourceGeneratedFileManager.cs index 39712bb1af0d2..fcc30e1589d32 100644 --- a/src/VisualStudio/Core/Def/Workspace/SourceGeneratedFileManager.cs +++ b/src/VisualStudio/Core/Def/Workspace/SourceGeneratedFileManager.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; @@ -16,6 +17,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.CodeAnalysis.Threading; @@ -63,14 +65,17 @@ internal sealed class SourceGeneratedFileManager : IOpenTextBufferEventListener private readonly VisualStudioWorkspace _visualStudioWorkspace; /// - /// When we have to put a placeholder file on disk, we put it in a directory named by the GUID portion of the DocumentId. - /// We store the actual DocumentId (which includes the ProjectId) and some other textual information in - /// , so that way we don't have to pack the information into the path itself. - /// If we put the GUIDs and string names directly as components of the path, we quickly run into MAX_PATH. If we had a way to do virtual - /// monikers that don't run into MAX_PATH issues then we absolutely would want to get rid of this. + /// When we have to put a placeholder file on disk, we put it in a directory named by the GUID portion of the + /// DocumentId. We store the actual DocumentId (which includes the ProjectId) and some other textual information in + /// , so that way we don't have to pack the information + /// into the path itself. If we put the GUIDs and string names directly as components of the path, we quickly run + /// into MAX_PATH. If we had a way to do virtual monikers that don't run into MAX_PATH issues then we absolutely + /// would want to get rid of this. /// - /// All accesses should be on the UI thread. - private readonly Dictionary _directoryInfoOnDiskByContainingDirectoryId = []; + /// + /// This can be accessed on any thread. + /// + private readonly ConcurrentDictionary _directoryInfoOnDiskByContainingDirectoryId = []; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -103,17 +108,26 @@ public SourceGeneratedFileManager( openTextBufferProvider.AddListener(this); } - public Func> GetNavigationCallback(SourceGeneratedDocument document, TextSpan sourceSpan) + /// + /// Takes a and maps it to a file path that can be opened in Visual Studio. + /// This works simply by creating an empty temporary file in a well known location that we will then listen to see + /// if visual studio opens it. If so, we we can map that location on disk back to right information for this document, find the contents in the workspace and + /// update the text buffer with those contents. + /// + /// + /// This is all currently necessary because much of VS requires an actual file on disk to open a document, along + /// with special handling for file:// uris. For example, navigating to a source generated document, or showing its + /// contents in the Navigate-To preview window. Ideally we could just inform them of special handling of some + /// roslyn-specific uri schema (a-la VSCode), but that isn't currently possible. + /// + public string MapSourceGeneratedDocumentToOpenableFilePath(SourceGeneratedDocument document) { // We will create an file name to represent this generated file; the Visual Studio shell APIs imply you can use a URI, // but most URIs are blocked other than file:// and http://; they also get extra handling to attempt to download the file so // those aren't really usable anyways. // The file name we create is \\ - - if (!_directoryInfoOnDiskByContainingDirectoryId.ContainsKey(document.Id.Id)) - { - _directoryInfoOnDiskByContainingDirectoryId.Add(document.Id.Id, document.Identity); - } + _directoryInfoOnDiskByContainingDirectoryId.TryAdd(document.Id.Id, document.Identity); // We must always ensure the file name portion of the path is just the hint name, which matches the compiler's choice so // debugging works properly. @@ -123,13 +137,21 @@ public Func> GetNavigationCallback(SourceGenerated // Normalize hint name (it always contains forward slashes). document.HintName.Replace('/', Path.DirectorySeparatorChar)); - Directory.CreateDirectory(Path.GetDirectoryName(temporaryFilePath)); - - // Don't write to the file if it's already there, as that potentially triggers a file reload - if (!File.Exists(temporaryFilePath)) + IOUtilities.PerformIO(() => { - File.WriteAllText(temporaryFilePath, ""); - } + Directory.CreateDirectory(Path.GetDirectoryName(temporaryFilePath)); + + // Don't write to the file if it's already there, as that potentially triggers a file reload + if (!File.Exists(temporaryFilePath)) + File.WriteAllText(temporaryFilePath, ""); + }); + + return temporaryFilePath; + } + + public Func> GetNavigationCallback(SourceGeneratedDocument document, TextSpan sourceSpan) + { + var temporaryFilePath = MapSourceGeneratedDocumentToOpenableFilePath(document); return async cancellationToken => {