Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ public void ReportIncomplete()
_searchCallback.ReportIncomplete(IncompleteReason.Parsing);
}

public Task AddResultsAsync(
public async Task AddResultsAsync(
ImmutableArray<INavigateToSearchResult> 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
Expand All @@ -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<string?> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,9 @@ internal sealed partial class RoslynSearchItemsSourceProvider
/// Implementation of the <see cref="ISearchResultViewFactory"/>. Responsible for actually producing both the
/// item presented in the search results list, and the async preview for that item.
/// </summary>
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)
{
Expand Down Expand Up @@ -59,43 +54,40 @@ public Task<IReadOnlyList<SearchResultPreviewPanelBase>> 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)];
}
}
}
60 changes: 41 additions & 19 deletions src/VisualStudio/Core/Def/Workspace/SourceGeneratedFileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -63,14 +65,17 @@ internal sealed class SourceGeneratedFileManager : IOpenTextBufferEventListener
private readonly VisualStudioWorkspace _visualStudioWorkspace;

/// <summary>
/// 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
/// <see cref="_directoryInfoOnDiskByContainingDirectoryId"/>, 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
/// <see cref="_directoryInfoOnDiskByContainingDirectoryId"/>, 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.
/// </summary>
/// <remarks>All accesses should be on the UI thread.</remarks>
private readonly Dictionary<Guid, SourceGeneratedDocumentIdentity> _directoryInfoOnDiskByContainingDirectoryId = [];
/// <remarks>
/// This can be accessed on any thread.
/// </remarks>
private readonly ConcurrentDictionary<Guid, SourceGeneratedDocumentIdentity> _directoryInfoOnDiskByContainingDirectoryId = [];

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
Expand Down Expand Up @@ -103,17 +108,26 @@ public SourceGeneratedFileManager(
openTextBufferProvider.AddListener(this);
}

public Func<CancellationToken, Task<bool>> GetNavigationCallback(SourceGeneratedDocument document, TextSpan sourceSpan)
/// <summary>
/// Takes a <see cref="SourceGeneratedDocument"/> 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 <see
/// cref="SourceGeneratedDocumentIdentity"/> information for this document, find the contents in the workspace and
/// update the text buffer with those contents.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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 <temp path>\<document id in GUID form>\<hint name>

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.
Expand All @@ -123,13 +137,21 @@ public Func<CancellationToken, Task<bool>> 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<CancellationToken, Task<bool>> GetNavigationCallback(SourceGeneratedDocument document, TextSpan sourceSpan)
{
var temporaryFilePath = MapSourceGeneratedDocumentToOpenableFilePath(document);

return async cancellationToken =>
{
Expand Down
Loading