diff --git a/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs b/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs index 098b15c620951..3f35470afd51d 100644 --- a/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs @@ -3,15 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Composition; -using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -22,154 +19,51 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute [ExportWorkspaceService(typeof(IDesignerAttributeDiscoveryService)), Shared] internal sealed partial class DesignerAttributeDiscoveryService : IDesignerAttributeDiscoveryService { - /// - /// Protects mutable state in this type. - /// - private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1); - - /// - /// Keep track of the last information we reported. We will avoid notifying the host if we recompute and these - /// don't change. - /// - private readonly ConcurrentDictionary _documentToLastReportedInformation = new(); - [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public DesignerAttributeDiscoveryService() { } - public async ValueTask ProcessSolutionAsync( - Solution solution, - DocumentId? priorityDocumentId, - IDesignerAttributeDiscoveryService.ICallback callback, - CancellationToken cancellationToken) - { - using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) - { - // Remove any documents that are now gone. - foreach (var docId in _documentToLastReportedInformation.Keys) - { - if (!solution.ContainsDocument(docId)) - _documentToLastReportedInformation.TryRemove(docId, out _); - } - - // Handle the priority doc first. - var priorityDocument = solution.GetDocument(priorityDocumentId); - if (priorityDocument != null) - await ProcessProjectAsync(priorityDocument.Project, priorityDocument, callback, cancellationToken).ConfigureAwait(false); - - // Process the rest of the projects in dependency order so that their data is ready when we hit the - // projects that depend on them. - var dependencyGraph = solution.GetProjectDependencyGraph(); - foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) - { - if (projectId != priorityDocumentId?.ProjectId) - await ProcessProjectAsync(solution.GetRequiredProject(projectId), specificDocument: null, callback, cancellationToken).ConfigureAwait(false); - } - } - } - - private async Task ProcessProjectAsync( + public async IAsyncEnumerable ProcessProjectAsync( Project project, - Document? specificDocument, - IDesignerAttributeDiscoveryService.ICallback callback, - CancellationToken cancellationToken) + DocumentId? priorityDocumentId, + [EnumeratorCancellation] CancellationToken cancellationToken) { + // Ignore projects that don't support compilation or don't even have the DesignerCategoryAttribute in it. if (!project.SupportsCompilation) - return; + yield break; var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); var designerCategoryType = compilation.DesignerCategoryAttributeType(); if (designerCategoryType == null) - return; - - await ScanForDesignerCategoryUsageAsync( - project, specificDocument, callback, designerCategoryType, cancellationToken).ConfigureAwait(false); - - // If we scanned just a specific document in the project, now scan the rest of the files. - if (specificDocument != null) - await ScanForDesignerCategoryUsageAsync(project, specificDocument: null, callback, designerCategoryType, cancellationToken).ConfigureAwait(false); - } - - private async Task ScanForDesignerCategoryUsageAsync( - Project project, - Document? specificDocument, - IDesignerAttributeDiscoveryService.ICallback callback, - INamedTypeSymbol designerCategoryType, - CancellationToken cancellationToken) - { - // We need to reanalyze the project whenever it (or any of its dependencies) have - // changed. We need to know about dependencies since if a downstream project adds the - // DesignerCategory attribute to a class, that can affect us when we examine the classes - // in this project. - var projectVersion = await project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + yield break; - // Now get all the values that actually changed and notify VS about them. We don't need - // to tell it about the ones that didn't change since that will have no effect on the - // user experience. - var changedData = await ComputeChangedDataAsync( - project, specificDocument, projectVersion, designerCategoryType, cancellationToken).ConfigureAwait(false); - - // Only bother reporting non-empty information to save an unnecessary RPC. - if (!changedData.IsEmpty) - await callback.ReportDesignerAttributeDataAsync(changedData, cancellationToken).ConfigureAwait(false); - - // Now, keep track of what we've reported to the host so we won't report unchanged files in the future. We - // do this after the report has gone through as we want to make sure that if it cancels for any reason we - // don't hold onto values that may not have made it all the way to the project system. - foreach (var data in changedData) - _documentToLastReportedInformation[data.DocumentId] = (data.Category, projectVersion); - } + // If there is a priority doc, then scan that first. + var priorityDocument = priorityDocumentId == null ? null : project.GetDocument(priorityDocumentId); + if (priorityDocument is { FilePath: not null }) + { + var data = await ComputeDesignerAttributeDataAsync(designerCategoryType, priorityDocument, cancellationToken).ConfigureAwait(false); + if (data != null) + yield return data.Value; + } - private async Task> ComputeChangedDataAsync( - Project project, - Document? specificDocument, - VersionStamp projectVersion, - INamedTypeSymbol designerCategoryType, - CancellationToken cancellationToken) - { - using var _1 = ArrayBuilder>.GetInstance(out var tasks); + // now process the rest of the documents. + using var _ = ArrayBuilder>.GetInstance(out var tasks); foreach (var document in project.Documents) { - // If we're only analyzing a specific document, then skip the rest. - if (specificDocument != null && document != specificDocument) - continue; - - // If we don't have a path for this document, we cant proceed with it. - // We need that path to inform the project system which file we're referring to. - if (document.FilePath == null) + if (document == priorityDocument || document.FilePath is null) continue; - // If nothing has changed at the top level between the last time we analyzed this document and now, then - // no need to analyze again. - if (_documentToLastReportedInformation.TryGetValue(document.Id, out var existingInfo) && - existingInfo.projectVersion == projectVersion) - { - continue; - } - tasks.Add(ComputeDesignerAttributeDataAsync(designerCategoryType, document, cancellationToken)); } - using var _2 = ArrayBuilder.GetInstance(tasks.Count, out var results); - - // Avoid unnecessary allocation of result array. - await Task.WhenAll((IEnumerable)tasks).ConfigureAwait(false); - - foreach (var task in tasks) + // Convert the tasks into one final stream we can read all the results from. + await foreach (var dataOpt in tasks.ToImmutable().StreamAsync(cancellationToken).ConfigureAwait(false)) { - var dataOpt = await task.ConfigureAwait(false); - if (dataOpt == null) - continue; - - var data = dataOpt.Value; - _documentToLastReportedInformation.TryGetValue(data.DocumentId, out var existingInfo); - if (existingInfo.category != data.Category) - results.Add(data); + if (dataOpt != null) + yield return dataOpt.Value; } - - return results.ToImmutableAndClear(); } private static async Task ComputeDesignerAttributeDataAsync( @@ -179,18 +73,16 @@ private async Task> ComputeChangedDataAsyn { Contract.ThrowIfNull(document.FilePath); - // We either haven't computed the designer info, or our data was out of date. We need - // So recompute here. Figure out what the current category is, and if that's different - // from what we previously stored. var category = await DesignerAttributeHelpers.ComputeDesignerAttributeCategoryAsync( designerCategoryType, document, cancellationToken).ConfigureAwait(false); - return new DesignerAttributeData - { - Category = category, - DocumentId = document.Id, - FilePath = document.FilePath, - }; + // If there's no category (the common case) don't return anything. The host itself will see no results + // returned and can handle that case (for example, if a type previously had the attribute but doesn't + // any longer). + if (category == null) + return null; + + return new DesignerAttributeData(category, document.Id, document.FilePath); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs index 150e957379fb3..b17014b068654 100644 --- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs +++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs @@ -10,24 +10,34 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute /// Serialization typed used to pass information to/from OOP and VS. /// [DataContract] - internal struct DesignerAttributeData + internal readonly struct DesignerAttributeData { /// /// The category specified in a [DesignerCategory("...")] attribute. /// [DataMember(Order = 0)] - public string? Category; + public readonly string? Category; /// /// The document this applies to. /// [DataMember(Order = 1)] - public DocumentId DocumentId; + public readonly DocumentId DocumentId; /// /// Path for this . /// [DataMember(Order = 2)] - public string FilePath; + public readonly string FilePath; + + public DesignerAttributeData(string? category, DocumentId documentId, string filePath) + { + Category = category; + DocumentId = documentId; + FilePath = filePath; + } + + public DesignerAttributeData WithCategory(string? category) + => new(category, DocumentId, FilePath); } } diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs index 038f14f77fc8b..97ffc48cef825 100644 --- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs +++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs @@ -54,6 +54,9 @@ internal static class DesignerAttributeHelpers } return null; + + static string? GetArgumentString(TypedConstant argument) + => argument is { IsNull: false, Type.SpecialType: SpecialType.System_String, Value: string stringValue } ? stringValue.Trim() : null; } private static SyntaxNode? FindFirstNonNestedClass( @@ -77,18 +80,5 @@ internal static class DesignerAttributeHelpers return null; } - - private static string? GetArgumentString(TypedConstant argument) - { - if (argument.Type == null || - argument.Type.SpecialType != SpecialType.System_String || - argument.IsNull || - argument.Value is not string stringValue) - { - return null; - } - - return stringValue.Trim(); - } } } diff --git a/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs index c3bc3fbf0af0d..c98d3fd5c4998 100644 --- a/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs +++ b/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs @@ -2,21 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Immutable; +using System.Collections.Generic; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.DesignerAttribute { - internal partial interface IDesignerAttributeDiscoveryService : IWorkspaceService + internal interface IDesignerAttributeDiscoveryService : IWorkspaceService { - public interface ICallback - { - ValueTask ReportDesignerAttributeDataAsync(ImmutableArray data, CancellationToken cancellationToken); - } - - ValueTask ProcessSolutionAsync(Solution solution, DocumentId? priorityDocumentId, ICallback callback, CancellationToken cancellationToken); + IAsyncEnumerable ProcessProjectAsync(Project project, DocumentId? priorityDocumentId, CancellationToken cancellationToken); } } diff --git a/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs index 4716273c30a92..15c8d86e9fda3 100644 --- a/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs +++ b/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs @@ -2,13 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Immutable; -using System.Composition; +using System.Collections.Generic; using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Remote; namespace Microsoft.CodeAnalysis.DesignerAttribute { @@ -18,27 +13,6 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute /// internal interface IRemoteDesignerAttributeDiscoveryService { - internal interface ICallback - { - ValueTask ReportDesignerAttributeDataAsync(RemoteServiceCallbackId callbackId, ImmutableArray data, CancellationToken cancellationToken); - } - - ValueTask DiscoverDesignerAttributesAsync(RemoteServiceCallbackId callbackId, Checksum solutionChecksum, DocumentId? priorityDocument, CancellationToken cancellationToken); - } - - [ExportRemoteServiceCallbackDispatcher(typeof(IRemoteDesignerAttributeDiscoveryService)), Shared] - internal sealed class RemoteDesignerAttributeDiscoveryCallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteDesignerAttributeDiscoveryService.ICallback - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RemoteDesignerAttributeDiscoveryCallbackDispatcher() - { - } - - private new IDesignerAttributeDiscoveryService.ICallback GetCallback(RemoteServiceCallbackId callbackId) - => (IDesignerAttributeDiscoveryService.ICallback)base.GetCallback(callbackId); - - public ValueTask ReportDesignerAttributeDataAsync(RemoteServiceCallbackId callbackId, ImmutableArray data, CancellationToken cancellationToken) - => GetCallback(callbackId).ReportDesignerAttributeDataAsync(data, cancellationToken); + IAsyncEnumerable DiscoverDesignerAttributesAsync(Checksum solutionChecksum, ProjectId project, DocumentId? priorityDocument, CancellationToken cancellationToken); } } diff --git a/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs b/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs index a10722fc7775c..f0503dafe78db 100644 --- a/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs +++ b/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs @@ -2,13 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.DesignerAttribute; +using Microsoft.CodeAnalysis.Editor.UnitTests; using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Utilities; @@ -19,37 +20,40 @@ namespace Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.DesignerAttri [UseExportProvider] public class DesignerAttributeServiceTests { - [Fact] - public async Task NoDesignerTest1() + private static readonly TestComposition s_inProcessComposition = EditorTestCompositions.EditorFeatures; + private static readonly TestComposition s_outOffProcessComposition = s_inProcessComposition.WithTestHostParts(TestHost.OutOfProcess); + + [Theory, CombinatorialData] + public async Task NoDesignerTest1(TestHost host) { var code = @"class Test { }"; - await TestAsync(code, category: null); + await TestAsync(code, category: null, host); } - [Fact] - public async Task NoDesignerOnSecondClass() + [Theory, CombinatorialData] + public async Task NoDesignerOnSecondClass(TestHost host) { await TestAsync( @"class Test1 { } [System.ComponentModel.DesignerCategory(""Form"")] -class Test2 { }", category: null); +class Test2 { }", category: null, host); } - [Fact] - public async Task NoDesignerOnStruct() + [Theory, CombinatorialData] + public async Task NoDesignerOnStruct(TestHost host) { await TestAsync( @" [System.ComponentModel.DesignerCategory(""Form"")] -struct Test1 { }", category: null); +struct Test1 { }", category: null, host); } - [Fact] - public async Task NoDesignerOnNestedClass() + [Theory, CombinatorialData] + public async Task NoDesignerOnNestedClass(TestHost host) { await TestAsync( @@ -57,42 +61,56 @@ await TestAsync( { [System.ComponentModel.DesignerCategory(""Form"")] class Test2 { } -}", category: null); +}", category: null, host); } - [Fact] - public async Task SimpleDesignerTest() + [Theory, CombinatorialData] + public async Task SimpleDesignerTest(TestHost host) { await TestAsync( @"[System.ComponentModel.DesignerCategory(""Form"")] -class Test { }", "Form"); +class Test { }", "Form", host); } - [Fact] - public async Task SimpleDesignerTest2() + [Theory, CombinatorialData] + public async Task SimpleDesignerTest2(TestHost host) { await TestAsync( @"using System.ComponentModel; [DesignerCategory(""Form"")] -class Test { }", "Form"); +class Test { }", "Form", host); } - private static async Task TestAsync(string codeWithMarker, string category) + private static async Task TestAsync(string codeWithMarker, string? category, TestHost host) { - using var workspace = TestWorkspace.CreateCSharp(codeWithMarker, openDocuments: false); + using var workspace = TestWorkspace.CreateCSharp( + codeWithMarker, openDocuments: false, composition: host == TestHost.OutOfProcess ? s_outOffProcessComposition : s_inProcessComposition); + + var solution = workspace.CurrentSolution; var hostDocument = workspace.Documents.First(); var documentId = hostDocument.Id; - var document = workspace.CurrentSolution.GetDocument(documentId); - - var compilation = await document.Project.GetCompilationAsync(); - var actual = await DesignerAttributeHelpers.ComputeDesignerAttributeCategoryAsync( - compilation.DesignerCategoryAttributeType(), document, CancellationToken.None); - Assert.Equal(category, actual); + var service = solution.Services.GetRequiredService(); + var stream = service.ProcessProjectAsync(solution.GetRequiredProject(documentId.ProjectId), priorityDocumentId: null, CancellationToken.None); + + var items = new List(); + await foreach (var item in stream) + items.Add(item); + + if (category != null) + { + Assert.Equal(1, items.Count); + Assert.Equal(category, items.Single().Category); + Assert.Equal(documentId, items.Single().DocumentId); + } + else + { + Assert.Empty(items); + } } } } diff --git a/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs b/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs index f8b55276c7f50..395b697978e3f 100644 --- a/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs +++ b/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs @@ -5,24 +5,24 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; +using EnvDTE; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.DesignerAttribute; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.SolutionCrawler; using Microsoft.VisualStudio.Designer.Interfaces; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; +using Microsoft.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Shell.Services; using Roslyn.Utilities; @@ -31,7 +31,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.DesignerAttribu { [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] internal class VisualStudioDesignerAttributeService : - ForegroundThreadAffinitizedObject, IDesignerAttributeDiscoveryService.ICallback, IEventListener, IDisposable + ForegroundThreadAffinitizedObject, IEventListener, IDisposable { private readonly VisualStudioWorkspaceImpl _workspace; @@ -52,11 +52,37 @@ internal class VisualStudioDesignerAttributeService : /// private IVSMDDesignerService? _legacyDesignerService; + /// + /// Queue that tells us to recompute designer attributes when workspace changes happen. This queue is + /// cancellable and will be restarted when new changes come in. That way we can be quickly update the designer + /// attribute for a file when a user edits it. + /// private readonly AsyncBatchingWorkQueue _workQueue; - // We'll get notifications from the OOP server about new attribute arguments. Collect those notifications and - // deliver them to VS in batches to prevent flooding the UI thread. - private readonly AsyncBatchingWorkQueue _projectSystemNotificationQueue; + /// + /// We'll get notifications from the OOP server about new attribute arguments. Collect those notifications and + /// deliver them to VS in batches to prevent flooding the UI thread. Importantly, we do not cancel this queue. + /// Once we've decided to update the project system, we want to allow that to proceed. + /// + /// This queue both sends the individual data objects we get back, or the solution instance once we're done with + /// a particular project request. The latter is used so that we can determine which documents are now gone, so + /// we can dump our cached data for them. + /// + /// + private readonly AsyncBatchingWorkQueue<(CodeAnalysis.Solution? solution, DesignerAttributeData? data)> _projectSystemNotificationQueue; + + /// + /// Keep track of the last version we were at when we processed a project. We'll skip reprocessing projects if + /// that version hasn't changed. + /// + private readonly ConcurrentDictionary _projectToLastComputedDependentSemanticVersion = new(); + + /// + /// Keep track of the last information we reported per document. We will avoid notifying the host if we + /// recompute and these don't change. Note: we keep track if we reported as well to + /// represent that the file is not designable. + /// + private readonly ConcurrentDictionary _documentToLastReportedCategory = new(); [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -78,7 +104,7 @@ public VisualStudioDesignerAttributeService( listener, ThreadingContext.DisposalToken); - _projectSystemNotificationQueue = new AsyncBatchingWorkQueue( + _projectSystemNotificationQueue = new AsyncBatchingWorkQueue<(CodeAnalysis.Solution? solution, DesignerAttributeData? data)>( TimeSpan.FromSeconds(1), this.NotifyProjectSystemAsync, listener, @@ -95,12 +121,14 @@ void IEventListener.StartListening(Workspace workspace, object _) if (workspace != _workspace) return; + // Register for changes, and kick off hte initial scan. _workspace.WorkspaceChanged += OnWorkspaceChanged; _workQueue.AddWork(cancelExistingWork: true); } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { + // cancel any existing work and start rescanning. this way we can respond to edits to a file very quickly. _workQueue.AddWork(cancelExistingWork: true); } @@ -111,38 +139,117 @@ private async ValueTask ProcessWorkspaceChangeAsync(CancellationToken cancellati cancellationToken.ThrowIfCancellationRequested(); + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + return; + var solution = _workspace.CurrentSolution; + + // remove any data for projects that are no longer around. + HandRemovedProjects(solution); + + // Now process all the projects we do have, prioritizing the active project/document first. + var trackingService = _workspace.Services.GetRequiredService(); + + var priorityDocument = solution.GetDocument(trackingService.TryGetActiveDocument()); + if (priorityDocument != null) + await ProcessProjectAsync(client, priorityDocument.Project, priorityDocument.Id, cancellationToken).ConfigureAwait(false); + + // Process the rest of the projects in dependency order so that their data is ready when we hit the + // projects that depend on them. + var dependencyGraph = solution.GetProjectDependencyGraph(); + foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) + { + // skip the prioritized project we handled above. + if (projectId == priorityDocument?.Id.ProjectId) + continue; + + await ProcessProjectAsync(client, solution.GetRequiredProject(projectId), priorityDocumentId: null, cancellationToken).ConfigureAwait(false); + } + } + + private void HandRemovedProjects(CodeAnalysis.Solution solution) + { foreach (var (projectId, _) in _cpsProjects) { if (!solution.ContainsProject(projectId)) _cpsProjects.TryRemove(projectId, out _); } - var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - return; + // when a project is removed, remove our cached attribute data for it. No point in actually notifying the + // host as there isn't any project anymore to notify it about. - var trackingService = _workspace.Services.GetRequiredService(); - var priorityDocument = trackingService.TryGetActiveDocument(); + foreach (var (projectId, _) in _projectToLastComputedDependentSemanticVersion) + { + if (!solution.ContainsProject(projectId)) + _projectToLastComputedDependentSemanticVersion.TryRemove(projectId, out _); + } + } - await client.TryInvokeAsync( - solution, - (service, checksum, callbackId, cancellationToken) => service.DiscoverDesignerAttributesAsync(callbackId, checksum, priorityDocument, cancellationToken), - callbackTarget: this, - cancellationToken).ConfigureAwait(false); + private async Task ProcessProjectAsync( + RemoteHostClient client, + CodeAnalysis.Project project, + DocumentId? priorityDocumentId, + CancellationToken cancellationToken) + { + // We need to recompute the designer attributes for a project if it's own semantic-version changes, or the + // semantic-version of any dependent projects change. The reason for checking dependent projects is that we + // look for the designer attribute on subclasses as well (so we have to walk the inheritance tree). This + // tree may be unfortunately be affected by dependent projects. In an ideal design we would require the + // attribute be on the declaration point so we could only check things that are known to directly have an + // attribute on them, and we wouldn't have to look up the inheritance hierarchy. + var dependentSemanticVersion = await project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + + if (!_projectToLastComputedDependentSemanticVersion.TryGetValue(project.Id, out var lastComputedVersion) || + lastComputedVersion != dependentSemanticVersion) + { + var stream = client.TryInvokeStreamAsync( + project, + (service, checksum, cancellationToken) => service.DiscoverDesignerAttributesAsync(checksum, project.Id, priorityDocumentId, cancellationToken), + cancellationToken); + + using var _ = PooledHashSet.GetInstance(out var seenDocuments); + + // get the results and add all the documents we hear about to the notification queue so they can be batched up. + await foreach (var data in stream.ConfigureAwait(false)) + { + seenDocuments.Add(data.DocumentId); + _projectSystemNotificationQueue.AddWork((solution: null, data)); + } + + // Also, for any documents we didn't hear about, ensure we emit a clear on its category if we have + // currently stored for it. This also ensures we initially report about all non-designer files when a + // solution is opened. This is needed in case the project file says it is designable, but the user made + // some change outside of VS that makes it non-designable. We will pick this up here and ensure the data + // is cleared. From that point on, we won't issue any more notifications about those files unless the + // category actually does change. + foreach (var document in project.Documents) + { + if (document.FilePath != null && !seenDocuments.Contains(document.Id)) + _projectSystemNotificationQueue.AddWork((solution: null, new DesignerAttributeData(null, document.Id, document.FilePath))); + } + + // once done, also enqueue the solution as well so that the project-system queue can cleanup + // any stale data about it. + _projectSystemNotificationQueue.AddWork((project.Solution, data: null)); + + // now that we're done processing the project, record this version-stamp so we don't have to process it again in the future. + _projectToLastComputedDependentSemanticVersion[project.Id] = dependentSemanticVersion; + } } private async ValueTask NotifyProjectSystemAsync( - ImmutableSegmentedList data, CancellationToken cancellationToken) + ImmutableSegmentedList<(CodeAnalysis.Solution? solution, DesignerAttributeData? data)> dataList, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var _1 = ArrayBuilder.GetInstance(out var filteredInfos); - AddFilteredInfos(data, filteredInfos); + using var _1 = ArrayBuilder.GetInstance(out var changedData); + using var _2 = ArrayBuilder.GetInstance(out var tasks); + + var latestSolution = AddChangedData(dataList, changedData); // Now, group all the notifications by project and update all the projects in parallel. - using var _2 = ArrayBuilder.GetInstance(out var tasks); - foreach (var group in filteredInfos.GroupBy(a => a.DocumentId.ProjectId)) + foreach (var group in changedData.GroupBy(a => a.DocumentId.ProjectId)) { cancellationToken.ThrowIfCancellationRequested(); tasks.Add(NotifyProjectSystemAsync(group.Key, group, cancellationToken)); @@ -150,40 +257,78 @@ private async ValueTask NotifyProjectSystemAsync( // Wait until all project updates have happened before processing the next batch. await Task.WhenAll(tasks).ConfigureAwait(false); + + // now that we've reported this data, record that we've done so so that we don't report the same data again in the future. + foreach (var data in changedData) + _documentToLastReportedCategory[data.DocumentId] = data.Category; + + // Now, check the documents we've stored against the changed projects to see if they're no longer around. If + // so, dump what we have. No need to notify anyone about this as the files are literally not in the + // solution anymore. this is just to ensure we don't hold onto data forever. + if (latestSolution != null) + { + foreach (var (documentId, _) in _documentToLastReportedCategory) + { + if (!latestSolution.ContainsDocument(documentId)) + _documentToLastReportedCategory.TryRemove(documentId, out _); + } + } } - private static void AddFilteredInfos(ImmutableSegmentedList data, ArrayBuilder filteredData) + private CodeAnalysis.Solution? AddChangedData( + ImmutableSegmentedList<(CodeAnalysis.Solution? solution, DesignerAttributeData? data)> dataList, + ArrayBuilder changedData) { - using var _ = PooledHashSet.GetInstance(out var seenDocumentIds); + using var _1 = PooledHashSet.GetInstance(out var seenDocumentIds); + using var _2 = ArrayBuilder.GetInstance(out var latestData); + + CodeAnalysis.Solution? lastSolution = null; + + for (var i = dataList.Count - 1; i >= 0; i--) + { + // go in reverse order so that results about the same document only take the later value. + var (solution, data) = dataList[i]; + + if (data != null) + { + if (seenDocumentIds.Add(data.Value.DocumentId)) + latestData.Add(data.Value); + } + + lastSolution ??= solution; + } - // Walk the list of designer items in reverse, and skip any items for a project once - // we've already seen it once. That way, we're only reporting the most up to date - // information for a project, and we're skipping the stale information. - for (var i = data.Count - 1; i >= 0; i--) + foreach (var data in latestData) { - var info = data[i]; - if (seenDocumentIds.Add(info.DocumentId)) - filteredData.Add(info); + // only issue a change notification for files we haven't issued a notification for, or for files that + // changed their category. + if (!_documentToLastReportedCategory.TryGetValue(data.DocumentId, out var existingCategory) || + existingCategory != data.Category) + { + changedData.Add(data); + } } + + return lastSolution; } private async Task NotifyProjectSystemAsync( ProjectId projectId, - IEnumerable data, + IEnumerable dataList, CancellationToken cancellationToken) { // Delegate to the CPS or legacy notification services as necessary. var cpsUpdateService = await GetUpdateServiceIfCpsProjectAsync(projectId, cancellationToken).ConfigureAwait(false); var task = cpsUpdateService == null - ? NotifyLegacyProjectSystemAsync(projectId, data, cancellationToken) - : NotifyCpsProjectSystemAsync(projectId, cpsUpdateService, data, cancellationToken); + ? NotifyLegacyProjectSystemAsync(projectId, dataList, cancellationToken) + : NotifyCpsProjectSystemAsync(projectId, cpsUpdateService, dataList, cancellationToken); await task.ConfigureAwait(false); } private async Task NotifyLegacyProjectSystemAsync( ProjectId projectId, - IEnumerable data, + IEnumerable dataList, CancellationToken cancellationToken) { // legacy project system can only be talked to on the UI thread. @@ -199,10 +344,10 @@ private async Task NotifyLegacyProjectSystemAsync( if (hierarchy == null) return; - foreach (var info in data) + foreach (var data in dataList) { cancellationToken.ThrowIfCancellationRequested(); - NotifyLegacyProjectSystemOnUIThread(designerService, hierarchy, info); + NotifyLegacyProjectSystemOnUIThread(designerService, hierarchy, data); } } @@ -244,7 +389,7 @@ private void NotifyLegacyProjectSystemOnUIThread( private async Task NotifyCpsProjectSystemAsync( ProjectId projectId, IProjectItemDesignerTypeUpdateService updateService, - IEnumerable data, + IEnumerable dataList, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -259,10 +404,10 @@ private async Task NotifyCpsProjectSystemAsync( using var _ = ArrayBuilder.GetInstance(out var tasks); - foreach (var info in data) + foreach (var data in dataList) { cancellationToken.ThrowIfCancellationRequested(); - tasks.Add(NotifyCpsProjectSystemAsync(updateService, info, cancellationToken)); + tasks.Add(NotifyCpsProjectSystemAsync(updateService, data, cancellationToken)); } await Task.WhenAll(tasks).ConfigureAwait(false); @@ -317,15 +462,5 @@ private static async Task NotifyCpsProjectSystemAsync( return serviceProvider.GetService(typeof(IProjectItemDesignerTypeUpdateService)) as IProjectItemDesignerTypeUpdateService; } } - - /// - /// Callback from the OOP service back into us. - /// - public ValueTask ReportDesignerAttributeDataAsync(ImmutableArray data, CancellationToken cancellationToken) - { - Contract.ThrowIfNull(_projectSystemNotificationQueue); - _projectSystemNotificationQueue.AddWork(data); - return ValueTaskFactory.CompletedTask; - } } } diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index 6114e73abc7f4..510474d6f82af 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -136,38 +136,24 @@ await client.TryInvokeAsync( var solutionChecksum = await solution.State.GetChecksumAsync(CancellationToken.None); await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, solution.WorkspaceVersion, CancellationToken.None); - var callback = new DesignerAttributeComputerCallback(); + using var connection = client.CreateConnection(callbackTarget: null); - using var connection = client.CreateConnection(callback); - - var invokeTask = connection.TryInvokeAsync( + var stream = connection.TryInvokeStreamAsync( solution, - (service, checksum, callbackId, cancellationToken) => service.DiscoverDesignerAttributesAsync(callbackId, checksum, priorityDocument: null, cancellationToken), + (service, checksum, cancellationToken) => service.DiscoverDesignerAttributesAsync(checksum, solution.Projects.Single().Id, priorityDocument: null, cancellationToken), cancellationTokenSource.Token); - var infos = await callback.Infos; - Assert.Equal(1, infos.Length); + var items = new List(); + await foreach (var item in stream) + items.Add(item); + + Assert.Equal(1, items.Count); - var info = infos[0]; + var info = items[0]; Assert.Equal("Form", info.Category); Assert.Equal(solution.Projects.Single().Documents.Single().Id, info.DocumentId); cancellationTokenSource.Cancel(); - - Assert.True(await invokeTask); - } - - private class DesignerAttributeComputerCallback : IDesignerAttributeDiscoveryService.ICallback - { - private readonly TaskCompletionSource> _infosSource = new(); - - public Task> Infos => _infosSource.Task; - - public ValueTask ReportDesignerAttributeDataAsync(ImmutableArray infos, CancellationToken cancellationToken) - { - _infosSource.SetResult(infos); - return ValueTaskFactory.CompletedTask; - } } [Fact] diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs index ab30b9b9feb54..12f3d124d8fac 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; @@ -81,5 +82,21 @@ static async IAsyncEnumerable ReadAllAsync(ChannelReader reader, [Enumerat } } } + + /// + /// Tasks an array of value producing tasks and produces a stream of results out of them. Like absolutely no ordering guarantee is provided. It will be expected that the + /// individual values from distinct tasks will be interleaved together. + /// + public static IAsyncEnumerable StreamAsync(this ImmutableArray> tasks, CancellationToken cancellationToken) + { + return tasks.SelectAsArray(static (t, cancellationToken) => CreateStream(t, cancellationToken), cancellationToken).MergeAsync(cancellationToken); + + static async IAsyncEnumerable CreateStream( + Task task, [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return await task.ConfigureAwait(false); + } + } } } diff --git a/src/Workspaces/Remote/Core/ServiceDescriptors.cs b/src/Workspaces/Remote/Core/ServiceDescriptors.cs index 35e8e67a66751..5b6932adfbef0 100644 --- a/src/Workspaces/Remote/Core/ServiceDescriptors.cs +++ b/src/Workspaces/Remote/Core/ServiceDescriptors.cs @@ -54,7 +54,7 @@ internal sealed class ServiceDescriptors (typeof(IRemoteAssetSynchronizationService), null), (typeof(IRemoteAsynchronousOperationListenerService), null), (typeof(IRemoteTaskListService), null), - (typeof(IRemoteDesignerAttributeDiscoveryService), typeof(IRemoteDesignerAttributeDiscoveryService.ICallback)), + (typeof(IRemoteDesignerAttributeDiscoveryService), null), (typeof(IRemoteDiagnosticAnalyzerService), null), (typeof(IRemoteSemanticClassificationService), null), (typeof(IRemoteDocumentHighlightsService), null), diff --git a/src/Workspaces/Remote/ServiceHub/Services/DesignerAttributeDiscovery/RemoteDesignerAttributeDiscoveryService.cs b/src/Workspaces/Remote/ServiceHub/Services/DesignerAttributeDiscovery/RemoteDesignerAttributeDiscoveryService.cs index 0d878efb54d66..5c3bb62686c99 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DesignerAttributeDiscovery/RemoteDesignerAttributeDiscoveryService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DesignerAttributeDiscovery/RemoteDesignerAttributeDiscoveryService.cs @@ -2,62 +2,55 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.DesignerAttribute; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SolutionCrawler; +using StreamJsonRpc; namespace Microsoft.CodeAnalysis.Remote { internal sealed class RemoteDesignerAttributeDiscoveryService : BrokeredServiceBase, IRemoteDesignerAttributeDiscoveryService { - private sealed class CallbackWrapper : IDesignerAttributeDiscoveryService.ICallback + /// + /// Allow designer attribute computation to continue on on the server, even while the client is processing the + /// last batch of results. + /// + /// + /// This value was not determined empirically. + /// + private const int MaxReadAhead = 64; + + internal sealed class Factory : FactoryBase { - private readonly RemoteCallback _callback; - private readonly RemoteServiceCallbackId _callbackId; - - public CallbackWrapper( - RemoteCallback callback, - RemoteServiceCallbackId callbackId) - { - _callback = callback; - _callbackId = callbackId; - } - - public ValueTask ReportDesignerAttributeDataAsync(ImmutableArray data, CancellationToken cancellationToken) - => _callback.InvokeAsync((callback, cancellationToken) => callback.ReportDesignerAttributeDataAsync(_callbackId, data, cancellationToken), cancellationToken); - } - - internal sealed class Factory : FactoryBase - { - protected override IRemoteDesignerAttributeDiscoveryService CreateService(in ServiceConstructionArguments arguments, RemoteCallback callback) - => new RemoteDesignerAttributeDiscoveryService(arguments, callback); + protected override IRemoteDesignerAttributeDiscoveryService CreateService(in ServiceConstructionArguments arguments) + => new RemoteDesignerAttributeDiscoveryService(arguments); } - private readonly RemoteCallback _callback; - - public RemoteDesignerAttributeDiscoveryService(in ServiceConstructionArguments arguments, RemoteCallback callback) + public RemoteDesignerAttributeDiscoveryService(in ServiceConstructionArguments arguments) : base(arguments) { - _callback = callback; } - public ValueTask DiscoverDesignerAttributesAsync( - RemoteServiceCallbackId callbackId, + public IAsyncEnumerable DiscoverDesignerAttributesAsync( Checksum solutionChecksum, + ProjectId projectId, DocumentId? priorityDocument, CancellationToken cancellationToken) { - return RunServiceAsync( + var stream = StreamWithSolutionAsync( solutionChecksum, - solution => + (solution, cancellationToken) => { + var project = solution.GetRequiredProject(projectId); var service = solution.Services.GetRequiredService(); - return service.ProcessSolutionAsync( - solution, priorityDocument, new CallbackWrapper(_callback, callbackId), cancellationToken); - }, - cancellationToken); + return service.ProcessProjectAsync(project, priorityDocument, cancellationToken); + }, cancellationToken); + return stream.WithJsonRpcSettings(new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); } } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs b/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs index 32604d49271e5..ee20abdd7faa8 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs @@ -58,19 +58,16 @@ public IAsyncEnumerable SearchDocumentAsync( ImmutableArray kinds, CancellationToken cancellationToken) { - return StreamWithSolutionAsync(solutionChecksum, SearchDocumentWorkerAsync, cancellationToken).WithJsonRpcSettings( - new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); - - async IAsyncEnumerable SearchDocumentWorkerAsync(Solution solution, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var document = solution.GetRequiredDocument(documentId); - - await foreach (var item in AbstractNavigateToSearchService.SearchDocumentInCurrentProcessAsync( - document, searchPattern, kinds.ToImmutableHashSet(), cancellationToken).ConfigureAwait(false)) + return StreamWithSolutionAsync( + solutionChecksum, + (solution, cancellationToken) => { - yield return item; - } - } + var document = solution.GetRequiredDocument(documentId); + + return AbstractNavigateToSearchService.SearchDocumentInCurrentProcessAsync( + document, searchPattern, kinds.ToImmutableHashSet(), cancellationToken); + }, + cancellationToken).WithJsonRpcSettings(new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); } public IAsyncEnumerable SearchProjectAsync( @@ -81,21 +78,17 @@ public IAsyncEnumerable SearchProjectAsync( ImmutableArray kinds, CancellationToken cancellationToken) { - return StreamWithSolutionAsync(solutionChecksum, SearchProjectWorkerAsync, cancellationToken).WithJsonRpcSettings( - new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); - - async IAsyncEnumerable SearchProjectWorkerAsync(Solution solution, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var project = solution.GetRequiredProject(projectId); + return StreamWithSolutionAsync( + solutionChecksum, + (solution, cancellationToken) => + { + var project = solution.GetRequiredProject(projectId); - var priorityDocuments = priorityDocumentIds.SelectAsArray(d => solution.GetRequiredDocument(d)); + var priorityDocuments = priorityDocumentIds.SelectAsArray(d => solution.GetRequiredDocument(d)); - await foreach (var item in AbstractNavigateToSearchService.SearchProjectInCurrentProcessAsync( - project, priorityDocuments, searchPattern, kinds.ToImmutableHashSet(), cancellationToken).ConfigureAwait(false)) - { - yield return item; - } - } + return AbstractNavigateToSearchService.SearchProjectInCurrentProcessAsync( + project, priorityDocuments, searchPattern, kinds.ToImmutableHashSet(), cancellationToken); + }, cancellationToken).WithJsonRpcSettings(new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); } public IAsyncEnumerable SearchGeneratedDocumentsAsync( @@ -105,19 +98,16 @@ public IAsyncEnumerable SearchGeneratedDocumentsAsync( ImmutableArray kinds, CancellationToken cancellationToken) { - return StreamWithSolutionAsync(solutionChecksum, SearchGeneratedDocumentsWorkerAsync, cancellationToken).WithJsonRpcSettings( - new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); - - async IAsyncEnumerable SearchGeneratedDocumentsWorkerAsync(Solution solution, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var project = solution.GetRequiredProject(projectId); - - await foreach (var item in AbstractNavigateToSearchService.SearchGeneratedDocumentsInCurrentProcessAsync( - project, searchPattern, kinds.ToImmutableHashSet(), cancellationToken).ConfigureAwait(false)) + return StreamWithSolutionAsync( + solutionChecksum, + (solution, cancellationToken) => { - yield return item; - } - } + var project = solution.GetRequiredProject(projectId); + + return AbstractNavigateToSearchService.SearchGeneratedDocumentsInCurrentProcessAsync( + project, searchPattern, kinds.ToImmutableHashSet(), cancellationToken); + }, + cancellationToken).WithJsonRpcSettings(new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead }); } public IAsyncEnumerable SearchCachedDocumentsAsync( @@ -131,7 +121,7 @@ public IAsyncEnumerable SearchCachedDocumentsAsync( // Intentionally do not call GetSolutionAsync here. We do not want the cost of // synchronizing the solution over to the remote side. Instead, we just directly - // check whatever cached data we have from the previous vs session. + // check whatever cached data we have from the previous VS session. var storageService = GetWorkspaceServices().GetPersistentStorageService(); return AbstractNavigateToSearchService.SearchCachedDocumentsInCurrentProcessAsync( storageService, documentKeys, priorityDocumentKeys, searchPattern, kinds.ToImmutableHashSet(), cancellationToken);