From 3d0c7b28cac915ed987752af45c1b9ba580031fa Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 09:42:43 -0700 Subject: [PATCH 01/45] Rename VisualStudioHostServicesProvider parameter and field to match name --- .../LanguageClient/RazorLanguageServerClient.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index bcaad84a57c..ce6c3de7276 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -45,7 +45,7 @@ internal class RazorLanguageServerClient( ITelemetryReporter telemetryReporter, IClientSettingsManager clientSettingsManager, ILspServerActivationTracker lspServerActivationTracker, - VisualStudioHostServicesProvider vsHostWorkspaceServicesProvider) + VisualStudioHostServicesProvider vsHostServicesProvider) : ILanguageClient, ILanguageClientCustomMessage2, ILanguageClientPriority { private readonly ILanguageClientBroker _languageClientBroker = languageClientBroker ?? throw new ArgumentNullException(nameof(languageClientBroker)); @@ -58,7 +58,7 @@ internal class RazorLanguageServerClient( private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore ?? throw new ArgumentNullException(nameof(projectConfigurationFilePathStore)); private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher ?? throw new ArgumentNullException(nameof(projectInfoEndpointPublisher)); private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - private readonly VisualStudioHostServicesProvider _vsHostWorkspaceServicesProvider = vsHostWorkspaceServicesProvider ?? throw new ArgumentNullException(nameof(vsHostWorkspaceServicesProvider)); + private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider ?? throw new ArgumentNullException(nameof(vsHostServicesProvider)); private readonly ILoggerFactory _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); private readonly RazorLogHubTraceProvider _traceProvider = traceProvider ?? throw new ArgumentNullException(nameof(traceProvider)); @@ -180,9 +180,9 @@ private async Task EnsureContainedLanguageServersInitializedAsync() private void ConfigureLanguageServer(IServiceCollection serviceCollection) { - if (_vsHostWorkspaceServicesProvider is not null) + if (_vsHostServicesProvider is not null) { - serviceCollection.AddSingleton(new HostServicesProviderAdapter(_vsHostWorkspaceServicesProvider)); + serviceCollection.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); } } From d7137c74c456e3a3378568535135c4e51e424ca4 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 10:23:40 -0700 Subject: [PATCH 02/45] Remove unneeded null arg checks --- .../RazorLanguageServerClient.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index ce6c3de7276..ce3a70cf16c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -48,19 +48,19 @@ internal class RazorLanguageServerClient( VisualStudioHostServicesProvider vsHostServicesProvider) : ILanguageClient, ILanguageClientCustomMessage2, ILanguageClientPriority { - private readonly ILanguageClientBroker _languageClientBroker = languageClientBroker ?? throw new ArgumentNullException(nameof(languageClientBroker)); - private readonly ILanguageServiceBroker2 _languageServiceBroker = languageServiceBroker ?? throw new ArgumentNullException(nameof(languageServiceBroker)); - private readonly ITelemetryReporter _telemetryReporter = telemetryReporter ?? throw new ArgumentNullException(nameof(telemetryReporter)); - private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager ?? throw new ArgumentNullException(nameof(clientSettingsManager)); - private readonly ILspServerActivationTracker _lspServerActivationTracker = lspServerActivationTracker ?? throw new ArgumentNullException(nameof(lspServerActivationTracker)); - private readonly RazorCustomMessageTarget _customMessageTarget = customTarget ?? throw new ArgumentNullException(nameof(customTarget)); - private readonly LSPRequestInvoker _requestInvoker = requestInvoker ?? throw new ArgumentNullException(nameof(requestInvoker)); - private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore ?? throw new ArgumentNullException(nameof(projectConfigurationFilePathStore)); - private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher ?? throw new ArgumentNullException(nameof(projectInfoEndpointPublisher)); - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider ?? throw new ArgumentNullException(nameof(vsHostServicesProvider)); - private readonly ILoggerFactory _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - private readonly RazorLogHubTraceProvider _traceProvider = traceProvider ?? throw new ArgumentNullException(nameof(traceProvider)); + private readonly ILanguageClientBroker _languageClientBroker = languageClientBroker; + private readonly ILanguageServiceBroker2 _languageServiceBroker = languageServiceBroker; + private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; + private readonly ILspServerActivationTracker _lspServerActivationTracker = lspServerActivationTracker; + private readonly RazorCustomMessageTarget _customMessageTarget = customTarget; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore; + private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher; + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; + private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider; + private readonly ILoggerFactory _loggerFactory = loggerFactory; + private readonly RazorLogHubTraceProvider _traceProvider = traceProvider; private RazorLanguageServerHost? _host; From 01ff3d202c9f610b67c2d4efd9234f4e9f7e2503 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 10:43:53 -0700 Subject: [PATCH 03/45] Rename RazorLanguageServerHost.Create parameter --- .../LanguageServer/RazorLanguageServerBenchmarkBase.cs | 2 +- .../Hosting/RazorLanguageServerHost.cs | 4 ++-- .../RazorLanguageServer.cs | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorLanguageServerBenchmarkBase.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorLanguageServerBenchmarkBase.cs index a156be288ea..3759705f138 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorLanguageServerBenchmarkBase.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorLanguageServerBenchmarkBase.cs @@ -35,7 +35,7 @@ public RazorLanguageServerBenchmarkBase() serverStream, razorLoggerFactory, NoOpTelemetryReporter.Instance, - configure: (collection) => + configureServices: (collection) => { collection.AddSingleton(); collection.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs index ce2cfb29766..b9cdcb3a0a3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs @@ -42,7 +42,7 @@ public static RazorLanguageServerHost Create( Stream output, ILoggerFactory loggerFactory, ITelemetryReporter telemetryReporter, - Action? configure = null, + Action? configureServices = null, LanguageServerFeatureOptions? featureOptions = null, RazorLSPOptions? razorLSPOptions = null, ILspServerActivationTracker? lspServerActivationTracker = null, @@ -61,7 +61,7 @@ public static RazorLanguageServerHost Create( jsonSerializer, loggerFactory, featureOptions, - configure, + configureServices, razorLSPOptions, lspServerActivationTracker, telemetryReporter); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 30505e0f54b..dd6f112dd71 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -40,7 +40,7 @@ internal partial class RazorLanguageServer : NewtonsoftLanguageServer? _configureServer; + private readonly Action? _configureServices; private readonly RazorLSPOptions _lspOptions; private readonly ILspServerActivationTracker? _lspServerActivationTracker; private readonly ITelemetryReporter _telemetryReporter; @@ -53,7 +53,7 @@ public RazorLanguageServer( JsonSerializer serializer, ILoggerFactory loggerFactory, LanguageServerFeatureOptions? featureOptions, - Action? configureServer, + Action? configureServices, RazorLSPOptions? lspOptions, ILspServerActivationTracker? lspServerActivationTracker, ITelemetryReporter telemetryReporter) @@ -62,7 +62,7 @@ public RazorLanguageServer( _jsonRpc = jsonRpc; _loggerFactory = loggerFactory; _featureOptions = featureOptions; - _configureServer = configureServer; + _configureServices = configureServices; _lspOptions = lspOptions ?? RazorLSPOptions.Default; _lspServerActivationTracker = lspServerActivationTracker; _telemetryReporter = telemetryReporter; @@ -104,9 +104,9 @@ protected override ILspServices ConstructLspServices() // Wrap the logger factory so that we can add [LSP] to the start of all the categories services.AddSingleton(loggerFactoryWrapper); - if (_configureServer is not null) + if (_configureServices is not null) { - _configureServer(services); + _configureServices(services); } services.AddSingleton(_clientConnection); From e8e85ebd1547dfad5cb8c9654f1a427a1d569397 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 11:14:09 -0700 Subject: [PATCH 04/45] Add new RazorProjectInfoPublisher implementation --- .../IRazorProjectInfoListener.cs | 12 ++++ .../IRazorProjectInfoPublisher.cs | 9 +++ .../RazorProjectInfoManager.Comparer.cs | 38 ++++++++++ .../RazorProjectInfoPublisher.cs | 69 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs new file mode 100644 index 00000000000..4cda18ecbcb --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; + +internal interface IRazorProjectInfoListener +{ + ValueTask UpdatedAsync(RazorProjectInfo projectInfo); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs new file mode 100644 index 00000000000..fb0f58253d3 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; + +internal interface IRazorProjectInfoPublisher +{ + void AddListener(IRazorProjectInfoListener listener); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs new file mode 100644 index 00000000000..ac480270b22 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; + +internal abstract partial class RazorProjectInfoPublisher +{ + private sealed class Comparer : IEqualityComparer + { + public static readonly Comparer Instance = new(); + + private Comparer() + { + } + + public bool Equals(RazorProjectInfo? x, RazorProjectInfo? y) + { + if (x is null) + { + return y is null; + } + else if (y is null) + { + return false; + } + + return x.ProjectKey.Equals(y.ProjectKey); + } + + public int GetHashCode(RazorProjectInfo obj) + { + return obj.ProjectKey.GetHashCode(); + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs new file mode 100644 index 00000000000..b244eff86a4 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Utilities; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; + +internal abstract partial class RazorProjectInfoPublisher : IRazorProjectInfoPublisher, IDisposable +{ + private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); + + private readonly CancellationTokenSource _disposeTokenSource; + private readonly AsyncBatchingWorkQueue _workQueue; + private ImmutableArray _listeners; + + protected RazorProjectInfoPublisher() + : this(s_delay) + { + } + + protected RazorProjectInfoPublisher(TimeSpan delay) + { + _disposeTokenSource = new(); + _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); + _listeners = []; + } + + public void Dispose() + { + _disposeTokenSource.Cancel(); + _disposeTokenSource.Dispose(); + } + + private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) + { + foreach (var projectInfo in items.GetMostRecentUniqueItems(Comparer.Instance)) + { + if (token.IsCancellationRequested) + { + return; + } + + foreach (var listener in _listeners) + { + if (token.IsCancellationRequested) + { + return; + } + + await listener.UpdatedAsync(projectInfo).ConfigureAwait(false); + } + } + } + + protected void AddWork(RazorProjectInfo projectInfo) + { + _workQueue.AddWork(projectInfo); + } + + void IRazorProjectInfoPublisher.AddListener(IRazorProjectInfoListener listener) + { + ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); + } +} From 02c1f553d5b89c70e23589395c1c869e2d145f5c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 11:39:49 -0700 Subject: [PATCH 05/45] Add MEF exported VS RazorProjectInfoManager --- .../VisualStudioRazorProjectInfoPublisher.cs | 62 +++++++++++++++++++ .../RazorLanguageServerClient.cs | 1 + 2 files changed, 63 insertions(+) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs new file mode 100644 index 00000000000..804c60273c2 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.Composition; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; + +[Export(typeof(IRazorStartupService)] +[Export(typeof(IRazorProjectInfoPublisher))] +internal sealed class VisualStudioRazorProjectInfoPublisher : CodeAnalysis.Razor.ProjectSystem.RazorProjectInfoPublisher, IRazorStartupService +{ + [ImportingConstructor] + public VisualStudioRazorProjectInfoPublisher(IProjectSnapshotManager projectManager) + : base() + { + projectManager.Changed += ProjectManager_Changed; + } + + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) + { + // Don't do any work if the solution is closing + if (e.SolutionIsClosing) + { + return; + } + + switch (e.Kind) + { + case ProjectChangeKind.ProjectAdded: + case ProjectChangeKind.ProjectChanged: + case ProjectChangeKind.DocumentRemoved: + case ProjectChangeKind.DocumentAdded: + var newer = e.Newer.AssumeNotNull(); + AddWork(newer.ToRazorProjectInfo()); + break; + + case ProjectChangeKind.ProjectRemoved: + var older = e.Older.AssumeNotNull(); + AddWork(new RazorProjectInfo( + older.Key, + older.FilePath, + configuration: FallbackRazorConfiguration.Latest, + rootNamespace: null, + displayName: "", + projectWorkspaceState: ProjectWorkspaceState.Default, + documents: [])); + + break; + + case ProjectChangeKind.DocumentChanged: + break; + + default: + throw new NotSupportedException($"Unsupported {nameof(ProjectChangeKind)}: {e.Kind}"); + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index ce3a70cf16c..30d9f389477 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -37,6 +37,7 @@ internal class RazorLanguageServerClient( LSPRequestInvoker requestInvoker, ProjectConfigurationFilePathStore projectConfigurationFilePathStore, RazorProjectInfoEndpointPublisher projectInfoEndpointPublisher, + IRazorProjectInfoPublisher projectInfoManager, ILoggerFactory loggerFactory, RazorLogHubTraceProvider traceProvider, LanguageServerFeatureOptions languageServerFeatureOptions, From 397bdf6eabc0adfaf340e9c9da92f89e5a8d39c3 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 12:56:59 -0700 Subject: [PATCH 06/45] Add RazorProjectInfoListener in language server and update project service --- .../ProjectSystem/RazorProjectInfoListener.cs | 68 ++++++++++++++++++ .../RazorLanguageServer.cs | 13 ++++ .../IRazorProjectInfoListener.cs | 4 +- .../IRazorProjectInfoPublisher.cs | 5 ++ ... => RazorProjectInfoPublisher.Comparer.cs} | 9 ++- .../RazorProjectInfoPublisher.cs | 71 +++++++++++++++++-- .../VisualStudioRazorProjectInfoPublisher.cs | 22 +++--- .../RazorLanguageServerClient.cs | 13 ++-- 8 files changed, 171 insertions(+), 34 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/{RazorProjectInfoManager.Comparer.cs => RazorProjectInfoPublisher.Comparer.cs} (70%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs new file mode 100644 index 00000000000..21923196451 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; + +internal sealed class RazorProjectInfoListener( + IRazorProjectService projectService, + IRazorProjectInfoPublisher publisher) + : IRazorProjectInfoListener, IOnInitialized +{ + private readonly IRazorProjectService _projectService = projectService; + private readonly IRazorProjectInfoPublisher _publisher = publisher; + + public async Task OnInitializedAsync(ILspServices services, CancellationToken cancellationToken) + { + _publisher.AddListener(this); + + // Add all existing projects + foreach (var projectInfo in _publisher.GetLatestProjects()) + { + await AddOrUpdateProjectAsync(projectInfo, cancellationToken).ConfigureAwait(false); + } + } + + public async ValueTask RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken) + { + await _projectService + .UpdateProjectAsync( + projectKey, + configuration: null, + rootNamespace: null, + displayName: "", + ProjectWorkspaceState.Default, + documents: [], + cancellationToken) + .ConfigureAwait(false); + } + + public async ValueTask UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) + { + await AddOrUpdateProjectAsync(projectInfo, cancellationToken).ConfigureAwait(false); + } + + private Task AddOrUpdateProjectAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) + { + return _projectService.AddOrUpdateProjectAsync( + projectInfo.ProjectKey, + projectInfo.FilePath, + projectInfo.Configuration, + projectInfo.RootNamespace, + projectInfo.DisplayName, + projectInfo.ProjectWorkspaceState, + projectInfo.Documents, + cancellationToken); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index dd6f112dd71..36904ba4000 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.DependencyInjection; @@ -109,6 +110,18 @@ protected override ILspServices ConstructLspServices() _configureServices(services); } + // TODO: Remove this + foreach (var service in services) + { + // Only register RazorProjectInfoListener if an IRazorProjectInfoPublisher was registered. + if (service.ServiceType == typeof(IRazorProjectInfoPublisher)) + { + services.AddSingleton(); + services.AddSingleton((services) => (IOnInitialized)services.GetRequiredService()); + break; + } + } + services.AddSingleton(_clientConnection); // Add the logger as a service in case anything in CLaSP pulls it out to do logging diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs index 4cda18ecbcb..3cb3c7b2f84 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.ProjectSystem; @@ -8,5 +9,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal interface IRazorProjectInfoListener { - ValueTask UpdatedAsync(RazorProjectInfo projectInfo); + ValueTask RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken); + ValueTask UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs index fb0f58253d3..b0617443e41 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs @@ -1,9 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.ProjectSystem; + namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal interface IRazorProjectInfoPublisher { + ImmutableArray GetLatestProjects(); + void AddListener(IRazorProjectInfoListener listener); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs similarity index 70% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs index ac480270b22..faf496ace86 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoManager.Comparer.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs @@ -2,13 +2,12 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal abstract partial class RazorProjectInfoPublisher { - private sealed class Comparer : IEqualityComparer + private sealed class Comparer : IEqualityComparer { public static readonly Comparer Instance = new(); @@ -16,7 +15,7 @@ private Comparer() { } - public bool Equals(RazorProjectInfo? x, RazorProjectInfo? y) + public bool Equals(Work? x, Work? y) { if (x is null) { @@ -30,9 +29,9 @@ public bool Equals(RazorProjectInfo? x, RazorProjectInfo? y) return x.ProjectKey.Equals(y.ProjectKey); } - public int GetHashCode(RazorProjectInfo obj) + public int GetHashCode(Work work) { - return obj.ProjectKey.GetHashCode(); + return work.ProjectKey.GetHashCode(); } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs index b244eff86a4..3dc56ab4df2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs @@ -2,9 +2,12 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Utilities; @@ -12,10 +15,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal abstract partial class RazorProjectInfoPublisher : IRazorProjectInfoPublisher, IDisposable { + private abstract record Work(ProjectKey ProjectKey); + private sealed record Update(RazorProjectInfo ProjectInfo) : Work(ProjectInfo.ProjectKey); + private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); + private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); private readonly CancellationTokenSource _disposeTokenSource; - private readonly AsyncBatchingWorkQueue _workQueue; + private readonly AsyncBatchingWorkQueue _workQueue; + + private readonly Dictionary _latestProjectInfo; private ImmutableArray _listeners; protected RazorProjectInfoPublisher() @@ -26,7 +35,8 @@ protected RazorProjectInfoPublisher() protected RazorProjectInfoPublisher(TimeSpan delay) { _disposeTokenSource = new(); - _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); + _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); + _latestProjectInfo = []; _listeners = []; } @@ -36,15 +46,33 @@ public void Dispose() _disposeTokenSource.Dispose(); } - private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) + private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) { - foreach (var projectInfo in items.GetMostRecentUniqueItems(Comparer.Instance)) + foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance)) { if (token.IsCancellationRequested) { return; } + lock (_latestProjectInfo) + { + switch (work) + { + case Update(var projectInfo): + _latestProjectInfo[projectInfo.ProjectKey] = projectInfo; + break; + + case Remove(var projectKey): + _latestProjectInfo.Remove(projectKey); + break; + + default: + Assumed.Unreachable(); + break; + } + } + foreach (var listener in _listeners) { if (token.IsCancellationRequested) @@ -52,14 +80,43 @@ private async ValueTask ProcessBatchAsync(ImmutableArray items return; } - await listener.UpdatedAsync(projectInfo).ConfigureAwait(false); + switch (work) + { + case Update(var projectInfo): + await listener.UpdatedAsync(projectInfo, token).ConfigureAwait(false); + break; + + case Remove(var projectKey): + await listener.RemovedAsync(projectKey, token).ConfigureAwait(false); + break; + } } } } - protected void AddWork(RazorProjectInfo projectInfo) + protected void EnqueueUpdate(RazorProjectInfo projectInfo) { - _workQueue.AddWork(projectInfo); + _workQueue.AddWork(new Update(projectInfo)); + } + + protected void EnqueueRemove(ProjectKey projectKey) + { + _workQueue.AddWork(new Remove(projectKey)); + } + + ImmutableArray IRazorProjectInfoPublisher.GetLatestProjects() + { + lock (_latestProjectInfo) + { + using var builder = new PooledArrayBuilder(capacity: _latestProjectInfo.Count); + + foreach (var (_, projectInfo) in _latestProjectInfo) + { + builder.Add(projectInfo); + } + + return builder.DrainToImmutable(); + } } void IRazorProjectInfoPublisher.AddListener(IRazorProjectInfoListener listener) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs index 804c60273c2..b035d8c88bc 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs @@ -4,20 +4,22 @@ using System; using System.ComponentModel.Composition; using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -[Export(typeof(IRazorStartupService)] [Export(typeof(IRazorProjectInfoPublisher))] -internal sealed class VisualStudioRazorProjectInfoPublisher : CodeAnalysis.Razor.ProjectSystem.RazorProjectInfoPublisher, IRazorStartupService +internal sealed class VisualStudioRazorProjectInfoPublisher : CodeAnalysis.Razor.ProjectSystem.RazorProjectInfoPublisher { [ImportingConstructor] public VisualStudioRazorProjectInfoPublisher(IProjectSnapshotManager projectManager) : base() { + foreach (var project in projectManager.GetProjects()) + { + EnqueueUpdate(project.ToRazorProjectInfo()); + } + projectManager.Changed += ProjectManager_Changed; } @@ -36,20 +38,12 @@ private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) case ProjectChangeKind.DocumentRemoved: case ProjectChangeKind.DocumentAdded: var newer = e.Newer.AssumeNotNull(); - AddWork(newer.ToRazorProjectInfo()); + EnqueueUpdate(newer.ToRazorProjectInfo()); break; case ProjectChangeKind.ProjectRemoved: var older = e.Older.AssumeNotNull(); - AddWork(new RazorProjectInfo( - older.Key, - older.FilePath, - configuration: FallbackRazorConfiguration.Latest, - rootNamespace: null, - displayName: "", - projectWorkspaceState: ProjectWorkspaceState.Default, - documents: [])); - + EnqueueRemove(older.Key); break; case ProjectChangeKind.DocumentChanged: diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index 30d9f389477..f261b4320dd 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -37,7 +37,7 @@ internal class RazorLanguageServerClient( LSPRequestInvoker requestInvoker, ProjectConfigurationFilePathStore projectConfigurationFilePathStore, RazorProjectInfoEndpointPublisher projectInfoEndpointPublisher, - IRazorProjectInfoPublisher projectInfoManager, + IRazorProjectInfoPublisher projectInfoPublisher, ILoggerFactory loggerFactory, RazorLogHubTraceProvider traceProvider, LanguageServerFeatureOptions languageServerFeatureOptions, @@ -58,6 +58,7 @@ internal class RazorLanguageServerClient( private readonly LSPRequestInvoker _requestInvoker = requestInvoker; private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore; private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher; + private readonly IRazorProjectInfoPublisher _projectInfoPublisher = projectInfoPublisher; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider; private readonly ILoggerFactory _loggerFactory = loggerFactory; @@ -109,7 +110,7 @@ public event AsyncEventHandler? StopAsync serverStream, _loggerFactory, _telemetryReporter, - ConfigureLanguageServer, + ConfigureServices, _languageServerFeatureOptions, lspOptions, _lspServerActivationTracker, @@ -179,12 +180,10 @@ private async Task EnsureContainedLanguageServersInitializedAsync() _lspServerActivationTracker.Activated(); } - private void ConfigureLanguageServer(IServiceCollection serviceCollection) + private void ConfigureServices(IServiceCollection serviceCollection) { - if (_vsHostServicesProvider is not null) - { - serviceCollection.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); - } + serviceCollection.AddSingleton(_projectInfoPublisher); + serviceCollection.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); } private async Task EnsureCleanedUpServerAsync() From c447e69a9a89cd175a5247b97e3c1f3bbb09d1ae Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 14:08:30 -0700 Subject: [PATCH 07/45] Stop updating ProjectConfigurationFileStore --- .../VisualStudioRazorProjectInfoPublisher.cs | 27 ++++++++- .../RazorLanguageServerClient.cs | 58 ------------------ .../DefaultWindowsRazorProjectHost.cs | 6 +- .../ProjectSystem/FallbackProjectManager.cs | 6 -- .../FallbackWindowsRazorProjectHost.cs | 15 +---- .../WindowsRazorProjectHostBase.cs | 6 +- .../DefaultWindowsRazorProjectHostTest.cs | 34 ++++------- .../FallbackProjectManagerTest.cs | 1 - .../FallbackWindowsRazorProjectHostTest.cs | 59 ++++++++----------- .../RazorDynamicFileInfoProviderTest.cs | 1 - 10 files changed, 65 insertions(+), 148 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs index b035d8c88bc..c6fb97a0839 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs @@ -5,6 +5,8 @@ using System.ComponentModel.Composition; using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; @@ -12,15 +14,34 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; internal sealed class VisualStudioRazorProjectInfoPublisher : CodeAnalysis.Razor.ProjectSystem.RazorProjectInfoPublisher { [ImportingConstructor] - public VisualStudioRazorProjectInfoPublisher(IProjectSnapshotManager projectManager) + public VisualStudioRazorProjectInfoPublisher( + JoinableTaskContext joinableTaskContext, + LSPEditorFeatureDetector lspEditorFeatureDetector, + IProjectSnapshotManager projectManager) : base() { + var jtf = joinableTaskContext.Factory; + + _ = jtf.RunAsync(async () => + { + // Switch to the main thread because IsLSPEditorAvailable() expects to. + await jtf.SwitchToMainThreadAsync(); + + if (lspEditorFeatureDetector.IsLSPEditorAvailable()) + { + Initialize(projectManager); + } + }); + } + + private void Initialize(IProjectSnapshotManager projectManager) + { + projectManager.Changed += ProjectManager_Changed; + foreach (var project in projectManager.GetProjects()) { EnqueueUpdate(project.ToRazorProjectInfo()); } - - projectManager.Changed += ProjectManager_Changed; } private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index f261b4320dd..a2e86f4a13e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -12,9 +12,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; @@ -35,7 +33,6 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient; internal class RazorLanguageServerClient( RazorCustomMessageTarget customTarget, LSPRequestInvoker requestInvoker, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore, RazorProjectInfoEndpointPublisher projectInfoEndpointPublisher, IRazorProjectInfoPublisher projectInfoPublisher, ILoggerFactory loggerFactory, @@ -56,7 +53,6 @@ internal class RazorLanguageServerClient( private readonly ILspServerActivationTracker _lspServerActivationTracker = lspServerActivationTracker; private readonly RazorCustomMessageTarget _customMessageTarget = customTarget; private readonly LSPRequestInvoker _requestInvoker = requestInvoker; - private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore; private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher; private readonly IRazorProjectInfoPublisher _projectInfoPublisher = projectInfoPublisher; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; @@ -196,54 +192,11 @@ private async Task EnsureCleanedUpServerAsync() if (_host is not null) { - _projectConfigurationFilePathStore.Changed -= ProjectConfigurationFilePathStore_Changed; // Server still hasn't shutdown, wait for it to shutdown await _host.WaitForExitAsync().ConfigureAwait(false); } } - private void ProjectConfigurationFilePathStore_Changed(object sender, ProjectConfigurationFilePathChangedEventArgs args) - { - _ = ProjectConfigurationFilePathStore_ChangedAsync(args, CancellationToken.None); - } - - private async Task ProjectConfigurationFilePathStore_ChangedAsync(ProjectConfigurationFilePathChangedEventArgs args, CancellationToken cancellationToken) - { - if (_languageServerFeatureOptions.DisableRazorLanguageServer || _languageServerFeatureOptions.UseProjectConfigurationEndpoint) - { - return; - } - - try - { - var parameter = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = args.ProjectKey.Id, - ConfigurationFilePath = args.ConfigurationFilePath, - }; - - await _requestInvoker.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorMonitorProjectConfigurationFilePathEndpoint, - RazorLSPConstants.RazorLanguageServerName, - parameter, - cancellationToken); - } - catch (Exception) - { - // We're fire and forgetting here, if the request fails we're ok with that. - // - // Note: When moving between solutions this can fail with a null reference exception because the underlying LSP platform's - // JsonRpc object will be `null`. This can happen in two situations: - // 1. There's currently a race in the platform on shutting down/activating so we don't get the opportunity to properly detach - // from the configuration file path store changed event properly. - // Tracked by: https://github.com/dotnet/aspnetcore/issues/23819 - // 2. The LSP platform failed to shutdown our language server properly due to a JsonRpc timeout. There's currently a limitation in - // the LSP platform APIs where we don't know if the LSP platform requested shutdown but our language server never saw it. Therefore, - // we will null-ref until our language server client boot-logic kicks back in and re-activates resulting in the old server being - // being cleaned up. - } - } - public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask; public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) @@ -282,17 +235,6 @@ private void ServerStarted() { _projectInfoEndpointPublisher.StartSending(); } - else - { - _projectConfigurationFilePathStore.Changed += ProjectConfigurationFilePathStore_Changed; - - var mappings = _projectConfigurationFilePathStore.GetMappings(); - foreach (var mapping in mappings) - { - var args = new ProjectConfigurationFilePathChangedEventArgs(mapping.Key, mapping.Value); - ProjectConfigurationFilePathStore_Changed(this, args); - } - } } private sealed class HostServicesProviderAdapter(VisualStudioHostServicesProvider vsHostServicesProvider) : IHostServicesProvider diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWindowsRazorProjectHost.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWindowsRazorProjectHost.cs index bae9b601a3a..d3514dc58de 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWindowsRazorProjectHost.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWindowsRazorProjectHost.cs @@ -47,9 +47,8 @@ public DefaultWindowsRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, IProjectSnapshotManager projectManager, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore, LanguageServerFeatureOptions languageServerFeatureOptions) - : base(commonServices, serviceProvider, projectManager, projectConfigurationFilePathStore) + : base(commonServices, serviceProvider, projectManager) { _languageServerFeatureOptions = languageServerFeatureOptions; } @@ -98,9 +97,6 @@ await UpdateAsync( var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, intermediatePath, configuration, rootNamespace, displayName); - var projectConfigurationFile = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName); - ProjectConfigurationFilePathStore.Set(hostProject.Key, projectConfigurationFile); - UpdateProject(updater, hostProject); for (var i = 0; i < changedDocuments.Length; i++) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackProjectManager.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackProjectManager.cs index 16d690de406..09130e8cb44 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackProjectManager.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackProjectManager.cs @@ -26,14 +26,12 @@ namespace Microsoft.VisualStudio.Razor.ProjectSystem; [method: ImportingConstructor] internal sealed class FallbackProjectManager( [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore, LanguageServerFeatureOptions languageServerFeatureOptions, IProjectSnapshotManager projectManager, IWorkspaceProvider workspaceProvider, ITelemetryReporter telemetryReporter) { private readonly IServiceProvider _serviceProvider = serviceProvider; - private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly IProjectSnapshotManager _projectManager = projectManager; private readonly IWorkspaceProvider _workspaceProvider = workspaceProvider; @@ -123,10 +121,6 @@ private void AddFallbackProject(ProjectId projectId, string filePath, Cancellati cancellationToken); AddFallbackDocument(hostProject.Key, filePath, project.FilePath, cancellationToken); - - var configurationFilePath = Path.Combine(intermediateOutputPath, _languageServerFeatureOptions.ProjectConfigurationFileName); - - _projectConfigurationFilePathStore.Set(hostProject.Key, configurationFilePath); } private void AddFallbackDocument(ProjectKey projectKey, string filePath, string projectFilePath, CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackWindowsRazorProjectHost.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackWindowsRazorProjectHost.cs index 2f23389aeb9..229221b53e4 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackWindowsRazorProjectHost.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackWindowsRazorProjectHost.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.Shell; using ContentItem = Microsoft.VisualStudio.Razor.ProjectSystem.ManagedProjectSystemSchema.ContentItem; @@ -40,18 +39,14 @@ internal class FallbackWindowsRazorProjectHost : WindowsRazorProjectHostBase NoneItem.SchemaName, ConfigurationGeneralSchemaName, }); - private readonly LanguageServerFeatureOptions? _languageServerFeatureOptions; [ImportingConstructor] public FallbackWindowsRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - IProjectSnapshotManager projectManager, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore, - LanguageServerFeatureOptions? languageServerFeatureOptions) - : base(commonServices, serviceProvider, projectManager, projectConfigurationFilePathStore) + IProjectSnapshotManager projectManager) + : base(commonServices, serviceProvider, projectManager) { - _languageServerFeatureOptions = languageServerFeatureOptions; } protected override ImmutableHashSet GetRuleNames() => s_ruleNames; @@ -148,12 +143,6 @@ await UpdateAsync(updater => var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, intermediatePath, configuration, rootNamespace: null, displayName); - if (_languageServerFeatureOptions is not null) - { - var projectConfigurationFile = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName); - ProjectConfigurationFilePathStore.Set(hostProject.Key, projectConfigurationFile); - } - UpdateProject(updater, hostProject); for (var i = 0; i < changedDocuments.Length; i++) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/WindowsRazorProjectHostBase.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/WindowsRazorProjectHostBase.cs index 60628fba5c5..9c1f525174d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/WindowsRazorProjectHostBase.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/WindowsRazorProjectHostBase.cs @@ -32,7 +32,6 @@ internal abstract partial class WindowsRazorProjectHostBase : OnceInitializedOnc private readonly Dictionary _projectSubscriptions = new(); private readonly List _disposables = new(); - protected readonly ProjectConfigurationFilePathStore ProjectConfigurationFilePathStore; internal const string BaseIntermediateOutputPathPropertyName = "BaseIntermediateOutputPath"; internal const string IntermediateOutputPathPropertyName = "IntermediateOutputPath"; @@ -47,8 +46,7 @@ internal abstract partial class WindowsRazorProjectHostBase : OnceInitializedOnc protected WindowsRazorProjectHostBase( IUnconfiguredProjectCommonServices commonServices, IServiceProvider serviceProvider, - IProjectSnapshotManager projectManager, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore) + IProjectSnapshotManager projectManager) : base(commonServices.ThreadingService.JoinableTaskContext) { CommonServices = commonServices; @@ -56,7 +54,6 @@ protected WindowsRazorProjectHostBase( _projectManager = projectManager; _lock = new AsyncSemaphore(initialCount: 1); - ProjectConfigurationFilePathStore = projectConfigurationFilePathStore; } protected abstract ImmutableHashSet GetRuleNames(); @@ -286,7 +283,6 @@ protected static void UpdateProject(ProjectSnapshotManager.Updater updater, Host protected void RemoveProject(ProjectSnapshotManager.Updater updater, ProjectKey projectKey) { updater.ProjectRemoved(projectKey); - ProjectConfigurationFilePathStore.Remove(projectKey); } private async Task ExecuteWithLockAsync(Func func) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultWindowsRazorProjectHostTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultWindowsRazorProjectHostTest.cs index 493237acc64..99ebb5d5cc9 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultWindowsRazorProjectHostTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultWindowsRazorProjectHostTest.cs @@ -10,15 +10,11 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.ProjectSystem.Properties; -using Moq; using Xunit; using Xunit.Abstractions; using Rules = Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules; @@ -30,7 +26,6 @@ public class DefaultWindowsRazorProjectHostTest : VisualStudioWorkspaceTestBase private readonly IServiceProvider _serviceProvider; private readonly ItemCollection _configurationItems; private readonly ItemCollection _extensionItems; - private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore; private readonly ItemCollection _razorComponentWithTargetPathItems; private readonly ItemCollection _razorGenerateWithTargetPathItems; private readonly PropertyCollection _razorGeneralProperties; @@ -49,11 +44,6 @@ public DefaultWindowsRazorProjectHostTest(ITestOutputHelper testOutput) _projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new Mock(MockBehavior.Strict); - projectConfigurationFilePathStore.Setup(s => s.Remove(It.IsAny())).Verifiable(); - projectConfigurationFilePathStore.Setup(s => s.Set(It.IsAny(), It.IsAny())).Verifiable(); - _projectConfigurationFilePathStore = projectConfigurationFilePathStore.Object; - _configurationItems = new ItemCollection(Rules.RazorConfiguration.SchemaName); _extensionItems = new ItemCollection(Rules.RazorExtension.SchemaName); _razorComponentWithTargetPathItems = new ItemCollection(Rules.RazorComponentWithTargetPath.SchemaName); @@ -634,7 +624,7 @@ public async Task DefaultRazorProjectHost_UIThread_CreateAndDispose_Succeeds() { // Arrange var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); // Act & Assert await host.LoadAsync(); @@ -649,7 +639,7 @@ public async Task DefaultRazorProjectHost_BackgroundThread_CreateAndDispose_Succ { // Arrange var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); // Act & Assert await Task.Run(async () => await host.LoadAsync()); @@ -668,7 +658,7 @@ public async Task DefaultRazorProjectHost_OnProjectChanged_NoRulesDefined() }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); // Act & Assert await Task.Run(async () => await host.LoadAsync()); @@ -712,7 +702,7 @@ public async Task OnProjectChanged_ReadsProperties_InitializesProject() var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); @@ -765,7 +755,7 @@ public async Task OnProjectChanged_ReadsProperties_InitializesProject() public void IntermediateOutputPathCalculationHandlesRelativePaths(string baseIntermediateOutputPath, string intermediateOutputPath, string expectedCombinedIOP) { var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); var state = TestProjectRuleSnapshot.CreateProperties( WindowsRazorProjectHostBase.ConfigurationGeneralSchemaName, @@ -808,7 +798,7 @@ public async Task OnProjectChanged_NoVersionFound_DoesNotInitializeProject() }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); await Task.Run(async () => await host.LoadAsync()); Assert.Empty(_projectManager.GetProjects()); @@ -859,7 +849,7 @@ public async Task OnProjectChanged_UpdateProject_MarksSolutionOpen() }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); @@ -913,7 +903,7 @@ public async Task OnProjectChanged_UpdateProject_Succeeds() }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); @@ -1069,7 +1059,7 @@ public async Task OnProjectChanged_VersionRemoved_DeInitializesProject() }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); @@ -1144,7 +1134,7 @@ public async Task OnProjectChanged_AfterDispose_IgnoresUpdate() }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); @@ -1224,7 +1214,7 @@ public async Task OnProjectRenamed_RemovesHostProject_CopiesConfiguration() var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); @@ -1321,7 +1311,7 @@ public async Task OnProjectChanged_ChangeIntermediateOutputPath_RemovesAndAddsPr }; var services = new TestProjectSystemServices(TestProjectData.SomeProject.FilePath); - var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); + var host = new DefaultWindowsRazorProjectHost(services, _serviceProvider, _projectManager, languageServerFeatureOptions: new TestLanguageServerFeatureOptions()); host.SkipIntermediateOutputPathExistCheck_TestOnly = true; await Task.Run(async () => await host.LoadAsync()); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs index aa083c0c6cc..4cc95be5adc 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs @@ -41,7 +41,6 @@ public FallbackProjectManagerTest(ITestOutputHelper testOutputHelper) _fallbackProjectManger = new FallbackProjectManager( serviceProvider, - _projectConfigurationFilePathStore, languageServerFeatureOptions, _projectManager, WorkspaceProvider, diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackWindowsRazorProjectHostTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackWindowsRazorProjectHostTest.cs index 4a068275ef5..171a3f96949 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackWindowsRazorProjectHostTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackWindowsRazorProjectHostTest.cs @@ -10,13 +10,10 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.ProjectSystem.Properties; -using Moq; using Xunit; using Xunit.Abstractions; using ItemReference = Microsoft.VisualStudio.Razor.ProjectSystem.ManagedProjectSystemSchema.ItemReference; @@ -28,7 +25,6 @@ public class FallbackWindowsRazorProjectHostTest : VisualStudioWorkspaceTestBase private readonly IServiceProvider _serviceProvider; private readonly ItemCollection _referenceItems; private readonly TestProjectSnapshotManager _projectManager; - private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore; private readonly ItemCollection _contentItems; private readonly ItemCollection _noneItems; @@ -44,10 +40,6 @@ public FallbackWindowsRazorProjectHostTest(ITestOutputHelper testOutput) _projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new Mock(MockBehavior.Strict); - projectConfigurationFilePathStore.Setup(s => s.Remove(It.IsAny())).Verifiable(); - _projectConfigurationFilePathStore = projectConfigurationFilePathStore.Object; - _referenceItems = new ItemCollection(ManagedProjectSystemSchema.ResolvedCompilationReference.SchemaName); _contentItems = new ItemCollection(ManagedProjectSystemSchema.ContentItem.SchemaName); _noneItems = new ItemCollection(ManagedProjectSystemSchema.NoneItem.SchemaName); @@ -71,7 +63,7 @@ public void GetChangedAndRemovedDocuments_ReturnsChangedContentAndNoneItems() }); var services = new TestProjectSystemServices("C:\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var changes = new TestProjectChangeDescription[] { afterChangeContentItems.ToChange(_contentItems.ToSnapshot()), @@ -113,11 +105,11 @@ public void GetCurrentDocuments_ReturnsContentAndNoneItems() }); var services = new TestProjectSystemServices("C:\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var changes = new TestProjectChangeDescription[] { - _contentItems.ToChange(), - _noneItems.ToChange(), + _contentItems.ToChange(), + _noneItems.ToChange(), }; var update = services.CreateUpdate(changes).Value; @@ -156,11 +148,11 @@ public void GetCurrentDocuments_IgnoresDotRazorFiles() }); var services = new TestProjectSystemServices("C:\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var changes = new TestProjectChangeDescription[] { - _contentItems.ToChange(), - _noneItems.ToChange(), + _contentItems.ToChange(), + _noneItems.ToChange(), }; var update = services.CreateUpdate(changes).Value; @@ -177,7 +169,7 @@ public void TryGetRazorDocument_NoFilePath_ReturnsFalse() // Arrange var services = new TestProjectSystemServices("C:\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var itemState = new Dictionary() { [ItemReference.LinkPropertyName] = "Index.cshtml", @@ -197,7 +189,7 @@ public void TryGetRazorDocument_NonRazorFilePath_ReturnsFalse() // Arrange var services = new TestProjectSystemServices("C:\\Path\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var itemState = new Dictionary() { [ItemReference.FullPathPropertyName] = "C:\\Path\\site.css", @@ -217,7 +209,7 @@ public void TryGetRazorDocument_NonRazorTargetPath_ReturnsFalse() // Arrange var services = new TestProjectSystemServices("C:\\Path\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var itemState = new Dictionary() { [ItemReference.LinkPropertyName] = "site.html", @@ -239,7 +231,7 @@ public void TryGetRazorDocument_JustFilePath_ReturnsTrue() var expectedPath = "C:\\Path\\Index.cshtml"; var services = new TestProjectSystemServices("C:\\Path\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var itemState = new Dictionary() { [ItemReference.FullPathPropertyName] = expectedPath, @@ -262,7 +254,7 @@ public void TryGetRazorDocument_LinkedFilepath_ReturnsTrue() var expectedTargetPath = "C:\\Path\\To\\Index.cshtml"; var services = new TestProjectSystemServices("C:\\Path\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var itemState = new Dictionary() { [ItemReference.LinkPropertyName] = "Index.cshtml", @@ -286,7 +278,7 @@ public void TryGetRazorDocument_SetsLegacyFileKind() var expectedTargetPath = "C:\\Path\\To\\Index.cshtml"; var services = new TestProjectSystemServices("C:\\Path\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); var itemState = new Dictionary() { [ItemReference.LinkPropertyName] = "Index.cshtml", @@ -309,7 +301,7 @@ public async Task FallbackRazorProjectHost_UIThread_CreateAndDispose_Succeeds() // Arrange var services = new TestProjectSystemServices("C:\\To\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); // Act & Assert await host.LoadAsync(); @@ -325,7 +317,7 @@ public async Task FallbackRazorProjectHost_BackgroundThread_CreateAndDispose_Suc // Arrange var services = new TestProjectSystemServices("Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); // Act & Assert await Task.Run(async () => await host.LoadAsync()); @@ -345,7 +337,7 @@ public async Task OnProjectChanged_NoRulesDefined() var services = new TestProjectSystemServices("Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore) + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager) { AssemblyVersion = new Version(2, 0), }; @@ -381,7 +373,7 @@ public async Task OnProjectChanged_ReadsProperties_InitializesProject() var services = new TestProjectSystemServices("C:\\Path\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore) + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager) { AssemblyVersion = new Version(2, 0), // Mock for reading the assembly's version }; @@ -416,7 +408,7 @@ public async Task OnProjectChanged_NoAssemblyFound_DoesNotInitializeProject() }; var services = new TestProjectSystemServices("Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); await Task.Run(async () => await host.LoadAsync()); Assert.Empty(_projectManager.GetProjects()); @@ -444,7 +436,7 @@ public async Task OnProjectChanged_AssemblyFoundButCannotReadVersion_DoesNotInit var services = new TestProjectSystemServices("Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore); + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager); await Task.Run(async () => await host.LoadAsync()); Assert.Empty(_projectManager.GetProjects()); @@ -483,7 +475,7 @@ public async Task OnProjectChanged_UpdateProject_Succeeds() var services = new TestProjectSystemServices("C:\\Path\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore) + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager) { AssemblyVersion = new Version(2, 0), }; @@ -528,7 +520,7 @@ public async Task OnProjectChanged_VersionRemoved_DeinitializesProject() var services = new TestProjectSystemServices("Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore) + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager) { AssemblyVersion = new Version(2, 0), }; @@ -573,7 +565,7 @@ public async Task OnProjectChanged_AfterDispose_IgnoresUpdate() var services = new TestProjectSystemServices("C:\\Path\\Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore) + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager) { AssemblyVersion = new Version(2, 0), }; @@ -618,7 +610,7 @@ public async Task OnProjectRenamed_RemovesHostProject_CopiesConfiguration() var services = new TestProjectSystemServices("Test.csproj"); - var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager, _projectConfigurationFilePathStore) + var host = new TestFallbackRazorProjectHost(services, _serviceProvider, _projectManager) { AssemblyVersion = new Version(2, 0), // Mock for reading the assembly's version }; @@ -652,9 +644,8 @@ private class TestFallbackRazorProjectHost : FallbackWindowsRazorProjectHost internal TestFallbackRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, IServiceProvider serviceProvider, - IProjectSnapshotManager projectManager, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore) - : base(commonServices, serviceProvider, projectManager, projectConfigurationFilePathStore, languageServerFeatureOptions: null) + IProjectSnapshotManager projectManager) + : base(commonServices, serviceProvider, projectManager) { SkipIntermediateOutputPathExistCheck_TestOnly = true; } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/RazorDynamicFileInfoProviderTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/RazorDynamicFileInfoProviderTest.cs index 9f69174c2dd..92963ea1b30 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/RazorDynamicFileInfoProviderTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/RazorDynamicFileInfoProviderTest.cs @@ -75,7 +75,6 @@ await _projectManager.UpdateAsync(updater => var fallbackProjectManager = new FallbackProjectManager( serviceProvider, - StrictMock.Of(), languageServerFeatureOptions, _projectManager, WorkspaceProvider, From 6735145d85778541d4f9100012ba19d3c8b1ef92 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 16:05:22 -0700 Subject: [PATCH 08/45] Clean up LanguageServerTestBase a bit --- ...egacyRazorCompletionResolveEndpointTest.cs | 5 +- .../RazorCompletionResolveEndpointTest.cs | 7 +-- .../ProtocolSerializer.cs | 21 +++++++++ .../LanguageServer/LanguageServerTestBase.cs | 47 ++++++------------- 4 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProtocolSerializer.cs diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LegacyRazorCompletionResolveEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LegacyRazorCompletionResolveEndpointTest.cs index e7f37e6b9c8..de733495687 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LegacyRazorCompletionResolveEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LegacyRazorCompletionResolveEndpointTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; @@ -247,10 +248,10 @@ private VSInternalCompletionList CreateLSPCompletionList(params RazorCompletionI private VSInternalCompletionItem ConvertToBridgedItem(CompletionItem completionItem) { var textWriter = new StringWriter(); - Serializer.Serialize(textWriter, completionItem); + ProtocolSerializer.Instance.Serialize(textWriter, completionItem); var stringBuilder = textWriter.GetStringBuilder(); var jsonReader = new JsonTextReader(new StringReader(stringBuilder.ToString())); - var bridgedItem = Serializer.Deserialize(jsonReader); + var bridgedItem = ProtocolSerializer.Instance.Deserialize(jsonReader); return bridgedItem; } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs index 3486482c726..932edc5b4d8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.VisualStudio.LanguageServer.Protocol; using Newtonsoft.Json; @@ -149,10 +150,10 @@ public async Task Handle_MergedCompletionListFindsProperCompletionList_Resolves( private VSInternalCompletionItem ConvertToBridgedItem(CompletionItem completionItem) { using var textWriter = new StringWriter(); - Serializer.Serialize(textWriter, completionItem); + ProtocolSerializer.Instance.Serialize(textWriter, completionItem); var stringBuilder = textWriter.GetStringBuilder(); using var jsonReader = new JsonTextReader(new StringReader(stringBuilder.ToString())); - var bridgedItem = Serializer.Deserialize(jsonReader); + var bridgedItem = ProtocolSerializer.Instance.Deserialize(jsonReader); return bridgedItem; } @@ -160,7 +161,7 @@ private class TestCompletionItemResolver : CompletionItemResolver { public override Task ResolveAsync( VSInternalCompletionItem item, - VSInternalCompletionList containingCompletionlist, + VSInternalCompletionList containingCompletionList, object originalRequestContext, VSInternalClientCapabilities clientCapabilities, CancellationToken cancellationToken) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProtocolSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProtocolSerializer.cs new file mode 100644 index 00000000000..5be7f54eb80 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProtocolSerializer.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Test; + +internal static class ProtocolSerializer +{ + public static JsonSerializer Instance { get; } = CreateSerializer(); + + private static JsonSerializer CreateSerializer() + { + var serializer = new JsonSerializer(); + serializer.AddVSInternalExtensionConverters(); + serializer.AddVSExtensionConverters(); + + return serializer; + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs index 2e71b84ae97..f8566ecb6af 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs @@ -22,31 +22,18 @@ using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; -using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; -using Newtonsoft.Json; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -public abstract class LanguageServerTestBase : ToolingTestBase +public abstract class LanguageServerTestBase(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { - private protected IRazorSpanMappingService SpanMappingService { get; } - private protected IFilePathService FilePathService { get; } + private ThrowingRazorSpanMappingService? _spanMappingService; + private LSPFilePathService? _filePathService; - protected JsonSerializer Serializer { get; } - - protected LanguageServerTestBase(ITestOutputHelper testOutput) - : base(testOutput) - { - SpanMappingService = new ThrowingRazorSpanMappingService(); - - Serializer = new JsonSerializer(); - Serializer.AddVSInternalExtensionConverters(); - Serializer.AddVSExtensionConverters(); - - FilePathService = new LSPFilePathService(TestLanguageServerFeatureOptions.Instance); - } + private protected IRazorSpanMappingService SpanMappingService => _spanMappingService ??= new(); + private protected IFilePathService FilePathService => _filePathService ??= new(TestLanguageServerFeatureOptions.Instance); private protected TestProjectSnapshotManager CreateProjectSnapshotManager() => CreateProjectSnapshotManager(ProjectEngineFactories.DefaultProvider); @@ -59,14 +46,10 @@ private protected TestProjectSnapshotManager CreateProjectSnapshotManager( DisposalToken, initializer: static updater => updater.ProjectAdded(MiscFilesHostProject.Instance)); - internal RazorRequestContext CreateRazorRequestContext(VersionedDocumentContext? documentContext, ILspServices? lspServices = null) - { - lspServices ??= new Mock(MockBehavior.Strict).Object; - - var requestContext = new RazorRequestContext(documentContext, lspServices, "lsp/method", uri: null); - - return requestContext; - } + private protected static RazorRequestContext CreateRazorRequestContext( + VersionedDocumentContext? documentContext, + ILspServices? lspServices = null) + => new(documentContext, lspServices ?? StrictMock.Of(), "lsp/method", uri: null); protected static RazorCodeDocument CreateCodeDocument(string text, ImmutableArray tagHelpers = default, string? filePath = null, string? rootNamespace = null) { @@ -104,22 +87,22 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web """, RazorSourceDocumentProperties.Create(importDocumentName, importDocumentName)); - var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, ImmutableArray.Create(defaultImportDocument), tagHelpers); + var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, [defaultImportDocument], tagHelpers); return codeDocument; } - internal static IDocumentContextFactory CreateDocumentContextFactory(Uri documentPath, string sourceText) + private protected static IDocumentContextFactory CreateDocumentContextFactory(Uri documentPath, string sourceText) { var codeDocument = CreateCodeDocument(sourceText); return CreateDocumentContextFactory(documentPath, codeDocument); } - internal static VersionedDocumentContext CreateDocumentContext(Uri documentPath, RazorCodeDocument codeDocument) + private protected static VersionedDocumentContext CreateDocumentContext(Uri documentPath, RazorCodeDocument codeDocument) { return TestDocumentContext.From(documentPath.GetAbsoluteOrUNCPath(), codeDocument, hostDocumentVersion: 1337); } - internal static IDocumentContextFactory CreateDocumentContextFactory( + private protected static IDocumentContextFactory CreateDocumentContextFactory( Uri documentPath, RazorCodeDocument codeDocument, bool documentFound = true) @@ -131,7 +114,7 @@ internal static IDocumentContextFactory CreateDocumentContextFactory( return documentContextFactory; } - internal static VersionedDocumentContext CreateDocumentContext(Uri uri, IDocumentSnapshot snapshot) + private protected static VersionedDocumentContext CreateDocumentContext(Uri uri, IDocumentSnapshot snapshot) { return new VersionedDocumentContext(uri, snapshot, projectContext: null, version: 0); } @@ -153,7 +136,7 @@ protected static TextLoader CreateTextLoader(string filePath, SourceText text) return mock.Object; } - internal static RazorLSPOptionsMonitor GetOptionsMonitor(bool enableFormatting = true, bool autoShowCompletion = true, bool autoListParams = true, bool formatOnType = true, bool autoInsertAttributeQuotes = true, bool colorBackground = false, bool codeBlockBraceOnNextLine = false, bool commitElementsWithSpace = true) + private protected static RazorLSPOptionsMonitor GetOptionsMonitor(bool enableFormatting = true, bool autoShowCompletion = true, bool autoListParams = true, bool formatOnType = true, bool autoInsertAttributeQuotes = true, bool colorBackground = false, bool codeBlockBraceOnNextLine = false, bool commitElementsWithSpace = true) { var configService = StrictMock.Of(); From b38dc0f2f945d392b53e07309307a9b4af27fc37 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 31 May 2024 16:10:40 -0700 Subject: [PATCH 09/45] Remove FallbackProjectManager ProjectConfigurationFileStore test --- .../FallbackProjectManagerTest.cs | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs index 4cc95be5adc..82845dea721 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs @@ -22,13 +22,11 @@ public class FallbackProjectManagerTest : VisualStudioWorkspaceTestBase { private readonly FallbackProjectManager _fallbackProjectManger; private readonly TestProjectSnapshotManager _projectManager; - private readonly TestProjectConfigurationFilePathStore _projectConfigurationFilePathStore; public FallbackProjectManagerTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { var languageServerFeatureOptions = TestLanguageServerFeatureOptions.Instance; - _projectConfigurationFilePathStore = new TestProjectConfigurationFilePathStore(); var serviceProvider = VsMocks.CreateServiceProvider(static b => b.AddComponentModel(static b => @@ -242,31 +240,6 @@ public async Task DynamicFileAdded_TrackedProject_IgnoresDocumentFromOutsideCone Assert.Equal(SomeProjectFile1.TargetPath, project.GetDocument(SomeProjectFile1.FilePath)!.TargetPath); } - [UIFact] - public async Task DynamicFileAdded_UnknownProject_SetsConfigurationFileStore() - { - var projectId = ProjectId.CreateNewId(); - var projectInfo = ProjectInfo.Create( - projectId, VersionStamp.Default, "DisplayName", "AssemblyName", LanguageNames.CSharp, filePath: SomeProject.FilePath) - .WithCompilationOutputInfo(new CompilationOutputInfo().WithAssemblyPath(Path.Combine(SomeProject.IntermediateOutputPath, "SomeProject.dll"))) - .WithDefaultNamespace("RootNamespace"); - - Workspace.TryApplyChanges(Workspace.CurrentSolution.AddProject(projectInfo)); - - _fallbackProjectManger.DynamicFileAdded( - projectId, - SomeProject.Key, - SomeProject.FilePath, - SomeProjectFile1.FilePath, - DisposalToken); - - await WaitForProjectManagerUpdatesAsync(); - - var kvp = Assert.Single(_projectConfigurationFilePathStore.GetMappings()); - Assert.Equal(SomeProject.Key, kvp.Key); - Assert.Equal(Path.Combine(SomeProject.IntermediateOutputPath, "project.razor.bin"), kvp.Value); - } - private Task WaitForProjectManagerUpdatesAsync() { // The FallbackProjectManager fires and forgets any updates to the project manager. From b0ce9805e6242d3c9d7807b756e7b7ec637997a2 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 5 Jun 2024 03:54:08 -0700 Subject: [PATCH 10/45] Merge publisher with base class and add tests --- .../ProjectSystem/RazorProjectInfoListener.cs | 2 +- .../IRazorProjectInfoPublisher.cs | 2 +- .../RazorProjectInfoPublisher.cs | 126 ------- ...udioRazorProjectInfoPublisher.Comparer.cs} | 5 +- ...oRazorProjectInfoPublisher.TestAccessor.cs | 22 ++ .../VisualStudioRazorProjectInfoPublisher.cs | 150 ++++++++- ...sualStudioRazorProjectInfoPublisherTest.cs | 311 ++++++++++++++++++ 7 files changed, 477 insertions(+), 141 deletions(-) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs rename src/Razor/src/{Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs => Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs} (85%) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs index 21923196451..9cfb27b541d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs @@ -28,7 +28,7 @@ public async Task OnInitializedAsync(ILspServices services, CancellationToken ca _publisher.AddListener(this); // Add all existing projects - foreach (var projectInfo in _publisher.GetLatestProjects()) + foreach (var projectInfo in _publisher.GetLatestProjectInfos()) { await AddOrUpdateProjectAsync(projectInfo, cancellationToken).ConfigureAwait(false); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs index b0617443e41..85c83586d37 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs @@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal interface IRazorProjectInfoPublisher { - ImmutableArray GetLatestProjects(); + ImmutableArray GetLatestProjectInfos(); void AddListener(IRazorProjectInfoListener listener); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs deleted file mode 100644 index 3dc56ab4df2..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Utilities; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; - -internal abstract partial class RazorProjectInfoPublisher : IRazorProjectInfoPublisher, IDisposable -{ - private abstract record Work(ProjectKey ProjectKey); - private sealed record Update(RazorProjectInfo ProjectInfo) : Work(ProjectInfo.ProjectKey); - private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); - - private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); - - private readonly CancellationTokenSource _disposeTokenSource; - private readonly AsyncBatchingWorkQueue _workQueue; - - private readonly Dictionary _latestProjectInfo; - private ImmutableArray _listeners; - - protected RazorProjectInfoPublisher() - : this(s_delay) - { - } - - protected RazorProjectInfoPublisher(TimeSpan delay) - { - _disposeTokenSource = new(); - _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); - _latestProjectInfo = []; - _listeners = []; - } - - public void Dispose() - { - _disposeTokenSource.Cancel(); - _disposeTokenSource.Dispose(); - } - - private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) - { - foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance)) - { - if (token.IsCancellationRequested) - { - return; - } - - lock (_latestProjectInfo) - { - switch (work) - { - case Update(var projectInfo): - _latestProjectInfo[projectInfo.ProjectKey] = projectInfo; - break; - - case Remove(var projectKey): - _latestProjectInfo.Remove(projectKey); - break; - - default: - Assumed.Unreachable(); - break; - } - } - - foreach (var listener in _listeners) - { - if (token.IsCancellationRequested) - { - return; - } - - switch (work) - { - case Update(var projectInfo): - await listener.UpdatedAsync(projectInfo, token).ConfigureAwait(false); - break; - - case Remove(var projectKey): - await listener.RemovedAsync(projectKey, token).ConfigureAwait(false); - break; - } - } - } - } - - protected void EnqueueUpdate(RazorProjectInfo projectInfo) - { - _workQueue.AddWork(new Update(projectInfo)); - } - - protected void EnqueueRemove(ProjectKey projectKey) - { - _workQueue.AddWork(new Remove(projectKey)); - } - - ImmutableArray IRazorProjectInfoPublisher.GetLatestProjects() - { - lock (_latestProjectInfo) - { - using var builder = new PooledArrayBuilder(capacity: _latestProjectInfo.Count); - - foreach (var (_, projectInfo) in _latestProjectInfo) - { - builder.Add(projectInfo); - } - - return builder.DrainToImmutable(); - } - } - - void IRazorProjectInfoPublisher.AddListener(IRazorProjectInfoListener listener) - { - ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs similarity index 85% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs index faf496ace86..2602ece695a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/RazorProjectInfoPublisher.Comparer.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; +namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -internal abstract partial class RazorProjectInfoPublisher +internal sealed partial class VisualStudioRazorProjectInfoPublisher { private sealed class Comparer : IEqualityComparer { @@ -34,4 +34,5 @@ public int GetHashCode(Work work) return work.ProjectKey.GetHashCode(); } } + } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs new file mode 100644 index 00000000000..c82cc6c609f --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; + +internal sealed partial class VisualStudioRazorProjectInfoPublisher +{ + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(VisualStudioRazorProjectInfoPublisher instance) + { + public async Task WaitForInitializeAsync() + { + await instance._initializeTask; + } + + public Task WaitUntilCurrentBatchCompletesAsync() + => instance._workQueue.WaitUntilCurrentBatchCompletesAsync(); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs index c6fb97a0839..7f47782e927 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs @@ -2,46 +2,96 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Utilities; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; [Export(typeof(IRazorProjectInfoPublisher))] -internal sealed class VisualStudioRazorProjectInfoPublisher : CodeAnalysis.Razor.ProjectSystem.RazorProjectInfoPublisher +internal sealed partial class VisualStudioRazorProjectInfoPublisher : IRazorProjectInfoPublisher, IDisposable { + private abstract record Work(ProjectKey ProjectKey); + private sealed record Update(RazorProjectInfo ProjectInfo) : Work(ProjectInfo.ProjectKey); + private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); + + private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); + + private readonly CancellationTokenSource _disposeTokenSource; + private readonly AsyncBatchingWorkQueue _workQueue; + + private readonly Dictionary _latestProjectInfo; + private ImmutableArray _listeners; + + private readonly JoinableTask _initializeTask; + [ImportingConstructor] public VisualStudioRazorProjectInfoPublisher( - JoinableTaskContext joinableTaskContext, + IProjectSnapshotManager projectManager, + LSPEditorFeatureDetector lspEditorFeatureDetector, + JoinableTaskContext joinableTaskContext) + : this(projectManager, lspEditorFeatureDetector, joinableTaskContext, s_delay) + { + } + + public VisualStudioRazorProjectInfoPublisher( + IProjectSnapshotManager projectManager, LSPEditorFeatureDetector lspEditorFeatureDetector, - IProjectSnapshotManager projectManager) - : base() + JoinableTaskContext joinableTaskContext, + TimeSpan delay) { + _disposeTokenSource = new(); + _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); + _latestProjectInfo = []; + _listeners = []; + var jtf = joinableTaskContext.Factory; - _ = jtf.RunAsync(async () => + _initializeTask = jtf.RunAsync(async () => { // Switch to the main thread because IsLSPEditorAvailable() expects to. await jtf.SwitchToMainThreadAsync(); + // Because this service is only consumed by the language server, we only initialize it + // when the LSP editor is available. if (lspEditorFeatureDetector.IsLSPEditorAvailable()) { - Initialize(projectManager); + await InitializeAsync(projectManager, _disposeTokenSource.Token); } }); + } - private void Initialize(IProjectSnapshotManager projectManager) + public void Dispose() { - projectManager.Changed += ProjectManager_Changed; + _disposeTokenSource.Cancel(); + _disposeTokenSource.Dispose(); + } - foreach (var project in projectManager.GetProjects()) + private async Task InitializeAsync(IProjectSnapshotManager projectManager, CancellationToken cancellationToken) + { + // Even though we aren't mutating the project snapshot manager, we call UpdateAsync(...) here to ensure + // that we run on its dispatcher. That ensures that no changes will code in while we are iterating the + // current set of projects. + await projectManager.UpdateAsync(updater => { - EnqueueUpdate(project.ToRazorProjectInfo()); - } + foreach (var project in updater.GetProjects()) + { + EnqueueUpdate(project.ToRazorProjectInfo()); + } + + projectManager.Changed += ProjectManager_Changed; + }, + cancellationToken); } private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) @@ -74,4 +124,82 @@ private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) throw new NotSupportedException($"Unsupported {nameof(ProjectChangeKind)}: {e.Kind}"); } } + + private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) + { + foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance)) + { + if (token.IsCancellationRequested) + { + return; + } + + lock (_latestProjectInfo) + { + switch (work) + { + case Update(var projectInfo): + _latestProjectInfo[projectInfo.ProjectKey] = projectInfo; + break; + + case Remove(var projectKey): + _latestProjectInfo.Remove(projectKey); + break; + + default: + Assumed.Unreachable(); + break; + } + } + + foreach (var listener in _listeners) + { + if (token.IsCancellationRequested) + { + return; + } + + switch (work) + { + case Update(var projectInfo): + await listener.UpdatedAsync(projectInfo, token).ConfigureAwait(false); + break; + + case Remove(var projectKey): + await listener.RemovedAsync(projectKey, token).ConfigureAwait(false); + break; + } + } + } + } + + private void EnqueueUpdate(RazorProjectInfo projectInfo) + { + _workQueue.AddWork(new Update(projectInfo)); + } + + private void EnqueueRemove(ProjectKey projectKey) + { + _workQueue.AddWork(new Remove(projectKey)); + } + + public ImmutableArray GetLatestProjectInfos() + { + lock (_latestProjectInfo) + { + using var builder = new PooledArrayBuilder(capacity: _latestProjectInfo.Count); + + foreach (var (_, projectInfo) in _latestProjectInfo) + { + builder.Add(projectInfo); + } + + return builder.DrainToImmutable(); + } + } + + public void AddListener(IRazorProjectInfoListener listener) + { + ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs new file mode 100644 index 00000000000..14b79a32e9a --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs @@ -0,0 +1,311 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; + +public class VisualStudioRazorProjectInfoPublisherTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) +{ + private static readonly HostProject s_hostProject1 = new( + projectFilePath: "C:/path/to/project1/project1.csproj", + intermediateOutputPath: "C:/path/to/project1/obj", + razorConfiguration: RazorConfiguration.Default, + rootNamespace: "TestNamespace"); + + private static readonly HostDocument s_hostDocument1 = new("C:/path/to/project1/file.razor", "file.razor"); + + private static readonly HostProject s_hostProject2 = new( + projectFilePath: "C:/path/to/project2/project2.csproj", + intermediateOutputPath: "C:/path/to/project2/obj", + razorConfiguration: RazorConfiguration.Default, + rootNamespace: "TestNamespace"); + + private static readonly HostDocument s_hostDocument2 = new("C:/path/to/project2/file.razor", "file.razor"); + + [UIFact] + public async Task PublishesExistingProjectsDuringInitialization() + { + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + + updater.ProjectAdded(s_hostProject2); + updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader("

Hello World

", s_hostDocument2.FilePath)); + }); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + // Sort projects by project key. + var latestProjects = publisher + .GetLatestProjectInfos() + .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); + + Assert.Equal(2, latestProjects.Length); + + var projectInfo1 = latestProjects[0]; + Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); + var document1 = Assert.Single(projectInfo1.Documents); + Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); + + var projectInfo2 = latestProjects[1]; + Assert.Equal(s_hostProject2.Key, projectInfo2.ProjectKey); + var document2 = Assert.Single(projectInfo2.Documents); + Assert.Equal(s_hostDocument2.FilePath, document2.FilePath); + } + + [UIFact] + public async Task NoLspEditor_DoesNotPublishExistingProjectsDuringInitialization() + { + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + }); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager, isLspEditorAvailable: false); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + Assert.Empty(publisher.GetLatestProjectInfos()); + } + + [UIFact] + public async Task PublishesProjectsAddedAfterInitialization() + { + var projectManager = CreateProjectSnapshotManager(); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + + updater.ProjectAdded(s_hostProject2); + updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader("

Hello World

", s_hostDocument2.FilePath)); + }); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + // Sort projects by project key. + var latestProjects = publisher + .GetLatestProjectInfos() + .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); + + Assert.Equal(2, latestProjects.Length); + + var projectInfo1 = latestProjects[0]; + Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); + var document1 = Assert.Single(projectInfo1.Documents); + Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); + + var projectInfo2 = latestProjects[1]; + Assert.Equal(s_hostProject2.Key, projectInfo2.ProjectKey); + var document2 = Assert.Single(projectInfo2.Documents); + Assert.Equal(s_hostDocument2.FilePath, document2.FilePath); + } + + [UIFact] + public async Task NoLspEditor_DoesNotPublishProjectsAddedAfterInitialization() + { + var projectManager = CreateProjectSnapshotManager(); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager, isLspEditorAvailable: false); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + }); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + Assert.Empty(publisher.GetLatestProjectInfos()); + } + + [UIFact] + public async Task PublishesDocumentAddedAfterInitialization() + { + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + }); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + + await projectManager.UpdateAsync(static updater => + { + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + }); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + var latestProjects = publisher.GetLatestProjectInfos(); + + var projectInfo1 = Assert.Single(latestProjects); + Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); + var document1 = Assert.Single(projectInfo1.Documents); + Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); + } + + [UIFact] + public async Task PublishesProjectRemovedAfterInitialization() + { + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + }); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + var latestProjects = publisher.GetLatestProjectInfos(); + + var projectInfo1 = Assert.Single(latestProjects); + Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectRemoved(s_hostProject1.Key); + }); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + Assert.Empty(publisher.GetLatestProjectInfos()); + } + + [UIFact] + public async Task ListenerNotifiedOfUpdates() + { + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + }); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + + var listener = new TestListener(); + publisher.AddListener(listener); + + await projectManager.UpdateAsync(static updater => + { + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + }); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + Assert.Empty(listener.Removes); + + var projectInfo1 = Assert.Single(listener.Updates); + Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); + var document1 = Assert.Single(projectInfo1.Documents); + Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); + } + + [UIFact] + public async Task ListenerNotifiedOfRemoves() + { + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectAdded(s_hostProject1); + }); + + var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + + var listener = new TestListener(); + publisher.AddListener(listener); + + await projectManager.UpdateAsync(static updater => + { + updater.ProjectRemoved(s_hostProject1.Key); + }); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + var projectKey1 = Assert.Single(listener.Removes); + Assert.Equal(s_hostProject1.Key, projectKey1); + + Assert.Empty(listener.Updates); + } + + private async Task<(VisualStudioRazorProjectInfoPublisher, VisualStudioRazorProjectInfoPublisher.TestAccessor)> CreatePublisherAndInitializeAsync( + IProjectSnapshotManager projectManager, + bool isLspEditorAvailable = true) + { + var lspEditorFeatureDetector = StrictMock.Of(x => + x.IsLSPEditorAvailable() == isLspEditorAvailable); + + var publisher = new VisualStudioRazorProjectInfoPublisher(projectManager, lspEditorFeatureDetector, JoinableTaskContext, delay: TimeSpan.FromMilliseconds(5)); + AddDisposable(publisher); + + var testAccessor = publisher.GetTestAccessor(); + await testAccessor.WaitForInitializeAsync(); + + return (publisher, testAccessor); + } + + private static TextLoader CreateTextLoader(string content, string filePath) + { + var mock = new StrictMock(); + + var sourceText = SourceText.From(content); + var textAndVersion = TextAndVersion.Create(sourceText, VersionStamp.Default, filePath); + + mock.Setup(x => x.LoadTextAndVersionAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(textAndVersion); + + return mock.Object; + } + + private sealed class TestListener : IRazorProjectInfoListener + { + private readonly ImmutableArray.Builder _removes = ImmutableArray.CreateBuilder(); + private readonly ImmutableArray.Builder _updates = ImmutableArray.CreateBuilder(); + + public ImmutableArray Removes => _removes.ToImmutable(); + public ImmutableArray Updates => _updates.ToImmutable(); + + public ValueTask RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken) + { + _removes.Add(projectKey); + return default; + } + + public ValueTask UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) + { + _updates.Add(projectInfo); + return default; + } + } +} From a84ec472614ca7ad4f68c25674717038f9a1f8d3 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 6 Jun 2024 09:41:08 -0700 Subject: [PATCH 11/45] Rename to RazorProjectInfoDriver --- .../ProjectSystem/RazorProjectInfoListener.cs | 6 ++-- .../RazorLanguageServer.cs | 2 +- ...ublisher.cs => IRazorProjectInfoDriver.cs} | 7 +++-- ....cs => RazorProjectInfoDriver.Comparer.cs} | 3 +- ...=> RazorProjectInfoDriver.TestAccessor.cs} | 4 +-- ...Publisher.cs => RazorProjectInfoDriver.cs} | 28 ++++++++++--------- .../RazorLanguageServerClient.cs | 6 ++-- ...sualStudioRazorProjectInfoPublisherTest.cs | 18 ++++++------ 8 files changed, 39 insertions(+), 35 deletions(-) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/{IRazorProjectInfoPublisher.cs => IRazorProjectInfoDriver.cs} (62%) rename src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/{VisualStudioRazorProjectInfoPublisher.Comparer.cs => RazorProjectInfoDriver.Comparer.cs} (92%) rename src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/{VisualStudioRazorProjectInfoPublisher.TestAccessor.cs => RazorProjectInfoDriver.TestAccessor.cs} (78%) rename src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/{VisualStudioRazorProjectInfoPublisher.cs => RazorProjectInfoDriver.cs} (90%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs index 9cfb27b541d..7ec142abeba 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs @@ -17,18 +17,18 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; internal sealed class RazorProjectInfoListener( IRazorProjectService projectService, - IRazorProjectInfoPublisher publisher) + IRazorProjectInfoDriver publisher) : IRazorProjectInfoListener, IOnInitialized { private readonly IRazorProjectService _projectService = projectService; - private readonly IRazorProjectInfoPublisher _publisher = publisher; + private readonly IRazorProjectInfoDriver _publisher = publisher; public async Task OnInitializedAsync(ILspServices services, CancellationToken cancellationToken) { _publisher.AddListener(this); // Add all existing projects - foreach (var projectInfo in _publisher.GetLatestProjectInfos()) + foreach (var projectInfo in _publisher.GetLatestProjectInfo()) { await AddOrUpdateProjectAsync(projectInfo, cancellationToken).ConfigureAwait(false); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 36904ba4000..84972435453 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -114,7 +114,7 @@ protected override ILspServices ConstructLspServices() foreach (var service in services) { // Only register RazorProjectInfoListener if an IRazorProjectInfoPublisher was registered. - if (service.ServiceType == typeof(IRazorProjectInfoPublisher)) + if (service.ServiceType == typeof(IRazorProjectInfoDriver)) { services.AddSingleton(); services.AddSingleton((services) => (IOnInitialized)services.GetRequiredService()); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs similarity index 62% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs index 85c83586d37..654f6a05365 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs @@ -6,9 +6,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal interface IRazorProjectInfoPublisher +/// +/// Handles project changes and notifies listeners of project updates and removal. +/// +internal interface IRazorProjectInfoDriver { - ImmutableArray GetLatestProjectInfos(); + ImmutableArray GetLatestProjectInfo(); void AddListener(IRazorProjectInfoListener listener); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs similarity index 92% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs index 2602ece695a..5d7aa651f10 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.Comparer.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs @@ -5,7 +5,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -internal sealed partial class VisualStudioRazorProjectInfoPublisher +internal sealed partial class RazorProjectInfoDriver { private sealed class Comparer : IEqualityComparer { @@ -34,5 +34,4 @@ public int GetHashCode(Work work) return work.ProjectKey.GetHashCode(); } } - } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs similarity index 78% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs index c82cc6c609f..cd80a938347 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.TestAccessor.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs @@ -5,11 +5,11 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -internal sealed partial class VisualStudioRazorProjectInfoPublisher +internal sealed partial class RazorProjectInfoDriver { internal TestAccessor GetTestAccessor() => new(this); - internal readonly struct TestAccessor(VisualStudioRazorProjectInfoPublisher instance) + internal readonly struct TestAccessor(RazorProjectInfoDriver instance) { public async Task WaitForInitializeAsync() { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs similarity index 90% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs index 7f47782e927..6f1286e8bef 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisher.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs @@ -17,8 +17,8 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -[Export(typeof(IRazorProjectInfoPublisher))] -internal sealed partial class VisualStudioRazorProjectInfoPublisher : IRazorProjectInfoPublisher, IDisposable +[Export(typeof(IRazorProjectInfoDriver))] +internal sealed partial class RazorProjectInfoDriver : IRazorProjectInfoDriver, IDisposable { private abstract record Work(ProjectKey ProjectKey); private sealed record Update(RazorProjectInfo ProjectInfo) : Work(ProjectInfo.ProjectKey); @@ -29,13 +29,13 @@ private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); private readonly CancellationTokenSource _disposeTokenSource; private readonly AsyncBatchingWorkQueue _workQueue; - private readonly Dictionary _latestProjectInfo; + private readonly Dictionary _latestProjectInfoMap; private ImmutableArray _listeners; private readonly JoinableTask _initializeTask; [ImportingConstructor] - public VisualStudioRazorProjectInfoPublisher( + public RazorProjectInfoDriver( IProjectSnapshotManager projectManager, LSPEditorFeatureDetector lspEditorFeatureDetector, JoinableTaskContext joinableTaskContext) @@ -43,7 +43,7 @@ public VisualStudioRazorProjectInfoPublisher( { } - public VisualStudioRazorProjectInfoPublisher( + public RazorProjectInfoDriver( IProjectSnapshotManager projectManager, LSPEditorFeatureDetector lspEditorFeatureDetector, JoinableTaskContext joinableTaskContext, @@ -51,7 +51,7 @@ public VisualStudioRazorProjectInfoPublisher( { _disposeTokenSource = new(); _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); - _latestProjectInfo = []; + _latestProjectInfoMap = []; _listeners = []; var jtf = joinableTaskContext.Factory; @@ -134,16 +134,17 @@ private async ValueTask ProcessBatchAsync(ImmutableArray items, Cancellati return; } - lock (_latestProjectInfo) + // Update our map first + lock (_latestProjectInfoMap) { switch (work) { case Update(var projectInfo): - _latestProjectInfo[projectInfo.ProjectKey] = projectInfo; + _latestProjectInfoMap[projectInfo.ProjectKey] = projectInfo; break; case Remove(var projectKey): - _latestProjectInfo.Remove(projectKey); + _latestProjectInfoMap.Remove(projectKey); break; default: @@ -152,6 +153,7 @@ private async ValueTask ProcessBatchAsync(ImmutableArray items, Cancellati } } + // Now, notify listeners foreach (var listener in _listeners) { if (token.IsCancellationRequested) @@ -183,13 +185,13 @@ private void EnqueueRemove(ProjectKey projectKey) _workQueue.AddWork(new Remove(projectKey)); } - public ImmutableArray GetLatestProjectInfos() + public ImmutableArray GetLatestProjectInfo() { - lock (_latestProjectInfo) + lock (_latestProjectInfoMap) { - using var builder = new PooledArrayBuilder(capacity: _latestProjectInfo.Count); + using var builder = new PooledArrayBuilder(capacity: _latestProjectInfoMap.Count); - foreach (var (_, projectInfo) in _latestProjectInfo) + foreach (var (_, projectInfo) in _latestProjectInfoMap) { builder.Add(projectInfo); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index a2e86f4a13e..c283ee32ffc 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -34,7 +34,7 @@ internal class RazorLanguageServerClient( RazorCustomMessageTarget customTarget, LSPRequestInvoker requestInvoker, RazorProjectInfoEndpointPublisher projectInfoEndpointPublisher, - IRazorProjectInfoPublisher projectInfoPublisher, + IRazorProjectInfoDriver projectInfoPublisher, ILoggerFactory loggerFactory, RazorLogHubTraceProvider traceProvider, LanguageServerFeatureOptions languageServerFeatureOptions, @@ -54,7 +54,7 @@ internal class RazorLanguageServerClient( private readonly RazorCustomMessageTarget _customMessageTarget = customTarget; private readonly LSPRequestInvoker _requestInvoker = requestInvoker; private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher; - private readonly IRazorProjectInfoPublisher _projectInfoPublisher = projectInfoPublisher; + private readonly IRazorProjectInfoDriver _projectInfoPublisher = projectInfoPublisher; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider; private readonly ILoggerFactory _loggerFactory = loggerFactory; @@ -178,7 +178,7 @@ private async Task EnsureContainedLanguageServersInitializedAsync() private void ConfigureServices(IServiceCollection serviceCollection) { - serviceCollection.AddSingleton(_projectInfoPublisher); + serviceCollection.AddSingleton(_projectInfoPublisher); serviceCollection.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs index 14b79a32e9a..0ba121f31da 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs @@ -57,7 +57,7 @@ await projectManager.UpdateAsync(static updater => // Sort projects by project key. var latestProjects = publisher - .GetLatestProjectInfos() + .GetLatestProjectInfo() .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); Assert.Equal(2, latestProjects.Length); @@ -88,7 +88,7 @@ await projectManager.UpdateAsync(static updater => await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - Assert.Empty(publisher.GetLatestProjectInfos()); + Assert.Empty(publisher.GetLatestProjectInfo()); } [UIFact] @@ -111,7 +111,7 @@ await projectManager.UpdateAsync(static updater => // Sort projects by project key. var latestProjects = publisher - .GetLatestProjectInfos() + .GetLatestProjectInfo() .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); Assert.Equal(2, latestProjects.Length); @@ -142,7 +142,7 @@ await projectManager.UpdateAsync(static updater => await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - Assert.Empty(publisher.GetLatestProjectInfos()); + Assert.Empty(publisher.GetLatestProjectInfo()); } [UIFact] @@ -164,7 +164,7 @@ await projectManager.UpdateAsync(static updater => await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - var latestProjects = publisher.GetLatestProjectInfos(); + var latestProjects = publisher.GetLatestProjectInfo(); var projectInfo1 = Assert.Single(latestProjects); Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); @@ -186,7 +186,7 @@ await projectManager.UpdateAsync(static updater => await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - var latestProjects = publisher.GetLatestProjectInfos(); + var latestProjects = publisher.GetLatestProjectInfo(); var projectInfo1 = Assert.Single(latestProjects); Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); @@ -198,7 +198,7 @@ await projectManager.UpdateAsync(static updater => await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - Assert.Empty(publisher.GetLatestProjectInfos()); + Assert.Empty(publisher.GetLatestProjectInfo()); } [UIFact] @@ -259,14 +259,14 @@ await projectManager.UpdateAsync(static updater => Assert.Empty(listener.Updates); } - private async Task<(VisualStudioRazorProjectInfoPublisher, VisualStudioRazorProjectInfoPublisher.TestAccessor)> CreatePublisherAndInitializeAsync( + private async Task<(RazorProjectInfoDriver, RazorProjectInfoDriver.TestAccessor)> CreatePublisherAndInitializeAsync( IProjectSnapshotManager projectManager, bool isLspEditorAvailable = true) { var lspEditorFeatureDetector = StrictMock.Of(x => x.IsLSPEditorAvailable() == isLspEditorAvailable); - var publisher = new VisualStudioRazorProjectInfoPublisher(projectManager, lspEditorFeatureDetector, JoinableTaskContext, delay: TimeSpan.FromMilliseconds(5)); + var publisher = new RazorProjectInfoDriver(projectManager, lspEditorFeatureDetector, JoinableTaskContext, delay: TimeSpan.FromMilliseconds(5)); AddDisposable(publisher); var testAccessor = publisher.GetTestAccessor(); From 29a48005ce06481c9c84a8e15c7e2f953c17587e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 6 Jun 2024 10:05:27 -0700 Subject: [PATCH 12/45] Don't export `RazorProjectInfoDriver` as a MEF service Since the lifetime of the `RazorProjectInfoDriver` is the same as the language service, we let our language server client create and initialize it during activation. This means that the driver no longer needs to check if `IsLSPEditorAvailable()` since its constructed by the LSP editor's language client. --- .../Hosting/RazorLanguageServerHost.cs | 1 + .../RazorLanguageServer.cs | 13 +--- .../RazorProjectInfoDriver.TestAccessor.cs | 5 -- .../ProjectSystem/RazorProjectInfoDriver.cs | 49 ++---------- .../RazorLanguageServerClient.cs | 24 +++--- ...rTest.cs => RazorProjectInfoDriverTest.cs} | 78 +++++-------------- 6 files changed, 44 insertions(+), 126 deletions(-) rename src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/{VisualStudioRazorProjectInfoPublisherTest.cs => RazorProjectInfoDriverTest.cs} (74%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs index b9cdcb3a0a3..447448d0396 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Protocol; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 84972435453..91c0d76fe94 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -110,17 +110,8 @@ protected override ILspServices ConstructLspServices() _configureServices(services); } - // TODO: Remove this - foreach (var service in services) - { - // Only register RazorProjectInfoListener if an IRazorProjectInfoPublisher was registered. - if (service.ServiceType == typeof(IRazorProjectInfoDriver)) - { - services.AddSingleton(); - services.AddSingleton((services) => (IOnInitialized)services.GetRequiredService()); - break; - } - } + services.AddSingleton(); + services.AddSingleton((services) => (IOnInitialized)services.GetRequiredService()); services.AddSingleton(_clientConnection); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs index cd80a938347..7c67f51e729 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs @@ -11,11 +11,6 @@ internal sealed partial class RazorProjectInfoDriver internal readonly struct TestAccessor(RazorProjectInfoDriver instance) { - public async Task WaitForInitializeAsync() - { - await instance._initializeTask; - } - public Task WaitUntilCurrentBatchCompletesAsync() => instance._workQueue.WaitUntilCurrentBatchCompletesAsync(); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs index 6f1286e8bef..f35a7c59520 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.ComponentModel.Composition; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; @@ -12,12 +11,9 @@ using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -[Export(typeof(IRazorProjectInfoDriver))] internal sealed partial class RazorProjectInfoDriver : IRazorProjectInfoDriver, IDisposable { private abstract record Work(ProjectKey ProjectKey); @@ -26,49 +22,20 @@ private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); + private readonly IProjectSnapshotManager _projectManager; private readonly CancellationTokenSource _disposeTokenSource; private readonly AsyncBatchingWorkQueue _workQueue; private readonly Dictionary _latestProjectInfoMap; private ImmutableArray _listeners; - private readonly JoinableTask _initializeTask; - - [ImportingConstructor] - public RazorProjectInfoDriver( - IProjectSnapshotManager projectManager, - LSPEditorFeatureDetector lspEditorFeatureDetector, - JoinableTaskContext joinableTaskContext) - : this(projectManager, lspEditorFeatureDetector, joinableTaskContext, s_delay) - { - } - - public RazorProjectInfoDriver( - IProjectSnapshotManager projectManager, - LSPEditorFeatureDetector lspEditorFeatureDetector, - JoinableTaskContext joinableTaskContext, - TimeSpan delay) + public RazorProjectInfoDriver(IProjectSnapshotManager projectManager, TimeSpan? delay = null) { + _projectManager = projectManager; _disposeTokenSource = new(); - _workQueue = new AsyncBatchingWorkQueue(delay, ProcessBatchAsync, _disposeTokenSource.Token); + _workQueue = new AsyncBatchingWorkQueue(delay ?? s_delay, ProcessBatchAsync, _disposeTokenSource.Token); _latestProjectInfoMap = []; _listeners = []; - - var jtf = joinableTaskContext.Factory; - - _initializeTask = jtf.RunAsync(async () => - { - // Switch to the main thread because IsLSPEditorAvailable() expects to. - await jtf.SwitchToMainThreadAsync(); - - // Because this service is only consumed by the language server, we only initialize it - // when the LSP editor is available. - if (lspEditorFeatureDetector.IsLSPEditorAvailable()) - { - await InitializeAsync(projectManager, _disposeTokenSource.Token); - } - }); - } public void Dispose() @@ -77,19 +44,19 @@ public void Dispose() _disposeTokenSource.Dispose(); } - private async Task InitializeAsync(IProjectSnapshotManager projectManager, CancellationToken cancellationToken) + public async Task InitializeAsync(CancellationToken cancellationToken) { // Even though we aren't mutating the project snapshot manager, we call UpdateAsync(...) here to ensure // that we run on its dispatcher. That ensures that no changes will code in while we are iterating the - // current set of projects. - await projectManager.UpdateAsync(updater => + // current set of projects and connected to the Changed event. + await _projectManager.UpdateAsync(updater => { foreach (var project in updater.GetProjects()) { EnqueueUpdate(project.ToRazorProjectInfo()); } - projectManager.Changed += ProjectManager_Changed; + _projectManager.Changed += ProjectManager_Changed; }, cancellationToken); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index c283ee32ffc..8219caf0be8 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -20,6 +20,7 @@ using Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; using Microsoft.VisualStudio.Razor.Logging; using Microsoft.VisualStudio.Razor.Settings; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Utilities; using Nerdbank.Streams; @@ -34,7 +35,7 @@ internal class RazorLanguageServerClient( RazorCustomMessageTarget customTarget, LSPRequestInvoker requestInvoker, RazorProjectInfoEndpointPublisher projectInfoEndpointPublisher, - IRazorProjectInfoDriver projectInfoPublisher, + IProjectSnapshotManager projectManager, ILoggerFactory loggerFactory, RazorLogHubTraceProvider traceProvider, LanguageServerFeatureOptions languageServerFeatureOptions, @@ -54,7 +55,7 @@ internal class RazorLanguageServerClient( private readonly RazorCustomMessageTarget _customMessageTarget = customTarget; private readonly LSPRequestInvoker _requestInvoker = requestInvoker; private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher; - private readonly IRazorProjectInfoDriver _projectInfoPublisher = projectInfoPublisher; + private readonly IProjectSnapshotManager _projectManager = projectManager; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider; private readonly ILoggerFactory _loggerFactory = loggerFactory; @@ -101,6 +102,9 @@ public event AsyncEventHandler? StopAsync var lspOptions = RazorLSPOptions.From(_clientSettingsManager.GetClientSettings()); + var projectInfoDriver = new RazorProjectInfoDriver(_projectManager); + await projectInfoDriver.InitializeAsync(token); + _host = RazorLanguageServerHost.Create( serverStream, serverStream, @@ -114,8 +118,14 @@ public event AsyncEventHandler? StopAsync // This must not happen on an RPC endpoint due to UIThread concerns, so ActivateAsync was chosen. await EnsureContainedLanguageServersInitializedAsync(); - var connection = new Connection(clientStream, clientStream); - return connection; + + return new Connection(clientStream, clientStream); + + void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(projectInfoDriver); + services.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); + } } internal static IEnumerable> GetRelevantContainedLanguageClientsAndMetadata(ILanguageServiceBroker2 languageServiceBroker) @@ -176,12 +186,6 @@ private async Task EnsureContainedLanguageServersInitializedAsync() _lspServerActivationTracker.Activated(); } - private void ConfigureServices(IServiceCollection serviceCollection) - { - serviceCollection.AddSingleton(_projectInfoPublisher); - serviceCollection.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); - } - private async Task EnsureCleanedUpServerAsync() { if (_host is null) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs similarity index 74% rename from src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs rename to src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs index 0ba121f31da..5f867503b4a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoPublisherTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Moq; using Xunit; @@ -19,7 +18,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -public class VisualStudioRazorProjectInfoPublisherTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) +public class RazorProjectInfoDriverTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { private static readonly HostProject s_hostProject1 = new( projectFilePath: "C:/path/to/project1/project1.csproj", @@ -38,7 +37,7 @@ public class VisualStudioRazorProjectInfoPublisherTest(ITestOutputHelper testOut private static readonly HostDocument s_hostDocument2 = new("C:/path/to/project2/file.razor", "file.razor"); [UIFact] - public async Task PublishesExistingProjectsDuringInitialization() + public async Task ProcessesExistingProjectsDuringInitialization() { var projectManager = CreateProjectSnapshotManager(); @@ -51,12 +50,12 @@ await projectManager.UpdateAsync(static updater => updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader("

Hello World

", s_hostDocument2.FilePath)); }); - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); // Sort projects by project key. - var latestProjects = publisher + var latestProjects = driver .GetLatestProjectInfo() .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); @@ -74,29 +73,11 @@ await projectManager.UpdateAsync(static updater => } [UIFact] - public async Task NoLspEditor_DoesNotPublishExistingProjectsDuringInitialization() - { - var projectManager = CreateProjectSnapshotManager(); - - await projectManager.UpdateAsync(static updater => - { - updater.ProjectAdded(s_hostProject1); - updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); - }); - - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager, isLspEditorAvailable: false); - - await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - - Assert.Empty(publisher.GetLatestProjectInfo()); - } - - [UIFact] - public async Task PublishesProjectsAddedAfterInitialization() + public async Task ProcessesProjectsAddedAfterInitialization() { var projectManager = CreateProjectSnapshotManager(); - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await projectManager.UpdateAsync(static updater => { @@ -128,25 +109,7 @@ await projectManager.UpdateAsync(static updater => } [UIFact] - public async Task NoLspEditor_DoesNotPublishProjectsAddedAfterInitialization() - { - var projectManager = CreateProjectSnapshotManager(); - - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager, isLspEditorAvailable: false); - - await projectManager.UpdateAsync(static updater => - { - updater.ProjectAdded(s_hostProject1); - updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); - }); - - await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - - Assert.Empty(publisher.GetLatestProjectInfo()); - } - - [UIFact] - public async Task PublishesDocumentAddedAfterInitialization() + public async Task ProcessesDocumentAddedAfterInitialization() { var projectManager = CreateProjectSnapshotManager(); @@ -155,7 +118,7 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await projectManager.UpdateAsync(static updater => { @@ -173,7 +136,7 @@ await projectManager.UpdateAsync(static updater => } [UIFact] - public async Task PublishesProjectRemovedAfterInitialization() + public async Task ProcessesProjectRemovedAfterInitialization() { var projectManager = CreateProjectSnapshotManager(); @@ -182,7 +145,7 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); @@ -211,7 +174,7 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); var listener = new TestListener(); publisher.AddListener(listener); @@ -241,7 +204,7 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreatePublisherAndInitializeAsync(projectManager); + var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); var listener = new TestListener(); publisher.AddListener(listener); @@ -259,20 +222,17 @@ await projectManager.UpdateAsync(static updater => Assert.Empty(listener.Updates); } - private async Task<(RazorProjectInfoDriver, RazorProjectInfoDriver.TestAccessor)> CreatePublisherAndInitializeAsync( - IProjectSnapshotManager projectManager, - bool isLspEditorAvailable = true) + private async Task<(RazorProjectInfoDriver, RazorProjectInfoDriver.TestAccessor)> CreateDriverAndInitializeAsync( + IProjectSnapshotManager projectManager) { - var lspEditorFeatureDetector = StrictMock.Of(x => - x.IsLSPEditorAvailable() == isLspEditorAvailable); + var driver = new RazorProjectInfoDriver(projectManager, delay: TimeSpan.FromMilliseconds(5)); + AddDisposable(driver); - var publisher = new RazorProjectInfoDriver(projectManager, lspEditorFeatureDetector, JoinableTaskContext, delay: TimeSpan.FromMilliseconds(5)); - AddDisposable(publisher); + var testAccessor = driver.GetTestAccessor(); - var testAccessor = publisher.GetTestAccessor(); - await testAccessor.WaitForInitializeAsync(); + await driver.InitializeAsync(DisposalToken); - return (publisher, testAccessor); + return (driver, testAccessor); } private static TextLoader CreateTextLoader(string content, string filePath) From b5158d7b7b2e70b26fbbc057d1a1b1b41b63e0c2 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 6 Jun 2024 10:10:04 -0700 Subject: [PATCH 13/45] Ensure that the `RazorLanguageServerHost` is disposed --- .../RazorLanguageServerClient.cs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index 8219caf0be8..70e49a239c2 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -128,6 +128,24 @@ void ConfigureServices(IServiceCollection services) } } + private async Task EnsureCleanedUpServerAsync() + { + if (_host is null) + { + // Server was already cleaned up + return; + } + + if (_host is not null) + { + // Server still hasn't shutdown, wait for it to shutdown + await _host.WaitForExitAsync().ConfigureAwait(false); + + _host.Dispose(); + _host = null; + } + } + internal static IEnumerable> GetRelevantContainedLanguageClientsAndMetadata(ILanguageServiceBroker2 languageServiceBroker) { var relevantClientAndMetadata = new List>(); @@ -186,21 +204,6 @@ private async Task EnsureContainedLanguageServersInitializedAsync() _lspServerActivationTracker.Activated(); } - private async Task EnsureCleanedUpServerAsync() - { - if (_host is null) - { - // Server was already cleaned up - return; - } - - if (_host is not null) - { - // Server still hasn't shutdown, wait for it to shutdown - await _host.WaitForExitAsync().ConfigureAwait(false); - } - } - public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask; public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) From 23997ca6be601c22b169f2ff3fce98c34b258f49 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 6 Jun 2024 12:05:46 -0700 Subject: [PATCH 14/45] Make `RazorProjectService` implement `IRazorProjectInfoListener` As part of this change, I've made `RazorProjectService` own the misc files project, rather than `SnapshotResolver`, which fixes a potential issue with initialization order. --- .../IServiceCollectionExtensions.cs | 2 + .../Hosting/RazorLanguageServerHost.cs | 2 + .../FileWatcherBasedRazorProjectInfoDriver.cs | 22 ++ .../ProjectSystem/RazorProjectInfoListener.cs | 68 ---- .../RazorProjectService.TestAccessor.cs | 17 + .../ProjectSystem/RazorProjectService.cs | 316 +++++++++++++----- .../RazorLanguageServer.cs | 15 +- .../IRazorProjectInfoListener.cs | 4 +- .../RazorLanguageServerClient.cs | 2 +- .../TestRazorProjectService.cs | 38 ++- .../RazorProjectInfoDriverTest.cs | 112 ++++--- 11 files changed, 374 insertions(+), 224 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index e81052b54e4..e83b86f144c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -211,6 +211,8 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton(); services.AddSingleton(); + services.AddSingleton((services) => (RazorProjectService)services.GetRequiredService()); + services.AddSingleton(sp => (RazorProjectService)sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs index 447448d0396..dc24da6e8a9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs @@ -47,6 +47,7 @@ public static RazorLanguageServerHost Create( LanguageServerFeatureOptions? featureOptions = null, RazorLSPOptions? razorLSPOptions = null, ILspServerActivationTracker? lspServerActivationTracker = null, + IRazorProjectInfoDriver? projectInfoDriver = null, TraceSource? traceSource = null) { var (jsonRpc, jsonSerializer) = CreateJsonRpc(input, output); @@ -65,6 +66,7 @@ public static RazorLanguageServerHost Create( configureServices, razorLSPOptions, lspServerActivationTracker, + projectInfoDriver, telemetryReporter); var host = new RazorLanguageServerHost(server); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs new file mode 100644 index 00000000000..edc1963f5f2 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; + +internal class FileWatcherBasedRazorProjectInfoDriver : IRazorProjectInfoDriver +{ + // TODO: Implement! + + private ImmutableArray _listeners; + + public ImmutableArray GetLatestProjectInfo() => []; + + public void AddListener(IRazorProjectInfoListener listener) + { + ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs deleted file mode 100644 index 7ec142abeba..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectInfoListener.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CommonLanguageServerProtocol.Framework; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; - -internal sealed class RazorProjectInfoListener( - IRazorProjectService projectService, - IRazorProjectInfoDriver publisher) - : IRazorProjectInfoListener, IOnInitialized -{ - private readonly IRazorProjectService _projectService = projectService; - private readonly IRazorProjectInfoDriver _publisher = publisher; - - public async Task OnInitializedAsync(ILspServices services, CancellationToken cancellationToken) - { - _publisher.AddListener(this); - - // Add all existing projects - foreach (var projectInfo in _publisher.GetLatestProjectInfo()) - { - await AddOrUpdateProjectAsync(projectInfo, cancellationToken).ConfigureAwait(false); - } - } - - public async ValueTask RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken) - { - await _projectService - .UpdateProjectAsync( - projectKey, - configuration: null, - rootNamespace: null, - displayName: "", - ProjectWorkspaceState.Default, - documents: [], - cancellationToken) - .ConfigureAwait(false); - } - - public async ValueTask UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) - { - await AddOrUpdateProjectAsync(projectInfo, cancellationToken).ConfigureAwait(false); - } - - private Task AddOrUpdateProjectAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) - { - return _projectService.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - projectInfo.Configuration, - projectInfo.RootNamespace, - projectInfo.DisplayName, - projectInfo.ProjectWorkspaceState, - projectInfo.Documents, - cancellationToken); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs new file mode 100644 index 00000000000..402e6c2cc78 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; + +internal partial class RazorProjectService +{ + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(RazorProjectService instance) + { + public ValueTask WaitForInitializationAsync() + => instance.WaitForInitializationAsync(); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index 14bc2e9d75a..b9e08edd406 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -19,28 +19,138 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.Threading; namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -internal class RazorProjectService( - RemoteTextLoaderFactory remoteTextLoaderFactory, - IDocumentVersionCache documentVersionCache, - IProjectSnapshotManager projectManager, - ILoggerFactory loggerFactory) - : IRazorProjectService +/// +/// Maintains the with the semantics of Razor's project model. +/// +/// +/// This service implements both to ensure it is created early and +/// to ensure that its initialization completes when the language server +/// finishes initialization. +/// +internal partial class RazorProjectService : IRazorProjectService, IRazorProjectInfoListener, IRazorStartupService, IOnInitialized, IDisposable { - private readonly IProjectSnapshotManager _projectManager = projectManager; - private readonly RemoteTextLoaderFactory _remoteTextLoaderFactory = remoteTextLoaderFactory; - private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache; - private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); + private readonly IRazorProjectInfoDriver _projectInfoDriver; + private readonly IProjectSnapshotManager _projectManager; + private readonly RemoteTextLoaderFactory _remoteTextLoaderFactory; + private readonly IDocumentVersionCache _documentVersionCache; + private readonly ILogger _logger; + + private readonly CancellationTokenSource _disposeTokenSource; + private readonly Task _initializeTask; + + public RazorProjectService( + IProjectSnapshotManager projectManager, + IRazorProjectInfoDriver projectInfoDriver, + IDocumentVersionCache documentVersionCache, + RemoteTextLoaderFactory remoteTextLoaderFactory, + ILoggerFactory loggerFactory) + { + _projectInfoDriver = projectInfoDriver; + _projectManager = projectManager; + _remoteTextLoaderFactory = remoteTextLoaderFactory; + _documentVersionCache = documentVersionCache; + _logger = loggerFactory.GetOrCreateLogger(); + + // We kick off initialization immediately to ensure that the IRazorProjectService + // is hot and ready to go when requests come in. + _disposeTokenSource = new(); + _initializeTask = InitializeAsync(_disposeTokenSource.Token); + } - public Task AddDocumentToMiscProjectAsync(string filePath, CancellationToken cancellationToken) + public void Dispose() { - return _projectManager.UpdateAsync( - updater: AddDocumentToMiscProjectCore, - state: filePath, - cancellationToken); + _disposeTokenSource.Cancel(); + _disposeTokenSource.Dispose(); + } + + private async Task InitializeAsync(CancellationToken cancellationToken) + { + // Add the MiscFilesProject + await _projectManager.UpdateAsync( + (updater, miscHostProject) => updater.ProjectAdded(miscHostProject), + state: MiscFilesHostProject.Instance, + cancellationToken) + .ConfigureAwait(false); + + // Register ourselves as a listener to the project driver. + _projectInfoDriver.AddListener(this); + + // Add all existing projects from the driver. + foreach (var projectInfo in _projectInfoDriver.GetLatestProjectInfo()) + { + await AddOrUpdateProjectCoreAsync( + projectInfo.ProjectKey, + projectInfo.FilePath, + projectInfo.Configuration, + projectInfo.RootNamespace, + projectInfo.DisplayName, + projectInfo.ProjectWorkspaceState, + projectInfo.Documents, + cancellationToken) + .ConfigureAwait(false); + } + } + + Task IOnInitialized.OnInitializedAsync(ILspServices services, CancellationToken cancellationToken) +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + => _initializeTask; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + + // Call to ensure that any public IRazorProjectService methods wait for initialization to complete. + private ValueTask WaitForInitializationAsync() + => _initializeTask is { IsCompleted: true } + ? default + : new(_initializeTask); + + async Task IRazorProjectInfoListener.UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) + { + // Don't update a project during initialization. + await WaitForInitializationAsync().ConfigureAwait(false); + + await AddOrUpdateProjectCoreAsync( + projectInfo.ProjectKey, + projectInfo.FilePath, + projectInfo.Configuration, + projectInfo.RootNamespace, + projectInfo.DisplayName, + projectInfo.ProjectWorkspaceState, + projectInfo.Documents, + cancellationToken) + .ConfigureAwait(false); + } + + async Task IRazorProjectInfoListener.RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken) + { + // Don't remove a project during initialization. + await WaitForInitializationAsync().ConfigureAwait(false); + + await AddOrUpdateProjectCoreAsync( + projectKey, + filePath: null, + configuration: null, + rootNamespace: null, + displayName: "", + ProjectWorkspaceState.Default, + documents: [], + cancellationToken) + .ConfigureAwait(false); + } + + public async Task AddDocumentToMiscProjectAsync(string filePath, CancellationToken cancellationToken) + { + await WaitForInitializationAsync().ConfigureAwait(false); + + await _projectManager + .UpdateAsync( + updater: AddDocumentToMiscProjectCore, + state: filePath, + cancellationToken) + .ConfigureAwait(false); } private void AddDocumentToMiscProjectCore(ProjectSnapshotManager.Updater updater, string filePath) @@ -68,9 +178,11 @@ private void AddDocumentToMiscProjectCore(ProjectSnapshotManager.Updater updater updater.DocumentAdded(miscFilesProject.Key, hostDocument, textLoader); } - public Task OpenDocumentAsync(string filePath, SourceText sourceText, int version, CancellationToken cancellationToken) + public async Task OpenDocumentAsync(string filePath, SourceText sourceText, int version, CancellationToken cancellationToken) { - return _projectManager.UpdateAsync( + await WaitForInitializationAsync().ConfigureAwait(false); + + await _projectManager.UpdateAsync( updater => { var textDocumentPath = FilePathNormalizer.Normalize(filePath); @@ -102,12 +214,15 @@ public Task OpenDocumentAsync(string filePath, SourceText sourceText, int versio TrackDocumentVersion(projectSnapshot, textDocumentPath, version, startGenerating: true); }); }, - cancellationToken); + cancellationToken) + .ConfigureAwait(false); } - public Task CloseDocumentAsync(string filePath, CancellationToken cancellationToken) + public async Task CloseDocumentAsync(string filePath, CancellationToken cancellationToken) { - return _projectManager.UpdateAsync( + await WaitForInitializationAsync().ConfigureAwait(false); + + await _projectManager.UpdateAsync( updater => { ActOnDocumentInMultipleProjects( @@ -120,12 +235,15 @@ public Task CloseDocumentAsync(string filePath, CancellationToken cancellationTo updater.DocumentClosed(projectSnapshot.Key, textDocumentPath, textLoader); }); }, - cancellationToken); + cancellationToken) + .ConfigureAwait(false); } - public Task RemoveDocumentAsync(string filePath, CancellationToken cancellationToken) + public async Task RemoveDocumentAsync(string filePath, CancellationToken cancellationToken) { - return _projectManager.UpdateAsync( + await WaitForInitializationAsync().ConfigureAwait(false); + + await _projectManager.UpdateAsync( updater => { ActOnDocumentInMultipleProjects( @@ -164,12 +282,15 @@ public Task RemoveDocumentAsync(string filePath, CancellationToken cancellationT } }); }, - cancellationToken); + cancellationToken) + .ConfigureAwait(false); } - public Task UpdateDocumentAsync(string filePath, SourceText sourceText, int version, CancellationToken cancellationToken) + public async Task UpdateDocumentAsync(string filePath, SourceText sourceText, int version, CancellationToken cancellationToken) { - return _projectManager.UpdateAsync( + await WaitForInitializationAsync().ConfigureAwait(false); + + await _projectManager.UpdateAsync( updater => { ActOnDocumentInMultipleProjects( @@ -189,7 +310,8 @@ public Task UpdateDocumentAsync(string filePath, SourceText sourceText, int vers TrackDocumentVersion(projectSnapshot, textDocumentPath, version, startGenerating: false); }); }, - cancellationToken); + cancellationToken) + .ConfigureAwait(false); } private void ActOnDocumentInMultipleProjects(string filePath, Action action) @@ -207,7 +329,7 @@ private void ActOnDocumentInMultipleProjects(string filePath, Action AddProjectAsync( + public async Task AddProjectAsync( string filePath, string intermediateOutputPath, RazorConfiguration? configuration, @@ -215,9 +337,13 @@ public Task AddProjectAsync( string? displayName, CancellationToken cancellationToken) { - return _projectManager.UpdateAsync( - updater => AddProjectCore(updater, filePath, intermediateOutputPath, configuration, rootNamespace, displayName), - cancellationToken); + await WaitForInitializationAsync().ConfigureAwait(false); + + return await _projectManager + .UpdateAsync( + updater => AddProjectCore(updater, filePath, intermediateOutputPath, configuration, rootNamespace, displayName), + cancellationToken) + .ConfigureAwait(false); } private ProjectKey AddProjectCore(ProjectSnapshotManager.Updater updater, string filePath, string intermediateOutputPath, RazorConfiguration? configuration, string? rootNamespace, string? displayName) @@ -236,7 +362,7 @@ private ProjectKey AddProjectCore(ProjectSnapshotManager.Updater updater, string return hostProject.Key; } - public Task UpdateProjectAsync( + public async Task UpdateProjectAsync( ProjectKey projectKey, RazorConfiguration? configuration, string? rootNamespace, @@ -245,10 +371,21 @@ public Task UpdateProjectAsync( ImmutableArray documents, CancellationToken cancellationToken) { - return AddOrUpdateProjectCoreAsync(projectKey, filePath: null, configuration, rootNamespace, displayName, projectWorkspaceState, documents, cancellationToken); + await WaitForInitializationAsync().ConfigureAwait(false); + + await AddOrUpdateProjectCoreAsync( + projectKey, + filePath: null, + configuration, + rootNamespace, + displayName, + projectWorkspaceState, + documents, + cancellationToken) + .ConfigureAwait(false); } - public Task AddOrUpdateProjectAsync( + public async Task AddOrUpdateProjectAsync( ProjectKey projectKey, string filePath, RazorConfiguration? configuration, @@ -258,7 +395,18 @@ public Task AddOrUpdateProjectAsync( ImmutableArray documents, CancellationToken cancellationToken) { - return AddOrUpdateProjectCoreAsync(projectKey, filePath, configuration, rootNamespace, displayName, projectWorkspaceState, documents, cancellationToken); + await WaitForInitializationAsync().ConfigureAwait(false); + + await AddOrUpdateProjectCoreAsync( + projectKey, + filePath, + configuration, + rootNamespace, + displayName, + projectWorkspaceState, + documents, + cancellationToken) + .ConfigureAwait(false); } private Task AddOrUpdateProjectCoreAsync( @@ -271,66 +419,68 @@ private Task AddOrUpdateProjectCoreAsync( ImmutableArray documents, CancellationToken cancellationToken) { + // Note: We specifically don't wait for initialization here because this is called *during* initialization. + // All other callers of this method must await WaitForInitializationAsync(). + return _projectManager.UpdateAsync( - updater => + updater => + { + if (!_projectManager.TryGetLoadedProject(projectKey, out var project)) + { + if (filePath is null) { - if (!_projectManager.TryGetLoadedProject(projectKey, out var project)) - { - if (filePath is null) - { - // Never tracked the project to begin with, noop. - _logger.LogInformation($"Failed to update untracked project '{projectKey}'."); - return; - - } + // Never tracked the project to begin with, noop. + _logger.LogInformation($"Failed to update untracked project '{projectKey}'."); + return; + } - // If we've been given a project file path, then we have enough info to add the project ourselves, because we know - // the intermediate output path from the id - var intermediateOutputPath = projectKey.Id; + // If we've been given a project file path, then we have enough info to add the project ourselves, because we know + // the intermediate output path from the id + var intermediateOutputPath = projectKey.Id; - var newKey = AddProjectCore(updater, filePath, intermediateOutputPath, configuration, rootNamespace, displayName); - Debug.Assert(newKey == projectKey); + var newKey = AddProjectCore(updater, filePath, intermediateOutputPath, configuration, rootNamespace, displayName); + Debug.Assert(newKey == projectKey); - project = _projectManager.GetLoadedProject(projectKey); - } + project = _projectManager.GetLoadedProject(projectKey); + } - UpdateProjectDocuments(updater, documents, project.Key); + UpdateProjectDocuments(updater, documents, project.Key); - if (!projectWorkspaceState.Equals(ProjectWorkspaceState.Default)) - { - _logger.LogInformation($"Updating project '{project.Key}' TagHelpers ({projectWorkspaceState.TagHelpers.Length}) and C# Language Version ({projectWorkspaceState.CSharpLanguageVersion})."); - } + if (!projectWorkspaceState.Equals(ProjectWorkspaceState.Default)) + { + _logger.LogInformation($"Updating project '{project.Key}' TagHelpers ({projectWorkspaceState.TagHelpers.Length}) and C# Language Version ({projectWorkspaceState.CSharpLanguageVersion})."); + } - updater.ProjectWorkspaceStateChanged(project.Key, projectWorkspaceState); + updater.ProjectWorkspaceStateChanged(project.Key, projectWorkspaceState); - var currentConfiguration = project.Configuration; - var currentRootNamespace = project.RootNamespace; - if (currentConfiguration.ConfigurationName == configuration?.ConfigurationName && - currentRootNamespace == rootNamespace) - { - _logger.LogTrace($"Updating project '{project.Key}'. The project is already using configuration '{configuration.ConfigurationName}' and root namespace '{rootNamespace}'."); - return; - } + var currentConfiguration = project.Configuration; + var currentRootNamespace = project.RootNamespace; + if (currentConfiguration.ConfigurationName == configuration?.ConfigurationName && + currentRootNamespace == rootNamespace) + { + _logger.LogTrace($"Updating project '{project.Key}'. The project is already using configuration '{configuration.ConfigurationName}' and root namespace '{rootNamespace}'."); + return; + } - if (configuration is null) - { - configuration = FallbackRazorConfiguration.Latest; - _logger.LogInformation($"Updating project '{project.Key}' to use the latest configuration ('{configuration.ConfigurationName}')'."); - } - else if (currentConfiguration.ConfigurationName != configuration.ConfigurationName) - { - _logger.LogInformation($"Updating project '{project.Key}' to Razor configuration '{configuration.ConfigurationName}' with language version '{configuration.LanguageVersion}'."); - } + if (configuration is null) + { + configuration = FallbackRazorConfiguration.Latest; + _logger.LogInformation($"Updating project '{project.Key}' to use the latest configuration ('{configuration.ConfigurationName}')'."); + } + else if (currentConfiguration.ConfigurationName != configuration.ConfigurationName) + { + _logger.LogInformation($"Updating project '{project.Key}' to Razor configuration '{configuration.ConfigurationName}' with language version '{configuration.LanguageVersion}'."); + } - if (currentRootNamespace != rootNamespace) - { - _logger.LogInformation($"Updating project '{project.Key}''s root namespace to '{rootNamespace}'."); - } + if (currentRootNamespace != rootNamespace) + { + _logger.LogInformation($"Updating project '{project.Key}''s root namespace to '{rootNamespace}'."); + } - var hostProject = new HostProject(project.FilePath, project.IntermediateOutputPath, configuration, rootNamespace, displayName); - updater.ProjectConfigurationChanged(hostProject); - }, - cancellationToken); + var hostProject = new HostProject(project.FilePath, project.IntermediateOutputPath, configuration, rootNamespace, displayName); + updater.ProjectConfigurationChanged(hostProject); + }, + cancellationToken); } private void UpdateProjectDocuments( diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 91c0d76fe94..ac6830910d5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -44,6 +44,7 @@ internal partial class RazorLanguageServer : NewtonsoftLanguageServer? _configureServices; private readonly RazorLSPOptions _lspOptions; private readonly ILspServerActivationTracker? _lspServerActivationTracker; + private readonly IRazorProjectInfoDriver? _projectInfoDriver; private readonly ITelemetryReporter _telemetryReporter; private readonly ClientConnection _clientConnection; @@ -57,6 +58,7 @@ public RazorLanguageServer( Action? configureServices, RazorLSPOptions? lspOptions, ILspServerActivationTracker? lspServerActivationTracker, + IRazorProjectInfoDriver? projectInfoDriver, ITelemetryReporter telemetryReporter) : base(jsonRpc, serializer, CreateILspLogger(loggerFactory, telemetryReporter)) { @@ -66,6 +68,7 @@ public RazorLanguageServer( _configureServices = configureServices; _lspOptions = lspOptions ?? RazorLSPOptions.Default; _lspServerActivationTracker = lspServerActivationTracker; + _projectInfoDriver = projectInfoDriver; _telemetryReporter = telemetryReporter; _clientConnection = new ClientConnection(_jsonRpc); @@ -110,9 +113,6 @@ protected override ILspServices ConstructLspServices() _configureServices(services); } - services.AddSingleton(); - services.AddSingleton((services) => (IOnInitialized)services.GetRequiredService()); - services.AddSingleton(_clientConnection); // Add the logger as a service in case anything in CLaSP pulls it out to do logging @@ -126,6 +126,15 @@ protected override ILspServices ConstructLspServices() services.AddSingleton(); + if (_projectInfoDriver is { } projectInfoDriver) + { + services.AddSingleton(_projectInfoDriver); + } + else + { + services.AddSingleton(); + } + services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker); services.AddDiagnosticServices(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs index 3cb3c7b2f84..e0ab8f312db 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoListener.cs @@ -9,6 +9,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal interface IRazorProjectInfoListener { - ValueTask RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken); - ValueTask UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken); + Task RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken); + Task UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index 70e49a239c2..fcd004c2fd0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -114,6 +114,7 @@ public event AsyncEventHandler? StopAsync _languageServerFeatureOptions, lspOptions, _lspServerActivationTracker, + projectInfoDriver, traceSource); // This must not happen on an RPC endpoint due to UIThread concerns, so ActivateAsync was chosen. @@ -123,7 +124,6 @@ public event AsyncEventHandler? StopAsync void ConfigureServices(IServiceCollection services) { - services.AddSingleton(projectInfoDriver); services.AddSingleton(new HostServicesProviderAdapter(_vsHostServicesProvider)); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs index 58e9a4a28b8..f0acf1af497 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs @@ -8,10 +8,12 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.Serialization; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Moq; namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; @@ -20,10 +22,27 @@ internal class TestRazorProjectService( IDocumentVersionCache documentVersionCache, IProjectSnapshotManager projectManager, ILoggerFactory loggerFactory) - : RazorProjectService(remoteTextLoaderFactory, documentVersionCache, projectManager, loggerFactory) + : RazorProjectService( + projectManager, + CreateProjectInfoDriver(), + documentVersionCache, + remoteTextLoaderFactory, + loggerFactory) { private readonly IProjectSnapshotManager _projectManager = projectManager; + private static IRazorProjectInfoDriver CreateProjectInfoDriver() + { + var mock = new StrictMock(); + + mock.Setup(x => x.GetLatestProjectInfo()) + .Returns([]); + + mock.Setup(x => x.AddListener(It.IsAny())); + + return mock.Object; + } + public async Task AddDocumentToPotentialProjectsAsync(string textDocumentPath, CancellationToken cancellationToken) { foreach (var projectSnapshot in _projectManager.FindPotentialProjects(textDocumentPath)) @@ -39,19 +58,4 @@ await this.UpdateProjectAsync(projectSnapshot.Key, projectSnapshot.Configuration documents, cancellationToken).ConfigureAwait(false); } } - - private static string GetTargetPath(string documentFilePath, string normalizedProjectPath) - { - var targetFilePath = FilePathNormalizer.Normalize(documentFilePath); - if (targetFilePath.StartsWith(normalizedProjectPath, FilePathComparison.Instance)) - { - // Make relative - targetFilePath = documentFilePath[normalizedProjectPath.Length..]; - } - - // Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations. - var normalizedTargetFilePath = targetFilePath.Replace('/', '\\').TrimStart('\\'); - - return normalizedTargetFilePath; - } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs index 5f867503b4a..356ee12da3b 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs @@ -6,13 +6,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Text; -using Moq; using Xunit; using Xunit.Abstractions; @@ -44,29 +41,34 @@ public async Task ProcessesExistingProjectsDuringInitialization() await projectManager.UpdateAsync(static updater => { updater.ProjectAdded(s_hostProject1); - updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader(s_hostDocument1.FilePath, "

Hello World

")); updater.ProjectAdded(s_hostProject2); - updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader("

Hello World

", s_hostDocument2.FilePath)); + updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader(s_hostDocument2.FilePath, "

Hello World

")); }); var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - // Sort projects by project key. - var latestProjects = driver - .GetLatestProjectInfo() + var latestProjects = driver.GetLatestProjectInfo(); + + // The misc files projects project should be present. + Assert.Contains(latestProjects, x => x.ProjectKey == MiscFilesHostProject.Instance.Key); + + // Sort the remaining projects by project key. + var projects = latestProjects + .WhereAsArray(x => x.ProjectKey != MiscFilesHostProject.Instance.Key) .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); - Assert.Equal(2, latestProjects.Length); + Assert.Equal(2, projects.Length); - var projectInfo1 = latestProjects[0]; + var projectInfo1 = projects[0]; Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); var document1 = Assert.Single(projectInfo1.Documents); Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); - var projectInfo2 = latestProjects[1]; + var projectInfo2 = projects[1]; Assert.Equal(s_hostProject2.Key, projectInfo2.ProjectKey); var document2 = Assert.Single(projectInfo2.Documents); Assert.Equal(s_hostDocument2.FilePath, document2.FilePath); @@ -77,32 +79,42 @@ public async Task ProcessesProjectsAddedAfterInitialization() { var projectManager = CreateProjectSnapshotManager(); - var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + + // The misc files projects project should be present after initialization. + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); + + var initialProjects = driver.GetLatestProjectInfo(); + var miscFilesProject = Assert.Single(initialProjects); + Assert.Equal(MiscFilesHostProject.Instance.Key, miscFilesProject.ProjectKey); + + // Now add some projects await projectManager.UpdateAsync(static updater => { updater.ProjectAdded(s_hostProject1); - updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader(s_hostDocument1.FilePath, "

Hello World

")); updater.ProjectAdded(s_hostProject2); - updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader("

Hello World

", s_hostDocument2.FilePath)); + updater.DocumentAdded(s_hostProject2.Key, s_hostDocument2, CreateTextLoader(s_hostDocument2.FilePath, "

Hello World

")); }); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - // Sort projects by project key. - var latestProjects = publisher + // Sort the non-misc files projects by project key. + var projects = driver .GetLatestProjectInfo() + .WhereAsArray(x => x.ProjectKey != MiscFilesHostProject.Instance.Key) .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); - Assert.Equal(2, latestProjects.Length); + Assert.Equal(2, projects.Length); - var projectInfo1 = latestProjects[0]; + var projectInfo1 = projects[0]; Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); var document1 = Assert.Single(projectInfo1.Documents); Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); - var projectInfo2 = latestProjects[1]; + var projectInfo2 = projects[1]; Assert.Equal(s_hostProject2.Key, projectInfo2.ProjectKey); var document2 = Assert.Single(projectInfo2.Documents); Assert.Equal(s_hostDocument2.FilePath, document2.FilePath); @@ -118,18 +130,22 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await projectManager.UpdateAsync(static updater => { - updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader(s_hostDocument1.FilePath, "

Hello World

")); }); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - var latestProjects = publisher.GetLatestProjectInfo(); + // Sort the non-misc files projects by project key. + var projects = driver + .GetLatestProjectInfo() + .WhereAsArray(x => x.ProjectKey != MiscFilesHostProject.Instance.Key) + .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); - var projectInfo1 = Assert.Single(latestProjects); + var projectInfo1 = Assert.Single(projects); Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); var document1 = Assert.Single(projectInfo1.Documents); Assert.Equal(s_hostDocument1.FilePath, document1.FilePath); @@ -145,13 +161,17 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - var latestProjects = publisher.GetLatestProjectInfo(); + // Sort the non-misc files projects by project key. + var projects = driver + .GetLatestProjectInfo() + .WhereAsArray(x => x.ProjectKey != MiscFilesHostProject.Instance.Key) + .Sort((x, y) => x.ProjectKey.Id.CompareTo(y.ProjectKey.Id)); - var projectInfo1 = Assert.Single(latestProjects); + var projectInfo1 = Assert.Single(projects); Assert.Equal(s_hostProject1.Key, projectInfo1.ProjectKey); await projectManager.UpdateAsync(static updater => @@ -161,7 +181,8 @@ await projectManager.UpdateAsync(static updater => await testAccessor.WaitUntilCurrentBatchCompletesAsync(); - Assert.Empty(publisher.GetLatestProjectInfo()); + var miscFilesProject = Assert.Single(driver.GetLatestProjectInfo()); + Assert.Equal(MiscFilesHostProject.Instance.Key, miscFilesProject.ProjectKey); } [UIFact] @@ -174,14 +195,16 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); var listener = new TestListener(); - publisher.AddListener(listener); + driver.AddListener(listener); await projectManager.UpdateAsync(static updater => { - updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader("

Hello World

", s_hostDocument1.FilePath)); + updater.DocumentAdded(s_hostProject1.Key, s_hostDocument1, CreateTextLoader(s_hostDocument1.FilePath, "

Hello World

")); }); await testAccessor.WaitUntilCurrentBatchCompletesAsync(); @@ -204,10 +227,12 @@ await projectManager.UpdateAsync(static updater => updater.ProjectAdded(s_hostProject1); }); - var (publisher, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + var (driver, testAccessor) = await CreateDriverAndInitializeAsync(projectManager); + + await testAccessor.WaitUntilCurrentBatchCompletesAsync(); var listener = new TestListener(); - publisher.AddListener(listener); + driver.AddListener(listener); await projectManager.UpdateAsync(static updater => { @@ -235,19 +260,6 @@ await projectManager.UpdateAsync(static updater => return (driver, testAccessor); } - private static TextLoader CreateTextLoader(string content, string filePath) - { - var mock = new StrictMock(); - - var sourceText = SourceText.From(content); - var textAndVersion = TextAndVersion.Create(sourceText, VersionStamp.Default, filePath); - - mock.Setup(x => x.LoadTextAndVersionAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(textAndVersion); - - return mock.Object; - } - private sealed class TestListener : IRazorProjectInfoListener { private readonly ImmutableArray.Builder _removes = ImmutableArray.CreateBuilder(); @@ -256,16 +268,16 @@ private sealed class TestListener : IRazorProjectInfoListener public ImmutableArray Removes => _removes.ToImmutable(); public ImmutableArray Updates => _updates.ToImmutable(); - public ValueTask RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken) + public Task RemovedAsync(ProjectKey projectKey, CancellationToken cancellationToken) { _removes.Add(projectKey); - return default; + return Task.CompletedTask; } - public ValueTask UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) + public Task UpdatedAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) { _updates.Add(projectInfo); - return default; + return Task.CompletedTask; } } } From c9d68ae5f3fb5f30f13b7b3b531a1a75dcf980bc Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 7 Jun 2024 13:34:23 -0700 Subject: [PATCH 15/45] Add extra logging and don't attempt to add misc-project a second time --- .../ProjectSystem/RazorProjectService.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index b9e08edd406..e05fa8f5b9f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; /// -/// Maintains the with the semantics of Razor's project model. +/// Maintains the language server's with the semantics of Razor's project model. /// /// /// This service implements both to ensure it is created early and @@ -70,12 +70,7 @@ public void Dispose() private async Task InitializeAsync(CancellationToken cancellationToken) { - // Add the MiscFilesProject - await _projectManager.UpdateAsync( - (updater, miscHostProject) => updater.ProjectAdded(miscHostProject), - state: MiscFilesHostProject.Instance, - cancellationToken) - .ConfigureAwait(false); + _logger.LogTrace($"Initializing {nameof(RazorProjectService)}..."); // Register ourselves as a listener to the project driver. _projectInfoDriver.AddListener(this); @@ -94,6 +89,9 @@ await AddOrUpdateProjectCoreAsync( cancellationToken) .ConfigureAwait(false); } + + _logger.LogTrace($"{nameof(RazorProjectService)} initialized."); + } Task IOnInitialized.OnInitializedAsync(ILspServices services, CancellationToken cancellationToken) @@ -112,6 +110,8 @@ async Task IRazorProjectInfoListener.UpdatedAsync(RazorProjectInfo projectInfo, // Don't update a project during initialization. await WaitForInitializationAsync().ConfigureAwait(false); + _logger.LogTrace($"{nameof(IRazorProjectInfoListener)} received update for {projectInfo.ProjectKey}"); + await AddOrUpdateProjectCoreAsync( projectInfo.ProjectKey, projectInfo.FilePath, @@ -129,6 +129,8 @@ async Task IRazorProjectInfoListener.RemovedAsync(ProjectKey projectKey, Cancell // Don't remove a project during initialization. await WaitForInitializationAsync().ConfigureAwait(false); + _logger.LogTrace($"{nameof(IRazorProjectInfoListener)} received remove for {projectKey}"); + await AddOrUpdateProjectCoreAsync( projectKey, filePath: null, From 7211fd8e2d238a45ccd46efcc6c18859d886c549 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 7 Jun 2024 14:43:28 -0700 Subject: [PATCH 16/45] Remove old RazorProjectInfoPublisher --- .../RazorProjectInfoPublisher.cs | 354 --------- .../RazorProjectInfoPublisherTest.cs | 742 ------------------ 2 files changed, 1096 deletions(-) delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorProjectInfoPublisher.cs delete mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorProjectInfoPublisherTest.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorProjectInfoPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorProjectInfoPublisher.cs deleted file mode 100644 index 4d6c38f27ac..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorProjectInfoPublisher.cs +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.VisualStudio.Razor.LanguageClient; - -/// -/// Publishes project.razor.bin files. -/// -[Export(typeof(IRazorStartupService))] -internal class RazorProjectInfoPublisher : IRazorStartupService -{ - internal readonly Dictionary DeferredPublishTasks; - - // Internal for testing - internal bool _active; - - private const string TempFileExt = ".temp"; - private readonly ILogger _logger; - private readonly LSPEditorFeatureDetector _lspEditorFeatureDetector; - private readonly IProjectSnapshotManager _projectManager; - private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore; - private readonly Dictionary _pendingProjectPublishes; - private readonly object _pendingProjectPublishesLock; - private readonly object _publishLock; - - private bool _documentsProcessed = false; - - [ImportingConstructor] - public RazorProjectInfoPublisher( - LSPEditorFeatureDetector lSPEditorFeatureDetector, - IProjectSnapshotManager projectManager, - ProjectConfigurationFilePathStore projectConfigurationFilePathStore, - ILoggerFactory loggerFactory) - { - DeferredPublishTasks = new Dictionary(FilePathComparer.Instance); - _pendingProjectPublishes = new Dictionary(); - _pendingProjectPublishesLock = new(); - _publishLock = new object(); - - _lspEditorFeatureDetector = lSPEditorFeatureDetector; - _projectConfigurationFilePathStore = projectConfigurationFilePathStore; - _logger = loggerFactory.GetOrCreateLogger(); - - _projectManager = projectManager; - _projectManager.Changed += ProjectManager_Changed; - } - - // Internal settable for testing - // 3000ms between publishes to prevent bursts of changes yet still be responsive to changes. - internal int EnqueueDelay { get; set; } = 3000; - - // Internal for testing - internal void EnqueuePublish(IProjectSnapshot projectSnapshot) - { - lock (_pendingProjectPublishesLock) - { - _pendingProjectPublishes[projectSnapshot.Key] = projectSnapshot; - } - - if (!DeferredPublishTasks.TryGetValue(projectSnapshot.FilePath, out var update) || update.IsCompleted) - { - DeferredPublishTasks[projectSnapshot.FilePath] = PublishAfterDelayAsync(projectSnapshot.Key); - } - } - - internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs args) - { - // Don't do any work if the solution is closing - if (args.SolutionIsClosing) - { - return; - } - - if (!_lspEditorFeatureDetector.IsLSPEditorAvailable()) - { - return; - } - - // Prior to doing any sort of project state serialization/publishing we avoid any excess work as long as there aren't any "open" Razor files. - // However, once a Razor file is opened we turn the firehose on and start doing work. - // By taking this lazy approach we ensure that we don't do any excess Razor work (serialization is expensive) in non-Razor scenarios. - - if (!_active) - { - // Not currently active, we need to decide if we should become active or if we should no-op. - - if (!_projectManager.GetOpenDocuments().IsEmpty) - { - // A Razor document was just opened, we should become "active" which means we'll constantly be monitoring project state. - _active = true; - - if (ProjectWorkspacePublishable(args)) - { - // Typically document open events don't result in us re-processing project state; however, given this is the first time a user opened a Razor document we should. - // Don't enqueue, just publish to get the most immediate result. - ImmediatePublish(args.Newer!); - return; - } - } - else - { - // No open documents and not active. No-op. - return; - } - } - - // All the below Publish's (except ProjectRemoved) wait until our project has been initialized (ProjectWorkspaceState != null) - // so that we don't publish half-finished projects, which can cause things like Semantic coloring to "flash" - // when they update repeatedly as they load. - switch (args.Kind) - { - case ProjectChangeKind.ProjectChanged: - if (!ProjectWorkspacePublishable(args)) - { - break; - } - - if (!ReferenceEquals(args.Newer!.ProjectWorkspaceState, args.Older!.ProjectWorkspaceState)) - { - // If our workspace state has changed since our last snapshot then this means pieces influencing - // TagHelper resolution have also changed. Fast path the TagHelper publish. - ImmediatePublish(args.Newer); - } - else - { - // There was a project change that doesn't seem to be related to TagHelpers, we can be - // less aggressive and do a delayed publish. - EnqueuePublish(args.Newer); - } - - break; - case ProjectChangeKind.DocumentChanged: - // DocumentChanged normally isn't a great trigger for publishing, given that it happens while a user types - // but for a brand new project, its possible this DocumentChanged actually represents a DocumentOpen, and - // it could be the first one, so its important to publish if there is no project configuration file present - if (ProjectWorkspacePublishable(args) && - _projectConfigurationFilePathStore.TryGet(args.ProjectKey, out var configurationFilePath) && - !FileExists(configurationFilePath)) - { - ImmediatePublish(args.Newer!); - } - - break; - - case ProjectChangeKind.DocumentRemoved: - case ProjectChangeKind.DocumentAdded: - - if (ProjectWorkspacePublishable(args)) - { - // These changes can come in bursts so we don't want to overload the publishing system. Therefore, - // we enqueue publishes and then publish the latest project after a delay. - EnqueuePublish(args.Newer!); - } - - break; - - case ProjectChangeKind.ProjectAdded: - - if (ProjectWorkspacePublishable(args)) - { - ImmediatePublish(args.Newer!); - } - - break; - - case ProjectChangeKind.ProjectRemoved: - RemovePublishingData(args.Older!); - break; - - default: - Debug.Fail("A new ProjectChangeKind has been added that the RazorProjectInfoPublisher doesn't know how to deal with"); - break; - } - - static bool ProjectWorkspacePublishable(ProjectChangeEventArgs args) - { - return args.Newer?.ProjectWorkspaceState != null; - } - } - - // Internal for testing - internal void Publish(IProjectSnapshot projectSnapshot) - { - if (projectSnapshot is null) - { - throw new ArgumentNullException(nameof(projectSnapshot)); - } - - lock (_publishLock) - { - string? configurationFilePath = null; - try - { - if (!_projectConfigurationFilePathStore.TryGet(projectSnapshot.Key, out configurationFilePath)) - { - return; - } - - // We don't want to serialize the project until it's ready to avoid flashing as the project loads different parts. - // Since the project configuration from last session likely still exists the experience is unlikely to be degraded by this delay. - // An exception is made for when there's no existing project configuration file because some flashing is preferable to having no TagHelper knowledge. - if (ShouldSerialize(projectSnapshot, configurationFilePath)) - { - SerializeToFile(projectSnapshot, configurationFilePath); - } - } - catch (Exception ex) - { - _logger.LogWarning($@"Could not update Razor project configuration file '{configurationFilePath}': -{ex}"); - } - } - } - - // Internal for testing - internal void RemovePublishingData(IProjectSnapshot projectSnapshot) - { - lock (_publishLock) - { - if (!_projectConfigurationFilePathStore.TryGet(projectSnapshot.Key, out var configurationFilePath)) - { - // If we don't track the value in PublishFilePathMappings that means it's already been removed, do nothing. - return; - } - - lock (_pendingProjectPublishesLock) - { - if (_pendingProjectPublishes.TryGetValue(projectSnapshot.Key, out _)) - { - // Project was removed while a delayed publish was in flight. Clear the in-flight publish so it noops. - _pendingProjectPublishes.Remove(projectSnapshot.Key); - } - } - } - } - - protected virtual void SerializeToFile(IProjectSnapshot projectSnapshot, string configurationFilePath) - { - // We need to avoid having an incomplete file at any point, but our - // project configuration file is large enough that it will be written as multiple operations. - var tempFilePath = string.Concat(configurationFilePath, TempFileExt); - var tempFileInfo = new FileInfo(tempFilePath); - - if (tempFileInfo.Exists) - { - // This could be caused by failures during serialization or early process termination. - tempFileInfo.Delete(); - } - - // This needs to be in explicit brackets because the operation needs to be completed - // by the time we move the tempfile into its place - using (var stream = tempFileInfo.Create()) - { - var projectInfo = projectSnapshot.ToRazorProjectInfo(); - projectInfo.SerializeTo(stream); - } - - var fileInfo = new FileInfo(configurationFilePath); - if (fileInfo.Exists) - { - fileInfo.Delete(); - } - - File.Move(tempFilePath, configurationFilePath); - } - - protected virtual bool FileExists(string file) - { - return File.Exists(file); - } - - protected virtual bool ShouldSerialize(IProjectSnapshot projectSnapshot, string configurationFilePath) - { - if (!FileExists(configurationFilePath)) - { - return true; - } - - // Don't serialize our understanding until we're "ready" - if (!_documentsProcessed) - { - if (projectSnapshot.DocumentFilePaths.Any(d => AspNetCore.Razor.Language.FileKinds.GetFileKindFromFilePath(d) - .Equals(AspNetCore.Razor.Language.FileKinds.Component, StringComparison.OrdinalIgnoreCase))) - { - foreach (var documentFilePath in projectSnapshot.DocumentFilePaths) - { - // We want to wait until at least one document has been processed (meaning it became a TagHelper. - // Because we don't have a way to tell which TagHelpers were created from the local project just from their descriptors we have to improvise - // We assume that a document has been processed if at least one Component matches the name of one of our files. - var fileName = Path.GetFileNameWithoutExtension(documentFilePath); - - if (projectSnapshot.GetDocument(documentFilePath) is { } documentSnapshot && - string.Equals(documentSnapshot.FileKind, AspNetCore.Razor.Language.FileKinds.Component, StringComparison.OrdinalIgnoreCase) && - projectSnapshot.GetTagHelpersSynchronously().Any(t => t.Name.EndsWith("." + fileName, StringComparison.OrdinalIgnoreCase))) - { - // Documents have been processed, lets publish - _documentsProcessed = true; - break; - } - } - } - else - { - // This project has no Components and thus cannot suffer from the lagging compilation problem. - _documentsProcessed = true; - } - } - - return _documentsProcessed; - } - - private void ImmediatePublish(IProjectSnapshot projectSnapshot) - { - lock (_pendingProjectPublishesLock) - { - // Clear any pending publish - _pendingProjectPublishes.Remove(projectSnapshot.Key); - } - - Publish(projectSnapshot); - } - - private async Task PublishAfterDelayAsync(ProjectKey projectKey) - { - await Task.Delay(EnqueueDelay).ConfigureAwait(false); - - IProjectSnapshot projectSnapshot; - lock (_pendingProjectPublishesLock) - { - if (!_pendingProjectPublishes.TryGetValue(projectKey, out projectSnapshot)) - { - // Project was removed while waiting for the publish delay. - return; - } - - _pendingProjectPublishes.Remove(projectKey); - } - - Publish(projectSnapshot); - } -} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorProjectInfoPublisherTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorProjectInfoPublisherTest.cs deleted file mode 100644 index 9f92a5e4d01..00000000000 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorProjectInfoPublisherTest.cs +++ /dev/null @@ -1,742 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Immutable; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CodeAnalysis.Text; -using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Microsoft.VisualStudio.Razor.LanguageClient; - -public class RazorProjectInfoPublisherTest(ITestOutputHelper testOutput) : VisualStudioWorkspaceTestBase(testOutput) -{ - [Fact] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/35945")] - public async Task ProjectManager_Changed_Remove_Change_NoopsOnDelayedPublish() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var tagHelpers = ImmutableArray.Create( - new TagHelperDescriptor(FileKinds.Component, "Namespace.FileNameOther", "Assembly", "FileName", "FileName document", "FileName hint", - caseSensitive: false, tagMatchingRules: default, attributeDescriptors: default, allowedChildTags: default, metadata: null!, diagnostics: default)); - - var initialProjectSnapshot = CreateProjectSnapshot( - @"C:\path\to\project.csproj", ProjectWorkspaceState.Create(tagHelpers, CodeAnalysis.CSharp.LanguageVersion.Preview)); - var expectedProjectSnapshot = CreateProjectSnapshot( - @"C:\path\to\project.csproj", ProjectWorkspaceState.Create(CodeAnalysis.CSharp.LanguageVersion.Preview)); - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Same(expectedProjectSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(expectedProjectSnapshot.Key, expectedConfigurationFilePath); - var documentRemovedArgs = ProjectChangeEventArgs.CreateTestInstance( - initialProjectSnapshot, initialProjectSnapshot, documentFilePath: @"C:\path\to\file.razor", ProjectChangeKind.DocumentRemoved); - var projectChangedArgs = ProjectChangeEventArgs.CreateTestInstance( - initialProjectSnapshot, expectedProjectSnapshot, documentFilePath: null!, ProjectChangeKind.ProjectChanged); - - // Act - publisher.ProjectManager_Changed(null!, documentRemovedArgs); - publisher.ProjectManager_Changed(null!, projectChangedArgs); - - // Assert - var stalePublishTask = Assert.Single(publisher.DeferredPublishTasks); - await stalePublishTask.Value; - Assert.True(serializationSuccessful); - } - - [Fact] - public async Task ProjectManager_Changed_NotActive_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var attemptedToSerialize = false; - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); - var hostDocument = new HostDocument(@"C:\path\to\file.razor", "file.razor"); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - }); - - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => attemptedToSerialize = true) - { - EnqueueDelay = 10, - }; - - // Act - await projectManager.UpdateAsync(updater => - { - updater.DocumentAdded(hostProject.Key, hostDocument, new EmptyTextLoader(hostDocument.FilePath)); - }); - - // Assert - Assert.Empty(publisher.DeferredPublishTasks); - Assert.False(attemptedToSerialize); - } - - [Fact] - public async Task ProjectManager_Changed_DocumentOpened_UninitializedProject_NotActive_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var attemptedToSerialize = false; - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); - var hostDocument = new HostDocument(@"C:\path\to\file.razor", "file.razor"); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.DocumentAdded(hostProject.Key, hostDocument, new EmptyTextLoader(hostDocument.FilePath)); - }); - - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => attemptedToSerialize = true) - { - EnqueueDelay = 10, - }; - - // Act - await projectManager.UpdateAsync(updater => - { - updater.DocumentOpened(hostProject.Key, hostDocument.FilePath, SourceText.From(string.Empty)); - }); - - // Assert - Assert.Empty(publisher.DeferredPublishTasks); - Assert.False(attemptedToSerialize); - } - - [Fact] - public async Task ProjectManager_Changed_DocumentOpened_InitializedProject_NotActive_Publishes() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); - var hostDocument = new HostDocument(@"C:\path\to\file.razor", "file.razor"); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Default); - updater.DocumentAdded(hostProject.Key, hostDocument, new EmptyTextLoader(hostDocument.FilePath)); - }); - - var projectSnapshot = projectManager.GetProjects()[0]; - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - projectConfigurationFilePathStore.Set(projectSnapshot.Key, expectedConfigurationFilePath); - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - EnqueueDelay = 10, - }; - - // Act - await projectManager.UpdateAsync(updater => - { - updater.DocumentOpened(hostProject.Key, hostDocument.FilePath, SourceText.From(string.Empty)); - }); - - // Assert - Assert.Empty(publisher.DeferredPublishTasks); - Assert.True(serializationSuccessful); - } - - [Fact] - public async Task ProjectManager_Changed_DocumentOpened_InitializedProject_NoFile_Active_Publishes() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); - var hostDocument = new HostDocument(@"C:\path\to\file.razor", "file.razor"); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Default); - updater.DocumentAdded(hostProject.Key, hostDocument, new EmptyTextLoader(hostDocument.FilePath)); - }); - - var projectSnapshot = projectManager.GetProjects()[0]; - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - projectConfigurationFilePathStore.Set(projectSnapshot.Key, expectedConfigurationFilePath); - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }, - configurationFileExists: false) - { - EnqueueDelay = 10000, // Long enqueue delay to make sure this test doesn't pass due to slow running, but broken product code - _active = true - }; - - // Act - await projectManager.UpdateAsync(updater => - { - updater.DocumentOpened(hostProject.Key, hostDocument.FilePath, SourceText.From(string.Empty)); - }); - - // Assert - Assert.Empty(publisher.DeferredPublishTasks); - Assert.True(serializationSuccessful); - } - - [Theory] - [InlineData(ProjectChangeKind.DocumentAdded)] - [InlineData(ProjectChangeKind.DocumentRemoved)] - [InlineData(ProjectChangeKind.ProjectChanged)] - internal async Task ProjectManager_Changed_EnqueuesPublishAsync(ProjectChangeKind changeKind) - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var projectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", ProjectWorkspaceState.Create(CodeAnalysis.CSharp.LanguageVersion.CSharp7_3)); - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Same(projectSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(projectSnapshot.Key, expectedConfigurationFilePath); - var args = ProjectChangeEventArgs.CreateTestInstance(projectSnapshot, projectSnapshot, documentFilePath: null!, changeKind); - - // Act - publisher.ProjectManager_Changed(null!, args); - - // Assert - var kvp = Assert.Single(publisher.DeferredPublishTasks); - await kvp.Value; - Assert.True(serializationSuccessful); - } - - [Fact] - internal async Task ProjectManager_ChangedTagHelpers_PublishesImmediately() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var projectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", ProjectWorkspaceState.Default); - var changedProjectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", ProjectWorkspaceState.Create(CodeAnalysis.CSharp.LanguageVersion.CSharp8)); - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var aboutToChange = false; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - if (!aboutToChange) - { - return; - } - - Assert.Same(changedProjectSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(projectSnapshot.Key, expectedConfigurationFilePath); - var args = ProjectChangeEventArgs.CreateTestInstance(projectSnapshot, projectSnapshot, documentFilePath: null!, ProjectChangeKind.ProjectChanged); - publisher.ProjectManager_Changed(null!, args); - - // Flush publish task - var kvp = Assert.Single(publisher.DeferredPublishTasks); - await kvp.Value; - aboutToChange = true; - publisher.DeferredPublishTasks.Clear(); - - var changedTagHelpersArgs = ProjectChangeEventArgs.CreateTestInstance( - projectSnapshot, changedProjectSnapshot, documentFilePath: null!, ProjectChangeKind.ProjectChanged); - - // Act - publisher.ProjectManager_Changed(null!, changedTagHelpersArgs); - - // Assert - Assert.Empty(publisher.DeferredPublishTasks); - Assert.True(serializationSuccessful); - } - - [Fact] - public async Task ProjectManager_Changed_ProjectRemoved_AfterEnqueuedPublishAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var attemptedToSerialize = false; - var projectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => attemptedToSerialize = true) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(projectSnapshot.Key, expectedConfigurationFilePath); - publisher.EnqueuePublish(projectSnapshot); - var args = ProjectChangeEventArgs.CreateTestInstance(projectSnapshot, newer: null!, documentFilePath: null!, ProjectChangeKind.ProjectRemoved); - - // Act - publisher.ProjectManager_Changed(null!, args); - - // Assert - var kvp = Assert.Single(publisher.DeferredPublishTasks); - await kvp.Value; - - Assert.False(attemptedToSerialize); - } - - [Fact] - public async Task EnqueuePublish_BatchesPublishRequestsAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var firstSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - var secondSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", [@"C:\path\to\file.cshtml"]); - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Same(secondSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(firstSnapshot.Key, expectedConfigurationFilePath); - - // Act - publisher.EnqueuePublish(firstSnapshot); - publisher.EnqueuePublish(secondSnapshot); - - // Assert - var kvp = Assert.Single(publisher.DeferredPublishTasks); - await kvp.Value; - Assert.True(serializationSuccessful); - } - - [Fact] - public async Task EnqueuePublish_OnProjectWithoutRazor_Publishes() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var firstSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - var secondSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", [@"C:\path\to\file.cshtml"]); - var expectedConfigurationFilePath = @"C:\path\to\objbin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Same(secondSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }, - useRealShouldSerialize: true) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(secondSnapshot.Key, expectedConfigurationFilePath); - - // Act - publisher.EnqueuePublish(secondSnapshot); - - // Assert - var kvp = Assert.Single(publisher.DeferredPublishTasks); - await kvp.Value; - Assert.True(serializationSuccessful); - } - - [Fact] - public async Task EnqueuePublish_OnProjectBeforeTagHelperProcessed_DoesNotPublish() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var firstSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - var tagHelpers = ImmutableArray.Create( - new TagHelperDescriptor(FileKinds.Component, "Namespace.FileNameOther", "Assembly", "FileName", "FileName document", "FileName hint", - caseSensitive: false, tagMatchingRules: default, attributeDescriptors: default, allowedChildTags: default, metadata: null!, diagnostics: default)); - - var secondSnapshot = CreateProjectSnapshot( - @"C:\path\to\project.csproj", - ProjectWorkspaceState.Create(tagHelpers, CodeAnalysis.CSharp.LanguageVersion.CSharp8), - ["FileName.razor"]); - - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Same(secondSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }, - useRealShouldSerialize: true) - { - EnqueueDelay = 10, - _active = true, - }; - - projectConfigurationFilePathStore.Set(firstSnapshot.Key, expectedConfigurationFilePath); - - // Act - publisher.EnqueuePublish(secondSnapshot); - - // Assert - var kvp = Assert.Single(publisher.DeferredPublishTasks); - await kvp.Value; - Assert.False(serializationSuccessful); - } - - [Fact] - public void Publish_UnsetConfigurationFilePath_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var publisher = new TestRazorProjectInfoPublisher(projectManager, projectConfigurationFilePathStore, LoggerFactory) - { - _active = true, - }; - - var omniSharpProjectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - - // Act & Assert - publisher.Publish(omniSharpProjectSnapshot); - } - - [Fact] - public void Publish_PublishesToSetPublishFilePath() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var omniSharpProjectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Same(omniSharpProjectSnapshot, snapshot); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - _active = true, - }; - - projectConfigurationFilePathStore.Set(omniSharpProjectSnapshot.Key, expectedConfigurationFilePath); - - // Act - publisher.Publish(omniSharpProjectSnapshot); - - // Assert - Assert.True(serializationSuccessful); - } - - [UIFact] - public async Task ProjectAdded_PublishesToCorrectFilePathAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }) - { - _active = true, - }; - - var projectFilePath = @"C:\path\to\project.csproj"; - var hostProject = new HostProject(projectFilePath, Path.Combine(Path.GetDirectoryName(projectFilePath), "obj"), RazorConfiguration.Default, "TestRootNamespace"); - projectConfigurationFilePathStore.Set(hostProject.Key, expectedConfigurationFilePath); - var projectWorkspaceState = ProjectWorkspaceState.Default; - - // Act - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.ProjectWorkspaceStateChanged(hostProject.Key, projectWorkspaceState); - }); - - // Assert - Assert.True(serializationSuccessful); - } - - [UIFact] - public async Task ProjectAdded_DoesNotPublishWithoutProjectWorkspaceStateAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Fail("Serialization should not have been attempted because there is no ProjectWorkspaceState."); - serializationSuccessful = true; - }) - { - _active = true, - }; - - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, "TestRootNamespace"); - projectConfigurationFilePathStore.Set(hostProject.Key, expectedConfigurationFilePath); - - // Act - await projectManager.UpdateAsync( - updater => updater.ProjectAdded(hostProject)); - - Assert.Empty(publisher.DeferredPublishTasks); - - // Assert - Assert.False(serializationSuccessful); - } - - [UIFact] - public async Task ProjectRemoved_UnSetPublishFilePath_NoopsAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var publisher = new TestRazorProjectInfoPublisher(projectManager, projectConfigurationFilePathStore, LoggerFactory) - { - _active = true, - }; - - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, "TestRootNamespace"); - await projectManager.UpdateAsync( - updater => updater.ProjectAdded(hostProject)); - - // Act & Assert - await projectManager.UpdateAsync( - updater => updater.ProjectRemoved(hostProject.Key)); - - Assert.Empty(publisher.DeferredPublishTasks); - } - - [UIFact] - public async Task ProjectAdded_DoesNotFireWhenNotReadyAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var serializationSuccessful = false; - var expectedConfigurationFilePath = @"C:\path\to\obj\bin\Debug\project.razor.bin"; - - var publisher = new TestRazorProjectInfoPublisher( - projectManager, - projectConfigurationFilePathStore, - LoggerFactory, - onSerializeToFile: (snapshot, configurationFilePath) => - { - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - serializationSuccessful = true; - }, - shouldSerialize: false) - { - _active = true, - }; - - var projectFilePath = @"C:\path\to\project.csproj"; - var hostProject = new HostProject( - projectFilePath, - Path.Combine(Path.GetDirectoryName(projectFilePath), "obj"), - RazorConfiguration.Default, - "TestRootNamespace"); - projectConfigurationFilePathStore.Set(hostProject.Key, expectedConfigurationFilePath); - var projectWorkspaceState = ProjectWorkspaceState.Default; - - // Act - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.ProjectWorkspaceStateChanged(hostProject.Key, projectWorkspaceState); - }); - - // Assert - Assert.False(serializationSuccessful); - } - - internal static IProjectSnapshot CreateProjectSnapshot( - string projectFilePath, - ProjectWorkspaceState? projectWorkspaceState = null, - string[]? documentFilePaths = null) - { - return TestProjectSnapshot.Create(projectFilePath, documentFilePaths ?? [], projectWorkspaceState); - } - - internal static IProjectSnapshot CreateProjectSnapshot(string projectFilePath, string[] documentFilePaths) - { - return TestProjectSnapshot.Create(projectFilePath, documentFilePaths); - } - - private class TestRazorProjectInfoPublisher( - IProjectSnapshotManager projectManager, - ProjectConfigurationFilePathStore projectStatePublishFilePathStore, - ILoggerFactory loggerFactory, - Action? onSerializeToFile = null, - bool shouldSerialize = true, - bool useRealShouldSerialize = false, - bool configurationFileExists = true) - : RazorProjectInfoPublisher( - s_lspEditorFeatureDetector.Object, - projectManager, - projectStatePublishFilePathStore, - loggerFactory) - { - private static readonly StrictMock s_lspEditorFeatureDetector = new(); - - private readonly Action _onSerializeToFile = onSerializeToFile ?? ((_1, _2) => throw new XunitException("SerializeToFile should not have been called.")); - - private readonly bool _shouldSerialize = shouldSerialize; - private readonly bool _useRealShouldSerialize = useRealShouldSerialize; - private readonly bool _configurationFileExists = configurationFileExists; - - static TestRazorProjectInfoPublisher() - { - s_lspEditorFeatureDetector - .Setup(t => t.IsLSPEditorAvailable()) - .Returns(true); - } - - protected override bool FileExists(string file) - { - return _configurationFileExists; - } - - protected override void SerializeToFile(IProjectSnapshot projectSnapshot, string configurationFilePath) => _onSerializeToFile?.Invoke(projectSnapshot, configurationFilePath); - - protected override bool ShouldSerialize(IProjectSnapshot projectSnapshot, string configurationFilePath) - { - if (_useRealShouldSerialize) - { - return base.ShouldSerialize(projectSnapshot, configurationFilePath); - } - else - { - return _shouldSerialize; - } - } - } -} From f27289748ab8b248e3a09cb5cb3abc5550871b1e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 7 Jun 2024 14:49:03 -0700 Subject: [PATCH 17/45] Improve ProjectWorkspaceStateGenerator logging --- .../ProjectSystem/ProjectKey.cs | 3 +++ .../ProjectWorkspaceStateGenerator.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/ProjectKey.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/ProjectKey.cs index 9773b9c537b..05756be6005 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/ProjectKey.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/ProjectKey.cs @@ -35,4 +35,7 @@ public bool Equals(ProjectKey other) public override int GetHashCode() => IsUnknown ? 0 : FilePathComparer.Instance.GetHashCode(Id); + + public override string ToString() + => IsUnknown ? "" : Id; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectWorkspaceStateGenerator.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectWorkspaceStateGenerator.cs index d35bc554844..e6b29390c64 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectWorkspaceStateGenerator.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectWorkspaceStateGenerator.cs @@ -212,6 +212,11 @@ private async Task TryEnterSemaphoreAsync(ProjectKey projectKey, Cancellat _logger.LogTrace($"Entered semaphore for '{projectKey}'"); return true; } + catch (OperationCanceledException) + { + _logger.LogTrace($"Update cancelled before entering semaphore for '{projectKey}'"); + return false; + } catch (Exception ex) { // Swallow object and task cancelled exceptions From 7d199604a09c104144b44544b8ef52114824100f Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 09:17:45 -0700 Subject: [PATCH 18/45] Remove RazorProjectInfoEndpointPublisher --- ...orProjectInfoEndpointPublisher.Comparer.cs | 45 --- ...ojectInfoEndpointPublisher.TestAccessor.cs | 34 -- .../RazorProjectInfoEndpointPublisher.cs | 182 ----------- .../RazorLanguageServerClient.cs | 19 +- .../RazorProjectInfoEndpointPublisherTest.cs | 297 ------------------ 5 files changed, 1 insertion(+), 576 deletions(-) delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.Comparer.cs delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.TestAccessor.cs delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.cs delete mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisherTest.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.Comparer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.Comparer.cs deleted file mode 100644 index 38f4f258fd3..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.Comparer.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; - -internal partial class RazorProjectInfoEndpointPublisher -{ - /// - /// Compares two work items from project info publishing work queue. - /// - /// - /// Project updates and project removal are treated as equivalent since project removal - /// work item should supersede any project updates in the queue. Project additions are not - /// placed in the queue so project removal would never supersede/overwrite project addition - /// (which would've resulted in us removing a project we never added). - /// - private sealed class Comparer : IEqualityComparer<(IProjectSnapshot Project, bool Removal)> - { - public static readonly Comparer Instance = new(); - - private Comparer() - { - } - - public bool Equals((IProjectSnapshot Project, bool Removal) x, (IProjectSnapshot Project, bool Removal) y) - { - // Project removal should replace project update so treat Removal and non-Removal - // of the same IProjectSnapshot as equivalent work item - var (snapshotX, _) = x; - var (snapshotY, _) = y; - - return snapshotX.Key.Equals(snapshotY.Key); - } - - public int GetHashCode((IProjectSnapshot Project, bool Removal) obj) - { - var (snapshot, _) = obj; - - return snapshot.Key.GetHashCode(); - } - } -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.TestAccessor.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.TestAccessor.cs deleted file mode 100644 index d97f96672dc..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.TestAccessor.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; - -internal partial class RazorProjectInfoEndpointPublisher -{ - internal TestAccessor GetTestAccessor() - => new(this); - - internal sealed class TestAccessor(RazorProjectInfoEndpointPublisher instance) - { - /// - /// Allows unit tests to imitate ProjectManager.Changed event firing - /// - public void ProjectManager_Changed(object sender, ProjectChangeEventArgs args) - => instance.ProjectManager_Changed(sender, args); - - /// - /// Allows unit tests to enqueue project update directly. - /// - public void EnqueuePublish(IProjectSnapshot projectSnapshot) - => instance.EnqueuePublish(projectSnapshot); - - /// - /// Allows unit tests to wait for all work queue items to be processed. - /// - public Task WaitUntilCurrentBatchCompletesAsync() - => instance._workQueue.WaitUntilCurrentBatchCompletesAsync(); - } -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.cs deleted file mode 100644 index c22b5ec742f..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisher.cs +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Serialization; -using Microsoft.CodeAnalysis.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Microsoft.VisualStudio.Threading; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; - -/// -/// Publishes project data (including TagHelper info) discovered OOB to the server via LSP notification -/// instead of old method of writing a project configuration bin file -/// -[Export(typeof(RazorProjectInfoEndpointPublisher))] -internal partial class RazorProjectInfoEndpointPublisher : IDisposable -{ - private readonly LSPRequestInvoker _requestInvoker; - private readonly IProjectSnapshotManager _projectManager; - private readonly IRazorProjectInfoFileSerializer _serializer; - - private readonly AsyncBatchingWorkQueue<(IProjectSnapshot Project, bool Removal)> _workQueue; - private readonly CancellationTokenSource _disposeTokenSource; - - // Delay between publishes to prevent bursts of changes yet still be responsive to changes. - private static readonly TimeSpan s_enqueueDelay = TimeSpan.FromMilliseconds(250); - - [ImportingConstructor] - public RazorProjectInfoEndpointPublisher( - LSPRequestInvoker requestInvoker, - IProjectSnapshotManager projectManager, - IRazorProjectInfoFileSerializer serializer) - : this(requestInvoker, projectManager, serializer, s_enqueueDelay) - { - } - - // Provided for tests to specify enqueue delay - public RazorProjectInfoEndpointPublisher( - LSPRequestInvoker requestInvoker, - IProjectSnapshotManager projectManager, - IRazorProjectInfoFileSerializer serializer, - TimeSpan enqueueDelay) - { - _requestInvoker = requestInvoker; - _projectManager = projectManager; - _serializer = serializer; - - _disposeTokenSource = new(); - _workQueue = new( - enqueueDelay, - ProcessBatchAsync, - _disposeTokenSource.Token); - } - - public void Dispose() - { - _disposeTokenSource.Cancel(); - _disposeTokenSource.Dispose(); - } - - public void StartSending() - { - _projectManager.Changed += ProjectManager_Changed; - - var projects = _projectManager.GetProjects(); - - PublishProjectsAsync(projects, _disposeTokenSource.Token).Forget(); - } - - private void ProjectManager_Changed(object sender, ProjectChangeEventArgs args) - { - // Don't do any work if the solution is closing - if (args.SolutionIsClosing) - { - return; - } - - switch (args.Kind) - { - case ProjectChangeKind.ProjectChanged: - case ProjectChangeKind.DocumentRemoved: - case ProjectChangeKind.DocumentAdded: - EnqueuePublish(args.Newer.AssumeNotNull()); - - break; - - case ProjectChangeKind.ProjectAdded: - // Don't enqueue project addition as those are unlikely to come through in large batches. - // Also ensures that we won't get project removal go through the queue without project addition. - PublishProjectsAsync([args.Newer.AssumeNotNull()], _disposeTokenSource.Token).Forget(); - - break; - - case ProjectChangeKind.ProjectRemoved: - // Enqueue removal so it will replace any other project changes in the work queue as they unnecessary now. - EnqueueRemoval(args.Older.AssumeNotNull()); - break; - - case ProjectChangeKind.DocumentChanged: - break; - - default: - Debug.Fail("A new ProjectChangeKind has been added that the RazorProjectInfoEndpointPublisher doesn't know how to deal with"); - break; - } - } - - private void EnqueuePublish(IProjectSnapshot projectSnapshot) - { - _workQueue.AddWork((Project: projectSnapshot, Removal: false)); - } - - private void EnqueueRemoval(IProjectSnapshot projectSnapshot) - { - _workQueue.AddWork((Project: projectSnapshot, Removal: true)); - } - - private async ValueTask ProcessBatchAsync(ImmutableArray<(IProjectSnapshot, bool)> items, CancellationToken cancellationToken) - { - using var projectKeyIds = new PooledArrayBuilder(capacity: items.Length); - using var filePaths = new PooledArrayBuilder(capacity: items.Length); - - foreach (var (project, removal) in items.GetMostRecentUniqueItems(Comparer.Instance)) - { - string? filePath = null; - - if (!removal) - { - var projectInfo = project.ToRazorProjectInfo(); - filePath = await _serializer.SerializeToTempFileAsync(projectInfo, cancellationToken).ConfigureAwait(false); - } - - projectKeyIds.Add(project.Key.Id); - filePaths.Add(filePath); - } - - await SendRequestAsync(projectKeyIds.ToArray(), filePaths.ToArray(), cancellationToken); - } - - private async Task PublishProjectsAsync(ImmutableArray projects, CancellationToken cancellationToken) - { - using var projectKeyIds = new PooledArrayBuilder(capacity: projects.Length); - using var filePaths = new PooledArrayBuilder(capacity: projects.Length); - - foreach (var project in projects) - { - var projectInfo = project.ToRazorProjectInfo(); - var filePath = await _serializer.SerializeToTempFileAsync(projectInfo, cancellationToken).ConfigureAwait(false); - - projectKeyIds.Add(project.Key.Id); - filePaths.Add(filePath); - } - - await SendRequestAsync(projectKeyIds.ToArray(), filePaths.ToArray(), cancellationToken); - } - - private Task SendRequestAsync(string[] projectKeyIds, string?[] filePaths, CancellationToken cancellationToken) - { - var parameter = new ProjectInfoParams - { - ProjectKeyIds = projectKeyIds, - FilePaths = filePaths - }; - - return _requestInvoker.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorProjectInfoEndpoint, - RazorLSPConstants.RazorLanguageServerName, - parameter, - cancellationToken); - } -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index fcd004c2fd0..445b3832e96 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -15,7 +15,6 @@ using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Client; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.Razor.LanguageClient.Endpoints; using Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; using Microsoft.VisualStudio.Razor.Logging; @@ -33,8 +32,6 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient; [method: ImportingConstructor] internal class RazorLanguageServerClient( RazorCustomMessageTarget customTarget, - LSPRequestInvoker requestInvoker, - RazorProjectInfoEndpointPublisher projectInfoEndpointPublisher, IProjectSnapshotManager projectManager, ILoggerFactory loggerFactory, RazorLogHubTraceProvider traceProvider, @@ -53,8 +50,6 @@ internal class RazorLanguageServerClient( private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; private readonly ILspServerActivationTracker _lspServerActivationTracker = lspServerActivationTracker; private readonly RazorCustomMessageTarget _customMessageTarget = customTarget; - private readonly LSPRequestInvoker _requestInvoker = requestInvoker; - private readonly RazorProjectInfoEndpointPublisher _projectInfoEndpointPublisher = projectInfoEndpointPublisher; private readonly IProjectSnapshotManager _projectManager = projectManager; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly VisualStudioHostServicesProvider _vsHostServicesProvider = vsHostServicesProvider; @@ -230,19 +225,7 @@ public Task OnLoadedAsync() } public Task OnServerInitializedAsync() - { - ServerStarted(); - - return Task.CompletedTask; - } - - private void ServerStarted() - { - if (_languageServerFeatureOptions.UseProjectConfigurationEndpoint) - { - _projectInfoEndpointPublisher.StartSending(); - } - } + => Task.CompletedTask; private sealed class HostServicesProviderAdapter(VisualStudioHostServicesProvider vsHostServicesProvider) : IHostServicesProvider { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisherTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisherTest.cs deleted file mode 100644 index 353e50027bf..00000000000 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoEndpointPublisherTest.cs +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Serialization; -using Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Moq; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; - -public class RazorProjectInfoEndpointPublisherTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) -{ - [Fact] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/35945")] - public async Task ProjectManager_Changed_Remove_Change_NoopsOnDelayedPublish() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - - var callCount = 0; - var tagHelpers = ImmutableArray.Create( - new TagHelperDescriptor(FileKinds.Component, "Namespace.FileNameOther", "Assembly", "FileName", "FileName document", "FileName hint", - caseSensitive: false, tagMatchingRules: default, attributeDescriptors: default, allowedChildTags: default, metadata: null!, diagnostics: default)); - - var initialProjectSnapshot = CreateProjectSnapshot( - @"C:\path\to\project.csproj", ProjectWorkspaceState.Create(tagHelpers, CodeAnalysis.CSharp.LanguageVersion.Preview)); - var expectedProjectSnapshot = CreateProjectSnapshot( - @"C:\path\to\project.csproj", ProjectWorkspaceState.Create(CodeAnalysis.CSharp.LanguageVersion.Preview)); - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorProjectInfoEndpoint, - RazorLSPConstants.RazorLanguageServerName, - It.IsAny(), - It.IsAny())) - .Callback((s1, s2, param, ct) => callCount++) - .ReturnsAsync(new ReinvokeResponse()); - - var serializer = new TestRazorProjectInfoFileSerializer(); - - using var publisher = CreateRazorProjectInfoEndpointPublisher(requestInvoker.Object, projectManager, serializer); - var publisherAccessor = publisher.GetTestAccessor(); - - var documentRemovedArgs = ProjectChangeEventArgs.CreateTestInstance( - initialProjectSnapshot, initialProjectSnapshot, documentFilePath: @"C:\path\to\file.razor", ProjectChangeKind.DocumentRemoved); - var projectChangedArgs = ProjectChangeEventArgs.CreateTestInstance( - initialProjectSnapshot, expectedProjectSnapshot, documentFilePath: null!, ProjectChangeKind.ProjectChanged); - - // Act - publisherAccessor.ProjectManager_Changed(null!, documentRemovedArgs); - publisherAccessor.ProjectManager_Changed(null!, projectChangedArgs); - await publisherAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - Assert.Equal(1, callCount); - } - - [Fact] - public async Task ProjectManager_Changed_NotActive_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); - var hostDocument = new HostDocument(@"C:\path\to\file.razor", "file.razor"); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - }); - - var callCount = 0; - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorProjectInfoEndpoint, - RazorLSPConstants.RazorLanguageServerName, - It.IsAny(), - It.IsAny())) - .Callback((s1, s2, param, ct) => callCount++) - .ReturnsAsync(new ReinvokeResponse()); - - var serializer = new TestRazorProjectInfoFileSerializer(); - - using var publisher = CreateRazorProjectInfoEndpointPublisher(requestInvoker.Object, projectManager, serializer); - var publisherAccessor = publisher.GetTestAccessor(); - - // Act - await projectManager.UpdateAsync(updater => - { - updater.DocumentAdded(hostProject.Key, hostDocument, new EmptyTextLoader(hostDocument.FilePath)); - }); - await publisherAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - Assert.Equal(0, callCount); - } - - [Fact] - public async Task ProjectManager_Changed_ServerStarted_InitializedProject_Publishes() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - - var hostProject = new HostProject(@"C:\path\to\project.csproj", @"C:\path\to\obj", RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); - var hostDocument = new HostDocument(@"C:\path\to\file.razor", "file.razor"); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Default); - updater.DocumentAdded(hostProject.Key, hostDocument, new EmptyTextLoader(hostDocument.FilePath)); - }); - - var projectSnapshot = projectManager.GetProjects()[0]; - - var callCount = 0; - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorProjectInfoEndpoint, - RazorLSPConstants.RazorLanguageServerName, - It.IsAny(), - It.IsAny())) - .Callback((s1, s2, param, ct) => callCount++) - .ReturnsAsync(new ReinvokeResponse()); - - var serializer = new TestRazorProjectInfoFileSerializer(); - - using var publisher = CreateRazorProjectInfoEndpointPublisher(requestInvoker.Object, projectManager, serializer); - - // Act - publisher.StartSending(); - - // Assert - Assert.Equal(1, callCount); - } - - [Theory] - [InlineData(ProjectChangeKind.DocumentAdded, true, false)] - [InlineData(ProjectChangeKind.DocumentRemoved, true, false)] - [InlineData(ProjectChangeKind.ProjectChanged, true, false)] - [InlineData(ProjectChangeKind.ProjectRemoved, true, true)] - [InlineData(ProjectChangeKind.ProjectAdded, false, false)] - internal async Task ProjectManager_Changed_EnqueuesPublishAsync(ProjectChangeKind changeKind, bool waitForQueueEmpty, bool expectNullProjectInfo) - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var projectConfigurationFilePathStore = new DefaultProjectConfigurationFilePathStore(); - - var projectSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", ProjectWorkspaceState.Create(CodeAnalysis.CSharp.LanguageVersion.CSharp7_3)); - var expectedProjectInfo = projectSnapshot.ToRazorProjectInfo(); - var callCount = 0; - var requestInvoker = new Mock(MockBehavior.Strict); - ProjectInfoParams? projectInfoParams = null; - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorProjectInfoEndpoint, - RazorLSPConstants.RazorLanguageServerName, - It.IsAny(), - It.IsAny())) - .Callback((s1, s2, param, ct) => - { - callCount++; - projectInfoParams = param; - }) - .ReturnsAsync(new ReinvokeResponse()); - - var serializer = new TestRazorProjectInfoFileSerializer(); - - using var publisher = CreateRazorProjectInfoEndpointPublisher(requestInvoker.Object, projectManager, serializer); - var publisherAccessor = publisher.GetTestAccessor(); - - var args = ProjectChangeEventArgs.CreateTestInstance(projectSnapshot, projectSnapshot, documentFilePath: null!, changeKind); - - // Act - publisherAccessor.ProjectManager_Changed(null!, args); - if (waitForQueueEmpty) - { - await publisherAccessor.WaitUntilCurrentBatchCompletesAsync(); - } - - // Assert - Assert.Equal(1, callCount); - Assert.NotNull(projectInfoParams); - var filePath = Assert.Single(projectInfoParams.FilePaths); - - if (expectNullProjectInfo) - { - Assert.Null(filePath); - } - else - { - Assert.NotNull(filePath); - var projectInfo = await serializer.DeserializeFromFileAndDeleteAsync(filePath, DisposalToken); - Assert.NotNull(projectInfo); - Assert.Equal(expectedProjectInfo, projectInfo); - } - } - - [Fact] - public async Task EnqueuePublish_BatchesPublishRequestsAsync() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - - var firstSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj"); - var secondSnapshot = CreateProjectSnapshot(@"C:\path\to\project.csproj", [@"C:\path\to\file.cshtml"]); - var expectedProjectInfo = secondSnapshot.ToRazorProjectInfo(); - - ProjectInfoParams? projectInfoParams = null; - var callCount = 0; - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync( - LanguageServerConstants.RazorProjectInfoEndpoint, - RazorLSPConstants.RazorLanguageServerName, - It.IsAny(), - It.IsAny())) - .Callback((s1, s2, param, ct) => - { - callCount++; - projectInfoParams = param; - }) - .ReturnsAsync(new ReinvokeResponse()); - - var serializer = new TestRazorProjectInfoFileSerializer(); - - using var publisher = CreateRazorProjectInfoEndpointPublisher(requestInvoker.Object, projectManager, serializer); - var publisherAccessor = publisher.GetTestAccessor(); - - // Act - publisherAccessor.EnqueuePublish(firstSnapshot); - publisherAccessor.EnqueuePublish(secondSnapshot); - await publisherAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - Assert.Equal(1, callCount); - Assert.NotNull(projectInfoParams); - var filePath = Assert.Single(projectInfoParams.FilePaths); - Assert.NotNull(filePath); - var projectInfo = await serializer.DeserializeFromFileAndDeleteAsync(filePath, DisposalToken); - Assert.Equal(expectedProjectInfo, projectInfo); - } - - internal static IProjectSnapshot CreateProjectSnapshot( - string projectFilePath, - ProjectWorkspaceState? projectWorkspaceState = null, - string[]? documentFilePaths = null) - { - return TestProjectSnapshot.Create(projectFilePath, documentFilePaths ?? [], projectWorkspaceState); - } - - internal static IProjectSnapshot CreateProjectSnapshot(string projectFilePath, string[] documentFilePaths) - { - return TestProjectSnapshot.Create(projectFilePath, documentFilePaths); - } - - private static RazorProjectInfoEndpointPublisher CreateRazorProjectInfoEndpointPublisher( - LSPRequestInvoker requestInvoker, - IProjectSnapshotManager projectSnapshotManager, - IRazorProjectInfoFileSerializer serializer) - => new(requestInvoker, projectSnapshotManager, serializer, TimeSpan.FromMilliseconds(5)); - - private sealed class TestRazorProjectInfoFileSerializer : IRazorProjectInfoFileSerializer - { - private readonly Dictionary _filePathToProjectInfoMap = new(FilePathComparer.Instance); - - public Task DeserializeFromFileAndDeleteAsync(string filePath, CancellationToken cancellationToken) - { - return Task.FromResult(_filePathToProjectInfoMap[filePath]); - } - - public Task SerializeToTempFileAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) - { - var filePath = Guid.NewGuid().ToString("D"); - _filePathToProjectInfoMap[filePath] = projectInfo; - - return Task.FromResult(filePath); - } - } -} From caaa0d7c20f2c269a1ccdc0eaf17ad35c431df5b Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 09:20:57 -0700 Subject: [PATCH 19/45] Remove ProjectConfigurationFilePathStore --- ...ctConfigurationFilePathChangedEventArgs.cs | 20 -- .../ProjectConfigurationFilePathStore.cs | 22 -- ...efaultProjectConfigurationFilePathStore.cs | 80 ------- ...ltProjectConfigurationFilePathStoreTest.cs | 216 ------------------ .../TestProjectConfigurationFilePathStore.cs | 29 --- 5 files changed, 367 deletions(-) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathChangedEventArgs.cs delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathStore.cs delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/DefaultProjectConfigurationFilePathStore.cs delete mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DefaultProjectConfigurationFilePathStoreTest.cs delete mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectConfigurationFilePathStore.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathChangedEventArgs.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathChangedEventArgs.cs deleted file mode 100644 index 5535f8c2614..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathChangedEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Razor.ProjectSystem; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; - -internal class ProjectConfigurationFilePathChangedEventArgs : EventArgs -{ - public ProjectConfigurationFilePathChangedEventArgs(ProjectKey projectKey, string? configurationFilePath) - { - ProjectKey = projectKey; - ConfigurationFilePath = configurationFilePath; - } - - public ProjectKey ProjectKey { get; } - - public string? ConfigurationFilePath { get; } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathStore.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathStore.cs deleted file mode 100644 index dde2f060f7f..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectConfigurationFilePathStore.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Razor.ProjectSystem; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; - -internal abstract class ProjectConfigurationFilePathStore -{ - public abstract event EventHandler? Changed; - - public abstract IReadOnlyDictionary GetMappings(); - - public abstract void Set(ProjectKey projectKey, string configurationFilePath); - - public abstract bool TryGet(ProjectKey projectKey, [NotNullWhen(returnValue: true)] out string? configurationFilePath); - - public abstract void Remove(ProjectKey projectKey); -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/DefaultProjectConfigurationFilePathStore.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/DefaultProjectConfigurationFilePathStore.cs deleted file mode 100644 index f6d19d307df..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/DefaultProjectConfigurationFilePathStore.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.Razor.LanguageClient; - -[Export(typeof(ProjectConfigurationFilePathStore))] -internal class DefaultProjectConfigurationFilePathStore : ProjectConfigurationFilePathStore -{ - private readonly Dictionary _mappings; - private readonly object _mappingsLock; - - public override event EventHandler? Changed; - - [ImportingConstructor] - public DefaultProjectConfigurationFilePathStore() - { - _mappings = new Dictionary(); - _mappingsLock = new object(); - } - - public override IReadOnlyDictionary GetMappings() => new Dictionary(_mappings); - - public override void Set(ProjectKey projectKey, string configurationFilePath) - { - if (configurationFilePath is null) - { - throw new ArgumentNullException(nameof(configurationFilePath)); - } - - lock (_mappingsLock) - { - // Resolve any relative pathing in the configuration path so we can talk in absolutes - configurationFilePath = Path.GetFullPath(configurationFilePath); - - if (_mappings.TryGetValue(projectKey, out var existingConfigurationFilePath) && - FilePathComparer.Instance.Equals(configurationFilePath, existingConfigurationFilePath)) - { - // Already have this mapping, don't invoke changed. - return; - } - - _mappings[projectKey] = configurationFilePath; - } - - var args = new ProjectConfigurationFilePathChangedEventArgs(projectKey, configurationFilePath); - Changed?.Invoke(this, args); - } - - public override void Remove(ProjectKey projectKey) - { - lock (_mappingsLock) - { - if (!_mappings.Remove(projectKey)) - { - // We weren't tracking the project file path, no-op. - return; - } - } - - var args = new ProjectConfigurationFilePathChangedEventArgs(projectKey, configurationFilePath: null); - Changed?.Invoke(this, args); - } - - public override bool TryGet(ProjectKey projectKey, [NotNullWhen(returnValue: true)] out string? configurationFilePath) - { - lock (_mappingsLock) - { - return _mappings.TryGetValue(projectKey, out configurationFilePath); - } - } -} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DefaultProjectConfigurationFilePathStoreTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DefaultProjectConfigurationFilePathStoreTest.cs deleted file mode 100644 index 807f2fca8c8..00000000000 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DefaultProjectConfigurationFilePathStoreTest.cs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -#nullable disable - -using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.VisualStudio.Razor.LanguageClient; - -public class DefaultProjectConfigurationFilePathStoreTest : ToolingTestBase -{ - public DefaultProjectConfigurationFilePathStoreTest(ITestOutputHelper testOutput) - : base(testOutput) - { - } - - [Fact] - public void Set_ResolvesRelativePaths() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - var configurationFilePath = @"C:\project\subpath\..\obj\project.razor.bin"; - var called = false; - store.Changed += (sender, args) => - { - called = true; - Assert.Equal(hostProject.Key, args.ProjectKey); - Assert.Equal(@"C:\project\obj\project.razor.bin", args.ConfigurationFilePath); - }; - - // Act - store.Set(hostProject.Key, configurationFilePath); - - // Assert - Assert.True(called); - } - - [Fact] - public void Set_InvokesChanged() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - var configurationFilePath = @"C:\project\obj\project.razor.bin"; - var called = false; - store.Changed += (sender, args) => - { - called = true; - Assert.Equal(hostProject.Key, args.ProjectKey); - Assert.Equal(configurationFilePath, args.ConfigurationFilePath); - }; - - // Act - store.Set(hostProject.Key, configurationFilePath); - - // Assert - Assert.True(called); - } - - [Fact] - public void Set_SameConfigurationFilePath_DoesNotInvokeChanged() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - var configurationFilePath = @"C:\project\obj\project.razor.bin"; - store.Set(hostProject.Key, configurationFilePath); - var called = false; - store.Changed += (sender, args) => called = true; - - // Act - store.Set(hostProject.Key, configurationFilePath); - - // Assert - Assert.False(called); - } - - [Fact] - public void Set_AllowsTryGet() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - var expectedConfigurationFilePath = @"C:\project\obj\project.razor.bin"; - store.Set(hostProject.Key, expectedConfigurationFilePath); - - // Act - var result = store.TryGet(hostProject.Key, out var configurationFilePath); - - // Assert - Assert.True(result); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - } - - [Fact] - public void Set_OverridesPrevious() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - var expectedConfigurationFilePath = @"C:\project\obj\project.razor.bin"; - - // Act - store.Set(hostProject.Key, @"C:\other\obj\project.razor.bin"); - store.Set(hostProject.Key, expectedConfigurationFilePath); - - // Assert - var result = store.TryGet(hostProject.Key, out var configurationFilePath); - Assert.True(result); - Assert.Equal(expectedConfigurationFilePath, configurationFilePath); - } - - [Fact] - public void GetMappings_NotMutable() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - - // Act - var mappings = store.GetMappings(); - var hostProject = new HostProject(@"C:\project.csproj", @"C:\project\obj", RazorConfiguration.Default, null); - store.Set(hostProject.Key, @"C:\project\obj\project.razor.bin"); - - // Assert - Assert.Empty(mappings); - } - - [Fact] - public void GetMappings_ReturnsAllSetMappings() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var expectedMappings = new Dictionary() - { - [TestProjectKey.Create(@"C:\project1\obj")] = @"C:\project1\obj\project.razor.bin" - }; - foreach (var mapping in expectedMappings) - { - store.Set(mapping.Key, mapping.Value); - } - - // Act - var mappings = store.GetMappings(); - - // Assert - Assert.Equal(expectedMappings, mappings); - } - - [Fact] - public void Remove_InvokesChanged() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - store.Set(hostProject.Key, @"C:\project\obj\project.razor.bin"); - var called = false; - store.Changed += (sender, args) => - { - called = true; - Assert.Equal(hostProject.Key, args.ProjectKey); - Assert.Null(args.ConfigurationFilePath); - }; - - // Act - store.Remove(hostProject.Key); - - // Assert - Assert.True(called); - } - - [Fact] - public void Remove_UntrackedProject_DoesNotInvokeChanged() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var called = false; - store.Changed += (sender, args) => called = true; - - // Act - store.Remove(TestProjectKey.Create(@"C:\project\obj")); - - // Assert - Assert.False(called); - } - - [Fact] - public void Remove_RemovesGettability() - { - // Arrange - var store = new DefaultProjectConfigurationFilePathStore(); - var projectFilePath = @"C:\project.csproj"; - var hostProject = new HostProject(projectFilePath, @"C:\project\obj", RazorConfiguration.Default, null); - store.Set(hostProject.Key, @"C:\project\obj\project.razor.bin"); - - // Act - store.Remove(hostProject.Key); - var result = store.TryGet(hostProject.Key, out _); - - // Assert - Assert.False(result); - } -} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectConfigurationFilePathStore.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectConfigurationFilePathStore.cs deleted file mode 100644 index 51d031dfca2..00000000000 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectConfigurationFilePathStore.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.Razor.ProjectSystem; - -internal class TestProjectConfigurationFilePathStore : ProjectConfigurationFilePathStore -{ - private Dictionary _mappings = new(); - - public override event EventHandler? Changed { add { } remove { } } - - public override IReadOnlyDictionary GetMappings() - => _mappings; - - public override void Remove(ProjectKey projectKey) - => _mappings.Remove(projectKey); - - public override void Set(ProjectKey projectKey, string configurationFilePath) - => _mappings[projectKey] = configurationFilePath; - - public override bool TryGet(ProjectKey projectKey, [NotNullWhen(true)] out string? configurationFilePath) - => _mappings.TryGetValue(projectKey, out configurationFilePath); -} From c437c02fe88b7fb7570a9d4f5e9f65805341f04f Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 10:34:21 -0700 Subject: [PATCH 20/45] Convert WorkspaceDirectoryPathResolver to interface and implement on CapabilitiesManager --- .../CapabilitiesManager.cs | 41 +++++++++++-------- .../DefaultWorkspaceDirectoryPathResolver.cs | 40 ------------------ .../IServiceCollectionExtensions.cs | 1 + ...olver.cs => IWorkspaceRootPathProvider.cs} | 4 +- ...torProjectConfigurationFilePathEndpoint.cs | 8 ++-- .../RazorFileChangeDetectorManager.cs | 6 +-- .../RazorLanguageServer.cs | 2 +- ...faultWorkspaceDirectoryPathResolverTest.cs | 33 ++++++--------- ...rojectConfigurationFilePathEndpointTest.cs | 38 +++++++++-------- .../RazorFileChangeDetectorManagerTest.cs | 25 ++++------- 10 files changed, 75 insertions(+), 123 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultWorkspaceDirectoryPathResolver.cs rename src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/{WorkspaceDirectoryPathResolver.cs => IWorkspaceRootPathProvider.cs} (69%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs index ee72f4cbadd..73581b62431 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs @@ -2,17 +2,20 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.RpcContracts.Settings; namespace Microsoft.AspNetCore.Razor.LanguageServer; -internal class CapabilitiesManager : IInitializeManager, IClientCapabilitiesService +internal sealed class CapabilitiesManager(ILspServices lspServices) + : IInitializeManager, IClientCapabilitiesService, IWorkspaceRootPathProvider { + private readonly ILspServices _lspServices = lspServices; private InitializeParams? _initializeParams; - private readonly ILspServices _lspServices; public bool HasInitialized => _initializeParams is not null; @@ -20,20 +23,9 @@ internal class CapabilitiesManager : IInitializeManager GetInitializeParams().Capabilities.ToVSInternalClientCapabilities(); - public CapabilitiesManager(ILspServices lspServices) - { - _lspServices = lspServices; - } - public InitializeParams GetInitializeParams() - { - if (_initializeParams is null) - { - throw new InvalidOperationException($"{nameof(GetInitializeParams)} was called before '{Methods.InitializeName}'"); - } - - return _initializeParams; - } + => _initializeParams ?? + throw new InvalidOperationException($"{nameof(GetInitializeParams)} was called before '{Methods.InitializeName}'"); public InitializeResult GetInitializeResult() { @@ -49,16 +41,29 @@ public InitializeResult GetInitializeResult() provider.ApplyCapabilities(serverCapabilities, vsClientCapabilities); } - var initializeResult = new InitializeResult + return new InitializeResult { Capabilities = serverCapabilities, }; - - return initializeResult; } public void SetInitializeParams(InitializeParams request) { _initializeParams = request; } + + public string GetRootPath() + { + var initializeParams = GetInitializeParams(); + + if (initializeParams.RootUri is null) + { +#pragma warning disable CS0618 // Type or member is obsolete + // RootUri was added in LSP3, fallback to RootPath + return initializeParams.RootPath.AssumeNotNull(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + return initializeParams.RootUri.GetAbsoluteOrUNCPath(); + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultWorkspaceDirectoryPathResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultWorkspaceDirectoryPathResolver.cs deleted file mode 100644 index 41fea8e688c..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultWorkspaceDirectoryPathResolver.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CommonLanguageServerProtocol.Framework; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal class DefaultWorkspaceDirectoryPathResolver : WorkspaceDirectoryPathResolver -{ - private readonly IInitializeManager _settingsManager; - - public DefaultWorkspaceDirectoryPathResolver(IInitializeManager settingsManager) - { - if (settingsManager is null) - { - throw new ArgumentNullException(nameof(settingsManager)); - } - - _settingsManager = settingsManager; - } - - public override string Resolve() - { - var clientSettings = _settingsManager.GetInitializeParams(); - if (clientSettings.RootUri is null) - { -#pragma warning disable CS0618 // Type or member is obsolete - Assumes.NotNull(clientSettings.RootPath); - // RootUri was added in LSP3, fallback to RootPath - return clientSettings.RootPath; -#pragma warning restore CS0618 // Type or member is obsolete - } - - var normalized = clientSettings.RootUri.GetAbsoluteOrUNCPath(); - return normalized; - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index e83b86f144c..7aa944dfa37 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -48,6 +48,7 @@ public static void AddLifeCycleServices(this IServiceCollection services, RazorL services.AddSingleton(); services.AddSingleton, CapabilitiesManager>(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton, RazorRequestContextFactory>(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceDirectoryPathResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs similarity index 69% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceDirectoryPathResolver.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs index 5465b6d42c6..534887e750f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceDirectoryPathResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -internal abstract class WorkspaceDirectoryPathResolver +internal interface IWorkspaceRootPathProvider { - public abstract string Resolve(); + string GetRootPath(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs index f64bf7b13f4..f728bac8460 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; internal class MonitorProjectConfigurationFilePathEndpoint : IRazorNotificationHandler, IDisposable { private readonly IProjectSnapshotManager _projectManager; - private readonly WorkspaceDirectoryPathResolver _workspaceDirectoryPathResolver; + private readonly IWorkspaceRootPathProvider _workspaceRootPathProvider; private readonly IEnumerable _listeners; private readonly LanguageServerFeatureOptions _options; private readonly ILoggerFactory _loggerFactory; @@ -35,13 +35,13 @@ internal class MonitorProjectConfigurationFilePathEndpoint : IRazorNotificationH public MonitorProjectConfigurationFilePathEndpoint( IProjectSnapshotManager projectManager, - WorkspaceDirectoryPathResolver workspaceDirectoryPathResolver, + IWorkspaceRootPathProvider workspaceRootPathProvider, IEnumerable listeners, LanguageServerFeatureOptions options, ILoggerFactory loggerFactory) { _projectManager = projectManager; - _workspaceDirectoryPathResolver = workspaceDirectoryPathResolver; + _workspaceRootPathProvider = workspaceRootPathProvider; _listeners = listeners; _options = options; _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); @@ -89,7 +89,7 @@ public async Task HandleNotificationAsync(MonitorProjectConfigurationFilePathPar if (_options.MonitorWorkspaceFolderForConfigurationFiles) { var normalizedConfigurationDirectory = FilePathNormalizer.NormalizeDirectory(configurationDirectory); - var workspaceDirectory = _workspaceDirectoryPathResolver.Resolve(); + var workspaceDirectory = _workspaceRootPathProvider.GetRootPath(); var normalizedWorkspaceDirectory = FilePathNormalizer.NormalizeDirectory(workspaceDirectory); if (normalizedConfigurationDirectory.StartsWith(normalizedWorkspaceDirectory, FilePathComparison.Instance)) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs index 17b727855b6..bf33eae4f10 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs @@ -12,10 +12,10 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; internal class RazorFileChangeDetectorManager( - WorkspaceDirectoryPathResolver workspaceDirectoryPathResolver, + IWorkspaceRootPathProvider workspaceRootPathProvider, IEnumerable fileChangeDetectors) : IOnInitialized, IDisposable { - private readonly WorkspaceDirectoryPathResolver _workspaceDirectoryPathResolver = workspaceDirectoryPathResolver; + private readonly IWorkspaceRootPathProvider _workspaceRootPathProvider = workspaceRootPathProvider; private readonly ImmutableArray _fileChangeDetectors = fileChangeDetectors.ToImmutableArray(); private readonly object _disposeLock = new(); private bool _disposed; @@ -24,7 +24,7 @@ public async Task OnInitializedAsync(ILspServices services, CancellationToken ca { // Initialized request, this occurs once the server and client have agreed on what sort of features they both support. It only happens once. - var workspaceDirectoryPath = _workspaceDirectoryPathResolver.Resolve(); + var workspaceDirectoryPath = _workspaceRootPathProvider.GetRootPath(); foreach (var fileChangeDetector in _fileChangeDetectors) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index ac6830910d5..8560b57bd40 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -158,7 +158,7 @@ protected override ILspServices ConstructLspServices() services.AddSingleton(); // Other - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // Get the DefaultSession for telemetry. This is set by VS with diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs index c4327f34f33..413347f3e7d 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs @@ -1,42 +1,34 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -#nullable disable - using System; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Moq; using Xunit; -using Microsoft.AspNetCore.Razor.Test.Common; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer; -public class DefaultWorkspaceDirectoryPathResolverTest : ToolingTestBase +public class DefaultWorkspaceDirectoryPathResolverTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { - public DefaultWorkspaceDirectoryPathResolverTest(ITestOutputHelper testOutput) - : base(testOutput) - { - } - [Fact] public void Resolve_RootUriUnavailable_UsesRootPath() { // Arrange var expectedWorkspaceDirectory = "/testpath"; #pragma warning disable CS0618 // Type or member is obsolete - var clientSettings = new InitializeParams() + var initializeParams = new InitializeParams() { RootPath = expectedWorkspaceDirectory }; #pragma warning restore CS0618 // Type or member is obsolete - var server = new Mock>(MockBehavior.Strict); - server.Setup(m => m.GetInitializeParams()).Returns(clientSettings); - var workspaceDirectoryPathResolver = new DefaultWorkspaceDirectoryPathResolver(server.Object); + + var capabilitiesManager = new CapabilitiesManager(StrictMock.Of()); + capabilitiesManager.SetInitializeParams(initializeParams); // Act - var workspaceDirectoryPath = workspaceDirectoryPathResolver.Resolve(); + var workspaceDirectoryPath = capabilitiesManager.GetRootPath(); // Assert Assert.Equal(expectedWorkspaceDirectory, workspaceDirectoryPath); @@ -53,19 +45,20 @@ public void Resolve_RootUriPrefered() Host = null, Path = initialWorkspaceDirectory, }; + #pragma warning disable CS0618 // Type or member is obsolete - var clientSettings = new InitializeParams() + var initializeParams = new InitializeParams() { RootPath = "/somethingelse", RootUri = uriBuilder.Uri, }; #pragma warning restore CS0618 // Type or member is obsolete - var server = new Mock>(MockBehavior.Strict); - server.Setup(s => s.GetInitializeParams()).Returns(clientSettings); - var workspaceDirectoryPathResolver = new DefaultWorkspaceDirectoryPathResolver(server.Object); + + var capabilitiesManager = new CapabilitiesManager(StrictMock.Of()); + capabilitiesManager.SetInitializeParams(initializeParams); // Act - var workspaceDirectoryPath = workspaceDirectoryPathResolver.Resolve(); + var workspaceDirectoryPath = capabilitiesManager.GetRootPath(); // Assert var expectedWorkspaceDirectory = "C:/testpath"; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs index de5785993bf..0bf2964aa5f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs @@ -24,13 +24,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; public class MonitorProjectConfigurationFilePathEndpointTest : LanguageServerTestBase { - private readonly WorkspaceDirectoryPathResolver _directoryPathResolver; + private readonly IWorkspaceRootPathProvider _workspaceRootPathProvider; public MonitorProjectConfigurationFilePathEndpointTest(ITestOutputHelper testOutput) : base(testOutput) { var path = PathUtilities.CreateRootedPath("dir"); - _directoryPathResolver = Mock.Of(resolver => resolver.Resolve() == path, MockBehavior.Strict); + _workspaceRootPathProvider = StrictMock.Of(resolver => + resolver.GetRootPath() == path); } [Fact] @@ -38,12 +39,12 @@ public async Task Handle_Disposed_Noops() { // Arrange var projectManager = CreateProjectSnapshotManager(); - var directoryPathResolver = new Mock(MockBehavior.Strict); - directoryPathResolver.Setup(resolver => resolver.Resolve()) + var workspaceRootPathProvider = new StrictMock(); + workspaceRootPathProvider.Setup(resolver => resolver.GetRootPath()) .Throws(); var configurationFileEndpoint = new MonitorProjectConfigurationFilePathEndpoint( projectManager, - directoryPathResolver.Object, + workspaceRootPathProvider.Object, listeners: [], TestLanguageServerFeatureOptions.Instance, LoggerFactory); @@ -69,12 +70,13 @@ public async Task Handle_ConfigurationFilePath_UntrackedMonitorNoops() { // Arrange var projectManager = CreateProjectSnapshotManager(); - var directoryPathResolver = new Mock(MockBehavior.Strict); - directoryPathResolver.Setup(resolver => resolver.Resolve()) + var workspaceRootPathProvider = new StrictMock(); + workspaceRootPathProvider + .Setup(resolver => resolver.GetRootPath()) .Throws(); var configurationFileEndpoint = new MonitorProjectConfigurationFilePathEndpoint( projectManager, - directoryPathResolver.Object, + workspaceRootPathProvider.Object, listeners: [], TestLanguageServerFeatureOptions.Instance, LoggerFactory); @@ -101,7 +103,7 @@ public async Task Handle_ConfigurationFilePath_TrackedMonitor_StopsMonitor() var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -139,7 +141,7 @@ public async Task Handle_InWorkspaceDirectory_Noops() var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -170,7 +172,7 @@ public async Task Handle_InWorkspaceDirectory_MonitorsIfLanguageFeatureOptionSet var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager, @@ -202,7 +204,7 @@ public async Task Handle_DuplicateMonitors_Noops() var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -235,7 +237,7 @@ public async Task Handle_ChangedConfigurationOutputPath_StartsWithNewPath() var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -276,7 +278,7 @@ public async Task Handle_ChangedConfigurationExternalToInternal_StopsWithoutRest var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -321,7 +323,7 @@ public async Task Handle_ProjectPublished() var detectors = new[] { projectOpenDebugDetector, releaseDetector, postPublishDebugDetector }; var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detectors[callCount++], - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -377,7 +379,7 @@ public async Task Handle_MultipleProjects_StartedAndStopped() var detectors = new[] { debug1Detector, debug2Detector, release1Detector }; var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detectors[callCount++], - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager); @@ -433,7 +435,7 @@ public async Task Handle_ConfigurationFilePath_TrackedMonitor_RemovesProject() var detector = new TestFileChangeDetector(); var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( () => detector, - _directoryPathResolver, + _workspaceRootPathProvider, listeners: [], LoggerFactory, projectManager, @@ -470,7 +472,7 @@ public async Task Handle_ConfigurationFilePath_TrackedMonitor_RemovesProject() private class TestMonitorProjectConfigurationFilePathEndpoint( Func fileChangeDetectorFactory, - WorkspaceDirectoryPathResolver workspaceDirectoryPathResolver, + IWorkspaceRootPathProvider workspaceDirectoryPathResolver, IEnumerable listeners, ILoggerFactory loggerFactory, IProjectSnapshotManager projectManager, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileChangeDetectorManagerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileChangeDetectorManagerTest.cs index 91f017f679a..bcb359d3616 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileChangeDetectorManagerTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorFileChangeDetectorManagerTest.cs @@ -28,15 +28,13 @@ public async Task InitializedAsync_StartsFileChangeDetectors() Path = initialWorkspaceDirectory }; - var clientSettings = new InitializeParams() + var initializeParams = new InitializeParams() { RootUri = uriBuilder.Uri, }; - var languageServerMock = new StrictMock>(); - languageServerMock - .Setup(s => s.GetInitializeParams()) - .Returns(clientSettings); + var capabilitiesManager = new CapabilitiesManager(StrictMock.Of()); + capabilitiesManager.SetInitializeParams(initializeParams); var expectedWorkspaceDirectory = $"\\\\{initialWorkspaceDirectory}"; @@ -56,8 +54,7 @@ public async Task InitializedAsync_StartsFileChangeDetectors() detectorMock2.Setup(x => x.Stop()); - var workspaceDirectoryPathResolver = new DefaultWorkspaceDirectoryPathResolver(languageServerMock.Object); - using (var detectorManager = new RazorFileChangeDetectorManager(workspaceDirectoryPathResolver, [detectorMock1.Object, detectorMock2.Object])) + using (var detectorManager = new RazorFileChangeDetectorManager(capabilitiesManager, [detectorMock1.Object, detectorMock2.Object])) { // Act await detectorManager.OnInitializedAsync(StrictMock.Of(), DisposalToken); @@ -66,7 +63,6 @@ public async Task InitializedAsync_StartsFileChangeDetectors() // Assert detectorMock1.VerifyAll(); detectorMock2.VerifyAll(); - languageServerMock.VerifyAll(); } [Fact] @@ -74,15 +70,13 @@ public async Task InitializedAsync_Disposed_ReStopsFileChangeDetectors() { // Arrange var expectedWorkspaceDirectory = "\\\\testpath"; - var clientSettings = new InitializeParams() + var initializeParams = new InitializeParams() { RootUri = new Uri(expectedWorkspaceDirectory), }; - var languageServerMock = new StrictMock>(); - languageServerMock - .Setup(s => s.GetInitializeParams()) - .Returns(clientSettings); + var capabilitiesManager = new CapabilitiesManager(StrictMock.Of()); + capabilitiesManager.SetInitializeParams(initializeParams); var detectorMock = new StrictMock(); var cts = new TaskCompletionSource(); @@ -94,8 +88,7 @@ public async Task InitializedAsync_Disposed_ReStopsFileChangeDetectors() .Setup(d => d.Stop()) .Callback(() => stopCount++); - var workspaceDirectoryPathResolver = new DefaultWorkspaceDirectoryPathResolver(languageServerMock.Object); - using var detectorManager = new RazorFileChangeDetectorManager(workspaceDirectoryPathResolver, [detectorMock.Object]); + using var detectorManager = new RazorFileChangeDetectorManager(capabilitiesManager, [detectorMock.Object]); // Act var initializeTask = detectorManager.OnInitializedAsync(StrictMock.Of(), DisposalToken); @@ -107,7 +100,5 @@ public async Task InitializedAsync_Disposed_ReStopsFileChangeDetectors() // Assert Assert.Equal(2, stopCount); - - languageServerMock.VerifyAll(); } } From 439aa7a03df59d600d282613cbe732155f6f9d83 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 13:53:17 -0700 Subject: [PATCH 21/45] Implement FileWatcherBasedRazorProjectInfoDriver --- .../IServiceCollectionExtensions.cs | 9 -- ...herBasedRazorProjectInfoDriver.Comparer.cs | 31 ++++ .../FileWatcherBasedRazorProjectInfoDriver.cs | 148 +++++++++++++++++- .../RazorLanguageServer.cs | 2 + .../ProjectSystem/RazorProjectInfo.cs | 5 + .../Logging/ILoggerFactoryExtensions.cs | 18 ++- ...bstractRazorProjectInfoDriver.Comparer.cs} | 4 +- ...actRazorProjectInfoDriver.TestAccessor.cs} | 6 +- .../AbstractRazorProjectInfoDriver.cs | 130 +++++++++++++++ .../ProjectSystem/RazorProjectInfoDriver.cs | 120 +------------- .../RazorLanguageServerClient.cs | 2 +- .../RazorProjectInfoDriverTest.cs | 4 +- 12 files changed, 338 insertions(+), 141 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.Comparer.cs rename src/Razor/src/{Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs => Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.Comparer.cs} (86%) rename src/Razor/src/{Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs => Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.TestAccessor.cs} (66%) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 7aa944dfa37..ff101515f49 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -230,15 +230,6 @@ public static void AddDocumentManagementServices(this IServiceCollection service } services.AddSingleton(); - - // If we're not monitoring the whole workspace folder for configuration changes, then we don't actually need the the file change - // detector wired up via DI, as the razor/monitorProjectConfigurationFilePath endpoint will directly construct one. This means - // it can be a little simpler, and doesn't need to worry about which folders it's told to listen to. - if (featureOptions.MonitorWorkspaceFolderForConfigurationFiles) - { - services.AddSingleton(); - } - services.AddSingleton(); // Document processed listeners diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.Comparer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.Comparer.cs new file mode 100644 index 00000000000..86ace844701 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.Comparer.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; + +internal partial class FileWatcherBasedRazorProjectInfoDriver +{ + private sealed class Comparer : IEqualityComparer<(string FilePath, ChangeKind Kind)> + { + public static readonly Comparer Instance = new(); + + private Comparer() + { + } + + public bool Equals((string FilePath, ChangeKind Kind) x, (string FilePath, ChangeKind Kind) y) + { + // We just want the most recent change to each file path. It's ok if there's a remove followed by an add, + // or an add followed by a remove for the same path. + return FilePathComparer.Instance.Equals(x.FilePath, y.FilePath); + } + + public int GetHashCode((string FilePath, ChangeKind Kind) obj) + { + return FilePathComparer.Instance.GetHashCode(obj.FilePath); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index edc1963f5f2..c15c503a433 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -1,22 +1,158 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Utilities; +using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -internal class FileWatcherBasedRazorProjectInfoDriver : IRazorProjectInfoDriver +internal partial class FileWatcherBasedRazorProjectInfoDriver : AbstractRazorProjectInfoDriver { - // TODO: Implement! + private enum ChangeKind { AddOrUpdate, Remove } - private ImmutableArray _listeners; + private static readonly ImmutableArray s_ignoredDirectories = + [ + "node_modules", + "bin", + ".vs", + ]; - public ImmutableArray GetLatestProjectInfo() => []; + private readonly IWorkspaceRootPathProvider _workspaceRootPathProvider; + private readonly LanguageServerFeatureOptions _options; + private readonly ILogger _logger; - public void AddListener(IRazorProjectInfoListener listener) + private readonly AsyncBatchingWorkQueue<(string FilePath, ChangeKind Kind)> _workQueue; + + private FileSystemWatcher? _fileSystemWatcher; + + public FileWatcherBasedRazorProjectInfoDriver( + IWorkspaceRootPathProvider workspaceRootPathProvider, + LanguageServerFeatureOptions options, + ILoggerFactory loggerFactory, + TimeSpan? delay = null) + : base(loggerFactory, delay) + { + _workspaceRootPathProvider = workspaceRootPathProvider; + _options = options; + _logger = loggerFactory.GetOrCreateLogger(); + + _workQueue = new(delay ?? DefaultDelay, ProcessBatchAsync, DisposalToken); + + // Dispose our FileSystemWatcher when the driver is disposed. + DisposalToken.Register(() => + { + _fileSystemWatcher?.Dispose(); + _fileSystemWatcher = null; + }); + } + + public async Task InitializeAsync(CancellationToken cancellationToken) + { + var workspaceDirectoryPath = _workspaceRootPathProvider.GetRootPath(); + + var existingConfigurationFiles = DirectoryHelper.GetFilteredFiles( + workspaceDirectoryPath, + _options.ProjectConfigurationFileName, + s_ignoredDirectories, + logger: _logger).ToImmutableArray(); + + foreach (var filePath in existingConfigurationFiles) + { + using var stream = File.OpenRead(filePath); + + var razorProjectInfo = await RazorProjectInfo + .DeserializeFromAsync(stream, cancellationToken) + .ConfigureAwait(false); + + EnqueueUpdate(razorProjectInfo); + } + + if (!Directory.Exists(workspaceDirectoryPath)) + { + _logger.LogInformation($"Creating workspace directory: '{workspaceDirectoryPath}'"); + Directory.CreateDirectory(workspaceDirectoryPath); + } + + _logger.LogInformation($"Starting configuration file change detector for '{workspaceDirectoryPath}'"); + _fileSystemWatcher = new FileSystemWatcher(workspaceDirectoryPath, _options.ProjectConfigurationFileName) + { + NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime, + IncludeSubdirectories = true, + }; + + _fileSystemWatcher.Created += (_, args) => EnqueueAddOrChange(args.FullPath); + _fileSystemWatcher.Changed += (_, args) => EnqueueAddOrChange(args.FullPath); + _fileSystemWatcher.Deleted += (_, args) => EnqueueRemove(args.FullPath); + _fileSystemWatcher.Renamed += (_, args) => EnqueueRename(args.OldFullPath, args.FullPath); + + _fileSystemWatcher.EnableRaisingEvents = true; + } + + private async ValueTask ProcessBatchAsync(ImmutableArray<(string FilePath, ChangeKind Kind)> items, CancellationToken token) + { + foreach (var (filePath, changeKind) in items.GetMostRecentUniqueItems(Comparer.Instance)) + { + if (token.IsCancellationRequested) + { + return; + } + + switch (changeKind) + { + case ChangeKind.AddOrUpdate: + using (var stream = File.OpenRead(filePath)) + { + var razorProjectInfo = await RazorProjectInfo + .DeserializeFromAsync(stream, token) + .ConfigureAwait(false); + + EnqueueUpdate(razorProjectInfo); + } + + break; + + case ChangeKind.Remove: + // The file path is located in the directory used as the ProjectKey. + var normalizedDirectory = FilePathNormalizer.GetNormalizedDirectoryName(filePath); + var projectKey = new ProjectKey(normalizedDirectory); + + EnqueueRemove(projectKey); + + break; + + default: + Assumed.Unreachable(); + break; + } + } + } + + private void EnqueueAddOrChange(string filePath) + { + _workQueue.AddWork((filePath, ChangeKind.AddOrUpdate)); + } + + private void EnqueueRemove(string filePath) + { + _workQueue.AddWork((filePath, ChangeKind.Remove)); + } + + private void EnqueueRename(string oldFilePath, string newFilePath) { - ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); + EnqueueRemove(oldFilePath); + EnqueueAddOrChange(newFilePath); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 8560b57bd40..d8d42a56def 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -132,6 +132,8 @@ protected override ILspServices ConstructLspServices() } else { + // If the language server was not created with an IRazorProjectInfoDriver, + // fall back to a FileWatcher-base driver. services.AddSingleton(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/RazorProjectInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/RazorProjectInfo.cs index ab832d2f825..07c818eb444 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/RazorProjectInfo.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/RazorProjectInfo.cs @@ -6,6 +6,8 @@ using System.Collections.Immutable; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using MessagePack; using MessagePack.Resolvers; using Microsoft.AspNetCore.Razor.Language; @@ -85,4 +87,7 @@ public void SerializeTo(Stream stream) public static RazorProjectInfo? DeserializeFrom(Stream stream) => MessagePackSerializer.Deserialize(stream, s_options); + + public static ValueTask DeserializeFromAsync(Stream stream, CancellationToken cancellationToken) + => MessagePackSerializer.DeserializeAsync(stream, s_options, cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs index db7ef6f6742..68125deb94b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Razor; namespace Microsoft.CodeAnalysis.Razor.Logging; @@ -9,16 +10,23 @@ internal static class ILoggerFactoryExtensions { public static ILogger GetOrCreateLogger(this ILoggerFactory factory) { - return factory.GetOrCreateLogger(TrimTypeName(typeof(T).FullName)); + return factory.GetOrCreateLogger(typeof(T)); + } + + public static ILogger GetOrCreateLogger(this ILoggerFactory factory, Type type) + { + return factory.GetOrCreateLogger(TrimTypeName(type.FullName.AssumeNotNull())); } private static string TrimTypeName(string name) { - string trimmedName; - _ = TryTrim(name, "Microsoft.VisualStudio.", out trimmedName) || - TryTrim(name, "Microsoft.AspNetCore.Razor.", out trimmedName); + if (TryTrim(name, "Microsoft.VisualStudio.", out var trimmedName) || + TryTrim(name, "Microsoft.AspNetCore.Razor.", out trimmedName)) + { + return trimmedName; + } - return trimmedName; + return name; static bool TryTrim(string name, string prefix, out string trimmedName) { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.Comparer.cs similarity index 86% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.Comparer.cs index 5d7aa651f10..54c0f9c8066 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.Comparer.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.Comparer.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal sealed partial class RazorProjectInfoDriver +internal abstract partial class AbstractRazorProjectInfoDriver { private sealed class Comparer : IEqualityComparer { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.TestAccessor.cs similarity index 66% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.TestAccessor.cs index 7c67f51e729..e2f37ff1b47 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.TestAccessor.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.TestAccessor.cs @@ -3,13 +3,13 @@ using System.Threading.Tasks; -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal sealed partial class RazorProjectInfoDriver +internal abstract partial class AbstractRazorProjectInfoDriver { internal TestAccessor GetTestAccessor() => new(this); - internal readonly struct TestAccessor(RazorProjectInfoDriver instance) + internal readonly struct TestAccessor(AbstractRazorProjectInfoDriver instance) { public Task WaitUntilCurrentBatchCompletesAsync() => instance._workQueue.WaitUntilCurrentBatchCompletesAsync(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs new file mode 100644 index 00000000000..80381ee823f --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Utilities; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; + +internal abstract partial class AbstractRazorProjectInfoDriver : IRazorProjectInfoDriver, IDisposable +{ + private abstract record Work(ProjectKey ProjectKey); + private sealed record Update(RazorProjectInfo ProjectInfo) : Work(ProjectInfo.ProjectKey); + private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); + + protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMilliseconds(250); + + protected readonly ILogger Logger; + + private readonly CancellationTokenSource _disposeTokenSource; + private readonly AsyncBatchingWorkQueue _workQueue; + + private readonly Dictionary _latestProjectInfoMap; + private ImmutableArray _listeners; + + protected CancellationToken DisposalToken => _disposeTokenSource.Token; + + protected AbstractRazorProjectInfoDriver(ILoggerFactory loggerFactory, TimeSpan? delay = null) + { + Logger = loggerFactory.GetOrCreateLogger(GetType()); + + _disposeTokenSource = new(); + _workQueue = new AsyncBatchingWorkQueue(delay ?? DefaultDelay, ProcessBatchAsync, _disposeTokenSource.Token); + _latestProjectInfoMap = []; + _listeners = []; + } + + public void Dispose() + { + _disposeTokenSource.Cancel(); + _disposeTokenSource.Dispose(); + } + + private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) + { + foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance)) + { + if (token.IsCancellationRequested) + { + return; + } + + // Update our map first + lock (_latestProjectInfoMap) + { + switch (work) + { + case Update(var projectInfo): + _latestProjectInfoMap[projectInfo.ProjectKey] = projectInfo; + break; + + case Remove(var projectKey): + _latestProjectInfoMap.Remove(projectKey); + break; + + default: + Assumed.Unreachable(); + break; + } + } + + // Now, notify listeners + foreach (var listener in _listeners) + { + if (token.IsCancellationRequested) + { + return; + } + + switch (work) + { + case Update(var projectInfo): + await listener.UpdatedAsync(projectInfo, token).ConfigureAwait(false); + break; + + case Remove(var projectKey): + await listener.RemovedAsync(projectKey, token).ConfigureAwait(false); + break; + } + } + } + } + + protected void EnqueueUpdate(RazorProjectInfo projectInfo) + { + _workQueue.AddWork(new Update(projectInfo)); + } + + protected void EnqueueRemove(ProjectKey projectKey) + { + _workQueue.AddWork(new Remove(projectKey)); + } + + public ImmutableArray GetLatestProjectInfo() + { + lock (_latestProjectInfoMap) + { + using var builder = new PooledArrayBuilder(capacity: _latestProjectInfoMap.Count); + + foreach (var (_, projectInfo) in _latestProjectInfoMap) + { + builder.Add(projectInfo); + } + + return builder.DrainToImmutable(); + } + } + + public void AddListener(IRazorProjectInfoListener listener) + { + ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs index f35a7c59520..8e43a5ec312 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs @@ -2,47 +2,21 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Utilities; namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -internal sealed partial class RazorProjectInfoDriver : IRazorProjectInfoDriver, IDisposable +internal sealed partial class RazorProjectInfoDriver( + IProjectSnapshotManager projectManager, + ILoggerFactory loggerFactory, + TimeSpan? delay = null) + : AbstractRazorProjectInfoDriver(loggerFactory, delay) { - private abstract record Work(ProjectKey ProjectKey); - private sealed record Update(RazorProjectInfo ProjectInfo) : Work(ProjectInfo.ProjectKey); - private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); - - private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); - - private readonly IProjectSnapshotManager _projectManager; - private readonly CancellationTokenSource _disposeTokenSource; - private readonly AsyncBatchingWorkQueue _workQueue; - - private readonly Dictionary _latestProjectInfoMap; - private ImmutableArray _listeners; - - public RazorProjectInfoDriver(IProjectSnapshotManager projectManager, TimeSpan? delay = null) - { - _projectManager = projectManager; - _disposeTokenSource = new(); - _workQueue = new AsyncBatchingWorkQueue(delay ?? s_delay, ProcessBatchAsync, _disposeTokenSource.Token); - _latestProjectInfoMap = []; - _listeners = []; - } - - public void Dispose() - { - _disposeTokenSource.Cancel(); - _disposeTokenSource.Dispose(); - } + private readonly IProjectSnapshotManager _projectManager = projectManager; public async Task InitializeAsync(CancellationToken cancellationToken) { @@ -91,84 +65,4 @@ private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) throw new NotSupportedException($"Unsupported {nameof(ProjectChangeKind)}: {e.Kind}"); } } - - private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) - { - foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance)) - { - if (token.IsCancellationRequested) - { - return; - } - - // Update our map first - lock (_latestProjectInfoMap) - { - switch (work) - { - case Update(var projectInfo): - _latestProjectInfoMap[projectInfo.ProjectKey] = projectInfo; - break; - - case Remove(var projectKey): - _latestProjectInfoMap.Remove(projectKey); - break; - - default: - Assumed.Unreachable(); - break; - } - } - - // Now, notify listeners - foreach (var listener in _listeners) - { - if (token.IsCancellationRequested) - { - return; - } - - switch (work) - { - case Update(var projectInfo): - await listener.UpdatedAsync(projectInfo, token).ConfigureAwait(false); - break; - - case Remove(var projectKey): - await listener.RemovedAsync(projectKey, token).ConfigureAwait(false); - break; - } - } - } - } - - private void EnqueueUpdate(RazorProjectInfo projectInfo) - { - _workQueue.AddWork(new Update(projectInfo)); - } - - private void EnqueueRemove(ProjectKey projectKey) - { - _workQueue.AddWork(new Remove(projectKey)); - } - - public ImmutableArray GetLatestProjectInfo() - { - lock (_latestProjectInfoMap) - { - using var builder = new PooledArrayBuilder(capacity: _latestProjectInfoMap.Count); - - foreach (var (_, projectInfo) in _latestProjectInfoMap) - { - builder.Add(projectInfo); - } - - return builder.DrainToImmutable(); - } - } - - public void AddListener(IRazorProjectInfoListener listener) - { - ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); - } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index 445b3832e96..bd8071dfd6c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -97,7 +97,7 @@ public event AsyncEventHandler? StopAsync var lspOptions = RazorLSPOptions.From(_clientSettingsManager.GetClientSettings()); - var projectInfoDriver = new RazorProjectInfoDriver(_projectManager); + var projectInfoDriver = new RazorProjectInfoDriver(_projectManager, _loggerFactory); await projectInfoDriver.InitializeAsync(token); _host = RazorLanguageServerHost.Create( diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs index 356ee12da3b..24461078235 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs @@ -247,10 +247,10 @@ await projectManager.UpdateAsync(static updater => Assert.Empty(listener.Updates); } - private async Task<(RazorProjectInfoDriver, RazorProjectInfoDriver.TestAccessor)> CreateDriverAndInitializeAsync( + private async Task<(RazorProjectInfoDriver, AbstractRazorProjectInfoDriver.TestAccessor)> CreateDriverAndInitializeAsync( IProjectSnapshotManager projectManager) { - var driver = new RazorProjectInfoDriver(projectManager, delay: TimeSpan.FromMilliseconds(5)); + var driver = new RazorProjectInfoDriver(projectManager, LoggerFactory, delay: TimeSpan.FromMilliseconds(5)); AddDisposable(driver); var testAccessor = driver.GetTestAccessor(); From 4ac135e7f9cfc9dcbeb954a16c1ce15e2a22c267 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 13:56:57 -0700 Subject: [PATCH 22/45] Remove MonitorProjectConfigurationFilePathEndpoint This is no longer used by Visual Studio and VS Code never used it. --- ...torProjectConfigurationFilePathEndpoint.cs | 215 -------- .../RazorLanguageServer.cs | 4 - .../Protocol/LanguageServerConstants.cs | 2 - ...nitorProjectConfigurationFilePathParams.cs | 11 - ...rojectConfigurationFilePathEndpointTest.cs | 511 ------------------ 5 files changed, 743 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/MonitorProjectConfigurationFilePathParams.cs delete mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs deleted file mode 100644 index f728bac8460..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MonitorProjectConfigurationFilePathEndpoint.cs +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; - -[RazorLanguageServerEndpoint(LanguageServerConstants.RazorMonitorProjectConfigurationFilePathEndpoint)] -internal class MonitorProjectConfigurationFilePathEndpoint : IRazorNotificationHandler, IDisposable -{ - private readonly IProjectSnapshotManager _projectManager; - private readonly IWorkspaceRootPathProvider _workspaceRootPathProvider; - private readonly IEnumerable _listeners; - private readonly LanguageServerFeatureOptions _options; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _outputPathMonitors; - private readonly object _disposeLock; - private bool _disposed; - - public bool MutatesSolutionState => false; - - public MonitorProjectConfigurationFilePathEndpoint( - IProjectSnapshotManager projectManager, - IWorkspaceRootPathProvider workspaceRootPathProvider, - IEnumerable listeners, - LanguageServerFeatureOptions options, - ILoggerFactory loggerFactory) - { - _projectManager = projectManager; - _workspaceRootPathProvider = workspaceRootPathProvider; - _listeners = listeners; - _options = options; - _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _logger = loggerFactory.GetOrCreateLogger(); - _outputPathMonitors = new ConcurrentDictionary(FilePathComparer.Instance); - _disposeLock = new object(); - } - - public async Task HandleNotificationAsync(MonitorProjectConfigurationFilePathParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) - { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - lock (_disposeLock) - { - if (_disposed) - { - return; - } - } - - if (request.ConfigurationFilePath is null) - { - _logger.LogInformation($"'null' configuration path provided. Stopping custom configuration monitoring for project '{request.ProjectKeyId}'."); - // If we're monitoring individual project configuration files, then the config file path should only be null if we're removing the - // project entirely. - await RemoveMonitorAsync(request.ProjectKeyId, removeProject: !_options.MonitorWorkspaceFolderForConfigurationFiles, cancellationToken).ConfigureAwait(false); - - return; - } - - if (!request.ConfigurationFilePath.EndsWith(_options.ProjectConfigurationFileName, StringComparison.Ordinal)) - { - _logger.LogError($"Invalid configuration file path provided for project '{request.ProjectKeyId}': '{request.ConfigurationFilePath}'"); - return; - } - - var configurationDirectory = Path.GetDirectoryName(request.ConfigurationFilePath); - Assumes.NotNull(configurationDirectory); - - var previousMonitorExists = _outputPathMonitors.TryGetValue(request.ProjectKeyId, out var entry); - - if (_options.MonitorWorkspaceFolderForConfigurationFiles) - { - var normalizedConfigurationDirectory = FilePathNormalizer.NormalizeDirectory(configurationDirectory); - var workspaceDirectory = _workspaceRootPathProvider.GetRootPath(); - var normalizedWorkspaceDirectory = FilePathNormalizer.NormalizeDirectory(workspaceDirectory); - - if (normalizedConfigurationDirectory.StartsWith(normalizedWorkspaceDirectory, FilePathComparison.Instance)) - { - if (previousMonitorExists) - { - _logger.LogInformation($"Configuration directory changed from external directory -> internal directory for project '{request.ProjectKeyId}, terminating existing monitor'."); - await RemoveMonitorAsync(request.ProjectKeyId, removeProject: false, cancellationToken).ConfigureAwait(false); - } - else - { - _logger.LogInformation($"No custom configuration directory required. The workspace directory is sufficient for '{ request.ProjectKeyId}'."); - } - - // Configuration directory is already in the workspace directory. We already monitor everything in the workspace directory. - return; - } - } - - if (previousMonitorExists) - { - if (FilePathComparer.Instance.Equals(configurationDirectory, entry.ConfigurationDirectory)) - { - _logger.LogInformation($"Already tracking configuration directory for project '{request.ProjectKeyId}'."); - - // Already tracking the requested configuration output path for this project - return; - } - - _logger.LogInformation($"Project configuration output path has changed. Stopping existing monitor for project '{request.ProjectKeyId}' so we can restart it with a new directory."); - await RemoveMonitorAsync(request.ProjectKeyId, removeProject: false, cancellationToken).ConfigureAwait(false); - } - - var detector = CreateFileChangeDetector(); - entry = (configurationDirectory, detector); - - if (!_outputPathMonitors.TryAdd(request.ProjectKeyId, entry)) - { - // There's a concurrent request going on for this specific project. To avoid calling "StartAsync" twice we return early. - // Note: This is an extremely edge case race condition that should in practice never happen due to how long it takes to calculate project state changes - return; - } - - _logger.LogInformation($"Starting new configuration monitor for project '{request.ProjectKeyId}' for directory '{configurationDirectory}'."); - await entry.Detector.StartAsync(configurationDirectory, cancellationToken).ConfigureAwait(false); - - if (cancellationToken.IsCancellationRequested) - { - // Request was cancelled while starting the detector. Need to stop it so we don't leak. - entry.Detector.Stop(); - return; - } - - if (!_outputPathMonitors.ContainsKey(request.ProjectKeyId)) - { - // This can happen if there were multiple concurrent requests to "remove" and "update" file change detectors for the same project path. - // In that case we need to stop the detector to ensure we don't leak. - entry.Detector.Stop(); - return; - } - - lock (_disposeLock) - { - if (_disposed) - { - // Server's being stopped. - entry.Detector.Stop(); - } - } - } - - private Task RemoveMonitorAsync(string projectKeyId, bool removeProject, CancellationToken cancellationToken) - { - // Should no longer monitor configuration output paths for the project - if (_outputPathMonitors.TryRemove(projectKeyId, out var removedEntry)) - { - removedEntry.Detector.Stop(); - } - else - { - // Concurrent requests to remove the same configuration output path for the project. We've already - // done the removal so we can just return gracefully. - } - - if (removeProject) - { - return _projectManager.UpdateAsync( - (updater, projectKey) => - { - updater.ProjectRemoved(projectKey); - }, - state: new ProjectKey(projectKeyId), - cancellationToken); - } - - return Task.CompletedTask; - } - - public void Dispose() - { - lock (_disposeLock) - { - if (_disposed) - { - return; - } - - _disposed = true; - } - - foreach (var entry in _outputPathMonitors) - { - entry.Value.Detector.Stop(); - } - } - - // Protected virtual for testing - protected virtual IFileChangeDetector CreateFileChangeDetector() - => new ProjectConfigurationFileChangeDetector( - _listeners, - _options, - _loggerFactory); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index d8d42a56def..23ed5cc67cd 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -195,10 +195,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption { services.AddHandler(); } - else - { - services.AddHandler(); - } services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs index 1110191c5df..933b1785632 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs @@ -13,8 +13,6 @@ internal static class LanguageServerConstants public const string RazorLanguageServerName = "Razor Language Server"; - public const string RazorMonitorProjectConfigurationFilePathEndpoint = "razor/monitorProjectConfigurationFilePath"; - public const string RazorProjectInfoEndpoint = "razor/projectInfo"; public const string RazorMapToDocumentRangesEndpoint = "razor/mapToDocumentRanges"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/MonitorProjectConfigurationFilePathParams.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/MonitorProjectConfigurationFilePathParams.cs deleted file mode 100644 index a5933658e58..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/MonitorProjectConfigurationFilePathParams.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -namespace Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; - -internal class MonitorProjectConfigurationFilePathParams -{ - public required string ProjectKeyId { get; set; } - - public required string? ConfigurationFilePath { get; set; } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs deleted file mode 100644 index 0bf2964aa5f..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MonitorProjectConfigurationFilePathEndpointTest.cs +++ /dev/null @@ -1,511 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; -using Moq; -using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -public class MonitorProjectConfigurationFilePathEndpointTest : LanguageServerTestBase -{ - private readonly IWorkspaceRootPathProvider _workspaceRootPathProvider; - - public MonitorProjectConfigurationFilePathEndpointTest(ITestOutputHelper testOutput) - : base(testOutput) - { - var path = PathUtilities.CreateRootedPath("dir"); - _workspaceRootPathProvider = StrictMock.Of(resolver => - resolver.GetRootPath() == path); - } - - [Fact] - public async Task Handle_Disposed_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var workspaceRootPathProvider = new StrictMock(); - workspaceRootPathProvider.Setup(resolver => resolver.GetRootPath()) - .Throws(); - var configurationFileEndpoint = new MonitorProjectConfigurationFilePathEndpoint( - projectManager, - workspaceRootPathProvider.Object, - listeners: [], - TestLanguageServerFeatureOptions.Instance, - LoggerFactory); - configurationFileEndpoint.Dispose(); - - var debugDirectory = PathUtilities.CreateRootedPath("dir", "obj", "Debug"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var request = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act & Assert - await configurationFileEndpoint.HandleNotificationAsync(request, requestContext, DisposalToken); - } - - [Fact] - public async Task Handle_ConfigurationFilePath_UntrackedMonitorNoops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var workspaceRootPathProvider = new StrictMock(); - workspaceRootPathProvider - .Setup(resolver => resolver.GetRootPath()) - .Throws(); - var configurationFileEndpoint = new MonitorProjectConfigurationFilePathEndpoint( - projectManager, - workspaceRootPathProvider.Object, - listeners: [], - TestLanguageServerFeatureOptions.Instance, - LoggerFactory); - - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var request = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = null!, - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act & Assert - await configurationFileEndpoint.HandleNotificationAsync(request, requestContext, DisposalToken); - } - - [Fact] - public async Task Handle_ConfigurationFilePath_TrackedMonitor_StopsMonitor() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory = PathUtilities.CreateRootedPath("externaldir", "obj", "Debug"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var startRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - await configurationFileEndpoint.HandleNotificationAsync(startRequest, requestContext, DisposalToken); - var stopRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = null!, - }; - - // Act - await configurationFileEndpoint.HandleNotificationAsync(stopRequest, requestContext, DisposalToken); - - // Assert - Assert.Equal(1, detector.StartCount); - Assert.Equal(1, detector.StopCount); - } - - [Fact] - public async Task Handle_InWorkspaceDirectory_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory = PathUtilities.CreateRootedPath("dir", "obj", "Debug"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var startRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - await configurationFileEndpoint.HandleNotificationAsync(startRequest, requestContext, DisposalToken); - - // Assert - Assert.Equal(0, detector.StartCount); - } - - [Fact] - public async Task Handle_InWorkspaceDirectory_MonitorsIfLanguageFeatureOptionSet() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager, - options: new TestLanguageServerFeatureOptions(monitorWorkspaceFolderForConfigurationFiles: false)); - - var debugDirectory = PathUtilities.CreateRootedPath("dir", "obj", "Debug"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var startRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - await configurationFileEndpoint.HandleNotificationAsync(startRequest, requestContext, DisposalToken); - - // Assert - Assert.Equal(1, detector.StartCount); - } - - [Fact] - public async Task Handle_DuplicateMonitors_Noops() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory = PathUtilities.CreateRootedPath("externaldir", "obj", "Debug"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var startRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - await configurationFileEndpoint.HandleNotificationAsync(startRequest, requestContext, DisposalToken); - await configurationFileEndpoint.HandleNotificationAsync(startRequest, requestContext, DisposalToken); - - // Assert - Assert.Equal(1, detector.StartCount); - Assert.Equal(0, detector.StopCount); - } - - [Fact] - public async Task Handle_ChangedConfigurationOutputPath_StartsWithNewPath() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory = PathUtilities.CreateRootedPath("externaldir", "obj", "Debug"); - var releaseDirectory = PathUtilities.CreateRootedPath("externaldir", "obj", "Release"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var debugOutputPath = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - - var releaseOutputPath = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(releaseDirectory, "project.razor.bin") - }; - - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - await configurationFileEndpoint.HandleNotificationAsync(debugOutputPath, requestContext, DisposalToken); - await configurationFileEndpoint.HandleNotificationAsync(releaseOutputPath, requestContext, DisposalToken); - - // Assert - Assert.Equal([debugDirectory, releaseDirectory], detector.StartedWithDirectory); - Assert.Equal(1, detector.StopCount); - } - - [Fact] - public async Task Handle_ChangedConfigurationExternalToInternal_StopsWithoutRestarting() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory = PathUtilities.CreateRootedPath("externaldir", "obj", "Debug"); - var releaseDirectory = PathUtilities.CreateRootedPath("dir", "obj", "Release"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var externalRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - - var internalRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(releaseDirectory, "project.razor.bin") - }; - - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - await configurationFileEndpoint.HandleNotificationAsync(externalRequest, requestContext, DisposalToken); - await configurationFileEndpoint.HandleNotificationAsync(internalRequest, requestContext, DisposalToken); - - // Assert - Assert.Equal([debugDirectory], detector.StartedWithDirectory); - Assert.Equal(1, detector.StopCount); - } - - [Fact] - public async Task Handle_ProjectPublished() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var callCount = 0; - var projectOpenDebugDetector = new TestFileChangeDetector(); - var releaseDetector = new TestFileChangeDetector(); - var postPublishDebugDetector = new TestFileChangeDetector(); - var detectors = new[] { projectOpenDebugDetector, releaseDetector, postPublishDebugDetector }; - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detectors[callCount++], - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory = PathUtilities.CreateRootedPath("externaldir1", "obj", "Debug"); - var releaseDirectory = PathUtilities.CreateRootedPath("externaldir1", "obj", "Release"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - var debugOutputPath = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - - var releaseOutputPath = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(releaseDirectory, "project.razor.bin") - }; - - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - - // Project opened, defaults to Debug output path - await configurationFileEndpoint.HandleNotificationAsync(debugOutputPath, requestContext, DisposalToken); - - // Project published (temporarily moves to release output path) - await configurationFileEndpoint.HandleNotificationAsync(releaseOutputPath, requestContext, DisposalToken); - - // Project publish finished (moves back to debug output path) - await configurationFileEndpoint.HandleNotificationAsync(debugOutputPath, requestContext, DisposalToken); - - // Assert - Assert.Equal(1, projectOpenDebugDetector.StartCount); - Assert.Equal(1, projectOpenDebugDetector.StopCount); - Assert.Equal(1, releaseDetector.StartCount); - Assert.Equal(1, releaseDetector.StopCount); - Assert.Equal(1, postPublishDebugDetector.StartCount); - Assert.Equal(0, postPublishDebugDetector.StopCount); - } - - [Fact] - public async Task Handle_MultipleProjects_StartedAndStopped() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var callCount = 0; - var debug1Detector = new TestFileChangeDetector(); - var debug2Detector = new TestFileChangeDetector(); - var release1Detector = new TestFileChangeDetector(); - var detectors = new[] { debug1Detector, debug2Detector, release1Detector }; - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detectors[callCount++], - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager); - - var debugDirectory1 = PathUtilities.CreateRootedPath("externaldir1", "obj", "Debug"); - var releaseDirectory1 = PathUtilities.CreateRootedPath("externaldir1", "obj", "Release"); - var debugDirectory2 = PathUtilities.CreateRootedPath("externaldir2", "obj", "Debug"); - var projectKeyDirectory1 = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey1 = TestProjectKey.Create(projectKeyDirectory1); - var projectKeyDirectory2 = PathUtilities.CreateRootedPath("dir", "obj2"); - var projectKey2 = TestProjectKey.Create(projectKeyDirectory2); - - var debugOutputPath1 = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey1.Id, - ConfigurationFilePath = Path.Combine(debugDirectory1, "project.razor.bin") - }; - - var releaseOutputPath1 = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey1.Id, - ConfigurationFilePath = Path.Combine(releaseDirectory1, "project.razor.bin") - }; - - var debugOutputPath2 = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey2.Id, - ConfigurationFilePath = Path.Combine(debugDirectory2, "project.razor.bin") - }; - - var requestContext = CreateRazorRequestContext(documentContext: null); - - // Act - await configurationFileEndpoint.HandleNotificationAsync(debugOutputPath1, requestContext, DisposalToken); - await configurationFileEndpoint.HandleNotificationAsync(debugOutputPath2, requestContext, DisposalToken); - await configurationFileEndpoint.HandleNotificationAsync(releaseOutputPath1, requestContext, DisposalToken); - - // Assert - Assert.Equal(1, debug1Detector.StartCount); - Assert.Equal(1, debug1Detector.StopCount); - Assert.Equal(1, debug2Detector.StartCount); - Assert.Equal(0, debug2Detector.StopCount); - Assert.Equal(1, release1Detector.StartCount); - Assert.Equal(0, release1Detector.StopCount); - } - - [Fact] - public async Task Handle_ConfigurationFilePath_TrackedMonitor_RemovesProject() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - - var detector = new TestFileChangeDetector(); - var configurationFileEndpoint = new TestMonitorProjectConfigurationFilePathEndpoint( - () => detector, - _workspaceRootPathProvider, - listeners: [], - LoggerFactory, - projectManager, - options: new TestLanguageServerFeatureOptions(monitorWorkspaceFolderForConfigurationFiles: false)); - - var debugDirectory = PathUtilities.CreateRootedPath("externaldir", "obj", "Debug"); - var projectKeyDirectory = PathUtilities.CreateRootedPath("dir", "obj"); - var projectKey = TestProjectKey.Create(projectKeyDirectory); - - //projectSnapshotManagerAccessor - // .Setup(a => a.Instance.ProjectRemoved(It.IsAny())) - // .Callback((ProjectKey key) => Assert.Equal(projectKey, key)); - - var startRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = Path.Combine(debugDirectory, "project.razor.bin") - }; - var requestContext = CreateRazorRequestContext(documentContext: null); - await configurationFileEndpoint.HandleNotificationAsync(startRequest, requestContext, DisposalToken); - var stopRequest = new MonitorProjectConfigurationFilePathParams() - { - ProjectKeyId = projectKey.Id, - ConfigurationFilePath = null!, - }; - - // Act - await configurationFileEndpoint.HandleNotificationAsync(stopRequest, requestContext, DisposalToken); - - // Assert - Assert.Equal(1, detector.StartCount); - Assert.Equal(1, detector.StopCount); - } - - private class TestMonitorProjectConfigurationFilePathEndpoint( - Func fileChangeDetectorFactory, - IWorkspaceRootPathProvider workspaceDirectoryPathResolver, - IEnumerable listeners, - ILoggerFactory loggerFactory, - IProjectSnapshotManager projectManager, - LanguageServerFeatureOptions? options = null) - : MonitorProjectConfigurationFilePathEndpoint( - projectManager, - workspaceDirectoryPathResolver, - listeners, - options ?? TestLanguageServerFeatureOptions.Instance, - loggerFactory) - { - private readonly Func _fileChangeDetectorFactory = fileChangeDetectorFactory ?? (() => Mock.Of(MockBehavior.Strict)); - - protected override IFileChangeDetector CreateFileChangeDetector() => _fileChangeDetectorFactory(); - } - - private class TestFileChangeDetector : IFileChangeDetector - { - public int StartCount => StartedWithDirectory.Count; - - public List StartedWithDirectory { get; } = new List(); - - public int StopCount { get; private set; } - - public Task StartAsync(string workspaceDirectory, CancellationToken cancellationToken) - { - StartedWithDirectory.Add(workspaceDirectory); - return Task.CompletedTask; - } - - public void Stop() - { - StopCount++; - } - } -} From 5f6036ef393d319de65a7c714bd7e7c7cbb33ef9 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:06:29 -0700 Subject: [PATCH 23/45] Remove ProjectInfoEndpoint --- .../IServiceCollectionExtensions.cs | 11 +- ...ojectConfigurationStateManager.Comparer.cs | 42 - .../ProjectConfigurationStateManager.cs | 165 ---- .../ProjectSystem/ProjectInfoEndpoint.cs | 49 -- .../RazorLanguageServer.cs | 6 - .../Protocol/LanguageServerConstants.cs | 2 - .../ProjectSystem/ProjectInfoParams.cs | 10 - ...ojectConfigurationStateSynchronizerTest.cs | 831 ------------------ .../RazorLanguageServerTest.cs | 5 - 9 files changed, 1 insertion(+), 1120 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.Comparer.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectInfoEndpoint.cs delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/ProjectInfoParams.cs delete mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationStateSynchronizerTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index ff101515f49..fac0483d1ae 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -219,16 +219,7 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); - if (featureOptions.UseProjectConfigurationEndpoint) - { - services.AddSingleton(); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } - + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.Comparer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.Comparer.cs deleted file mode 100644 index 6d9f9ee3ced..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.Comparer.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.ProjectSystem; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; - -internal partial class ProjectConfigurationStateManager -{ - /// - /// Compares two work items from project state manager work queue. - /// - /// - /// Project updates and project removal are treated as equivalent since project removal - /// work item should supersede any project updates in the queue. Project additions are not - /// placed in the queue so project removal would never supersede/overwrite project addition - /// (which would've resulted in us removing a project we never added). - /// - private sealed class Comparer : IEqualityComparer<(ProjectKey ProjectKey, RazorProjectInfo? ProjectInfo)> - { - public static readonly Comparer Instance = new(); - - private Comparer() - { - } - - public bool Equals((ProjectKey ProjectKey, RazorProjectInfo? ProjectInfo) x, (ProjectKey ProjectKey, RazorProjectInfo? ProjectInfo) y) - { - // Project removal should replace project update so treat Removal and non-Removal - // of the same ProjectKey as equivalent work item - return x.ProjectKey.Equals(y.ProjectKey); - } - - public int GetHashCode((ProjectKey ProjectKey, RazorProjectInfo? ProjectInfo) obj) - { - var (projectKey, _) = obj; - - return projectKey.GetHashCode(); - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.cs deleted file mode 100644 index 984da988af0..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectConfigurationStateManager.cs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Serialization; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Utilities; -using Microsoft.VisualStudio.Threading; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; - -/// -/// Updates project system service with new project info. -/// -/// -/// Used to figure out if the project system data is being added, updated or deleted -/// and acts accordingly to update project system service. -/// -internal partial class ProjectConfigurationStateManager : IDisposable -{ - private readonly IRazorProjectService _projectService; - private readonly IProjectSnapshotManager _projectManager; - private readonly ILogger _logger; - - private readonly AsyncBatchingWorkQueue<(ProjectKey ProjectKey, RazorProjectInfo? ProjectInfo)> _workQueue; - private readonly CancellationTokenSource _disposalTokenSource; - private static readonly TimeSpan s_enqueueDelay = TimeSpan.FromMilliseconds(250); - - public ProjectConfigurationStateManager( - IRazorProjectService projectService, - ILoggerFactory loggerFactory, - IProjectSnapshotManager projectManager) - : this(projectService, - loggerFactory, - projectManager, - s_enqueueDelay) - { - } - - // Provided for tests to specify enqueue delay - public ProjectConfigurationStateManager( - IRazorProjectService projectService, - ILoggerFactory loggerFactory, - IProjectSnapshotManager projectManager, - TimeSpan enqueueDelay) - { - _projectService = projectService; - _projectManager = projectManager; - _logger = loggerFactory.GetOrCreateLogger(); - - _disposalTokenSource = new(); - _workQueue = new( - enqueueDelay, - ProcessBatchAsync, - _disposalTokenSource.Token); - } - - public void Dispose() - { - _disposalTokenSource.Cancel(); - _disposalTokenSource.Dispose(); - } - - private async ValueTask ProcessBatchAsync(ImmutableArray<(ProjectKey, RazorProjectInfo?)> items, CancellationToken cancellationToken) - { - foreach (var (projectKey, projectInfo) in items.GetMostRecentUniqueItems(Comparer.Instance)) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - await UpdateProjectAsync(projectKey, projectInfo, cancellationToken).ConfigureAwait(false); - } - } - - public async Task ProjectInfoUpdatedAsync(ProjectKey projectKey, RazorProjectInfo? projectInfo, CancellationToken cancellationToken) - { - if (!_projectManager.TryGetLoadedProject(projectKey, out _)) - { - if (projectInfo is not null) - { - var intermediateOutputPath = projectKey.Id; - _logger.LogInformation($"Found no existing project key for project key '{projectKey.Id}'. Assuming new project."); - - projectKey = await AddProjectAsync(intermediateOutputPath, projectInfo, cancellationToken).ConfigureAwait(false); - } - else - { - _logger.LogWarning($"Found no existing project key '{projectKey.Id}' but projectInfo is null. Assuming no-op deletion."); - return; - } - } - else - { - _logger.LogInformation($"Project info changed for project '{projectKey.Id}'"); - } - - // projectInfo may be null, in which case we are enqueuing remove is "remove" - EnqueueUpdateProject(projectKey, projectInfo); - } - - private Task AddProjectAsync( - string intermediateOutputPath, - RazorProjectInfo projectInfo, - CancellationToken cancellationToken) - { - var projectFilePath = FilePathNormalizer.Normalize(projectInfo.FilePath); - var rootNamespace = projectInfo.RootNamespace; - - _logger.LogInformation($"Project configuration added for project '{projectFilePath}': '{intermediateOutputPath}'"); - - return _projectService.AddProjectAsync( - projectFilePath, - intermediateOutputPath, - projectInfo.Configuration, - rootNamespace, - projectInfo.DisplayName, - cancellationToken); - } - - private Task UpdateProjectAsync(ProjectKey projectKey, RazorProjectInfo? projectInfo, CancellationToken cancellationToken) - { - if (projectInfo is null) - { - return ResetProjectAsync(projectKey, cancellationToken); - } - - _logger.LogInformation($"Actually updating {projectKey} with a real projectInfo"); - - var projectWorkspaceState = projectInfo.ProjectWorkspaceState ?? ProjectWorkspaceState.Default; - var documents = projectInfo.Documents; - return _projectService.UpdateProjectAsync( - projectKey, - projectInfo.Configuration, - projectInfo.RootNamespace, - projectInfo.DisplayName, - projectWorkspaceState, - documents, - cancellationToken); - } - - private void EnqueueUpdateProject(ProjectKey projectKey, RazorProjectInfo? projectInfo) - { - _workQueue.AddWork((projectKey, projectInfo)); - } - - private Task ResetProjectAsync(ProjectKey projectKey, CancellationToken cancellationToken) - { - return _projectService.UpdateProjectAsync( - projectKey, - configuration: null, - rootNamespace: null, - displayName: "", - ProjectWorkspaceState.Default, - documents: ImmutableArray.Empty, - cancellationToken); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectInfoEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectInfoEndpoint.cs deleted file mode 100644 index 4844b2e644d..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/ProjectInfoEndpoint.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.LanguageServer.Serialization; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Serialization; -using Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; - -/// -/// Used to receive project system info updates from the client that were discovered OOB. -/// -[RazorLanguageServerEndpoint(LanguageServerConstants.RazorProjectInfoEndpoint)] -internal class ProjectInfoEndpoint( - ProjectConfigurationStateManager stateManager, - IRazorProjectInfoFileSerializer serializer) - : IRazorNotificationHandler -{ - private readonly ProjectConfigurationStateManager _stateManager = stateManager; - private readonly IRazorProjectInfoFileSerializer _serializer = serializer; - - public bool MutatesSolutionState => false; - - public async Task HandleNotificationAsync(ProjectInfoParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) - { - var count = request.ProjectKeyIds.Length; - - for (var i = 0; i < count; i++) - { - var projectKey = new ProjectKey(request.ProjectKeyIds[i]); - - RazorProjectInfo? projectInfo = null; - - if (request.FilePaths[i] is string filePath) - { - projectInfo = await _serializer.DeserializeFromFileAndDeleteAsync(filePath, cancellationToken).ConfigureAwait(false); - } - - await _stateManager.ProjectInfoUpdatedAsync(projectKey, projectInfo, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 23ed5cc67cd..ed65b3e327f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -190,12 +190,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); - // Project system info handler - if (featureOptions.UseProjectConfigurationEndpoint) - { - services.AddHandler(); - } - services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs index 933b1785632..be2fb1feec3 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs @@ -13,8 +13,6 @@ internal static class LanguageServerConstants public const string RazorLanguageServerName = "Razor Language Server"; - public const string RazorProjectInfoEndpoint = "razor/projectInfo"; - public const string RazorMapToDocumentRangesEndpoint = "razor/mapToDocumentRanges"; public const string RazorCodeActionRunnerCommand = "razor/runCodeAction"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/ProjectInfoParams.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/ProjectInfoParams.cs deleted file mode 100644 index 70bb99108fb..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/ProjectSystem/ProjectInfoParams.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -namespace Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.ProjectSystem; - -internal class ProjectInfoParams -{ - public required string[] ProjectKeyIds { get; init; } - public required string?[] FilePaths { get; init; } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationStateSynchronizerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationStateSynchronizerTest.cs deleted file mode 100644 index 77128251faa..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationStateSynchronizerTest.cs +++ /dev/null @@ -1,831 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.AspNetCore.Razor.LanguageServer.Serialization; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Serialization; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Workspaces; -using Moq; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -public class ProjectConfigurationStateSynchronizerTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) -{ - [Fact] - public async Task ProjectConfigurationFileChanged_Removed_UntrackedProject_CallsUpdate() - { - // Arrange - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/project.razor.bin", - kind: RazorFileChangeKind.Removed, - deserializer: StrictMock.Of()); - - var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(args.ConfigurationFilePath); - var projectKey = new ProjectKey(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.UpdateProjectAsync( - projectKey, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - // Act - synchronizer.ProjectConfigurationFileChanged(args); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Removed_NonNormalizedPaths() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - var intermediateOutputPath = projectInfo.ProjectKey.Id; - var projectKey = TestProjectKey.Create(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(x => x.AddOrUpdateProjectAsync( - projectKey, - projectInfo.FilePath, - It.IsAny(), - projectInfo.RootNamespace, - projectInfo.DisplayName, - projectInfo.ProjectWorkspaceState, - projectInfo.Documents, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - projectServiceMock - .Setup(x => x.UpdateProjectAsync( - projectKey, - null, - null, - "", - ProjectWorkspaceState.Default, - ImmutableArray.Empty, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var addArgs = new ProjectConfigurationFileChangeEventArgs( - "/path/to\\obj/project.razor.bin", - RazorFileChangeKind.Added, - CreateDeserializer(projectInfo)); - - synchronizer.ProjectConfigurationFileChanged(addArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - var removeArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Removed, - deserializer: StrictMock.Of()); - - // Act - synchronizer.ProjectConfigurationFileChanged(removeArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Added_CantDeserialize_Noops() - { - // Arrange - var projectServiceMock = new StrictMock(); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: CreateDeserializer(projectInfo: null)); - - // Act - synchronizer.ProjectConfigurationFileChanged(args); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Added_AddAndUpdatesProject() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - var intermediateOutputPath = projectInfo.ProjectKey.Id; - var projectKey = TestProjectKey.Create(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(service => service.AddOrUpdateProjectAsync( - projectKey, - projectInfo.FilePath, - It.IsAny(), - projectInfo.RootNamespace, - projectInfo.DisplayName, - projectInfo.ProjectWorkspaceState, - projectInfo.Documents, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: CreateDeserializer(projectInfo)); - - // Act - synchronizer.ProjectConfigurationFileChanged(args); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Removed_ResetsProject() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - var intermediateOutputPath = projectInfo.ProjectKey.Id; - var projectKey = TestProjectKey.Create(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(service => service.AddOrUpdateProjectAsync( - projectKey, - projectInfo.FilePath, - It.IsAny(), - projectInfo.RootNamespace, - projectInfo.DisplayName, - projectInfo.ProjectWorkspaceState, - projectInfo.Documents, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - projectServiceMock - .Setup(service => service.UpdateProjectAsync( - projectKey, - null, - null, - "", - ProjectWorkspaceState.Default, - ImmutableArray.Empty, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var addArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: CreateDeserializer(projectInfo)); - - synchronizer.ProjectConfigurationFileChanged(addArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - var removeArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Removed, - deserializer: StrictMock.Of()); - - // Act - synchronizer.ProjectConfigurationFileChanged(removeArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Changed_UpdatesProject() - { - // Arrange - var initialProjectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - var intermediateOutputPath = initialProjectInfo.ProjectKey.Id; - var projectKey = TestProjectKey.Create(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(service => service.AddOrUpdateProjectAsync( - projectKey, - initialProjectInfo.FilePath, - It.IsAny(), - initialProjectInfo.RootNamespace, - initialProjectInfo.DisplayName, - initialProjectInfo.ProjectWorkspaceState, - initialProjectInfo.Documents, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - var changedProjectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - new(RazorLanguageVersion.Experimental, - "TestConfiguration", - Extensions: []), - rootNamespace: "TestRootNamespace2", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp6), - documents: []); - projectServiceMock - .Setup(service => service.AddOrUpdateProjectAsync( - projectKey, - changedProjectInfo.FilePath, - It.IsAny(), - changedProjectInfo.RootNamespace, - changedProjectInfo.DisplayName, - changedProjectInfo.ProjectWorkspaceState, - changedProjectInfo.Documents, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var addArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: CreateDeserializer(initialProjectInfo)); - - synchronizer.ProjectConfigurationFileChanged(addArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - var changedArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Changed, - deserializer: CreateDeserializer(changedProjectInfo)); - - // Act - synchronizer.ProjectConfigurationFileChanged(changedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Changed_CantDeserialize_ResetsProject() - { - // Arrange - var initialProjectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - var intermediateOutputPath = initialProjectInfo.ProjectKey.Id; - var projectKey = TestProjectKey.Create(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(service => service.AddOrUpdateProjectAsync( - projectKey, - initialProjectInfo.FilePath, - It.IsAny(), - initialProjectInfo.RootNamespace, - initialProjectInfo.DisplayName, - initialProjectInfo.ProjectWorkspaceState, - initialProjectInfo.Documents, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - var changedProjectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - new(RazorLanguageVersion.Experimental, - "TestConfiguration", - Extensions: []), - rootNamespace: "TestRootNamespace2", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp6), - documents: []); - - // This is the request that happens when the server is reset - projectServiceMock - .Setup(service => service.UpdateProjectAsync( - projectKey, - null, - null, - "", - ProjectWorkspaceState.Default, - ImmutableArray.Empty, - It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var addArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: CreateDeserializer(initialProjectInfo)); - - synchronizer.ProjectConfigurationFileChanged(addArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - var changedArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Changed, - deserializer: CreateDeserializer(projectInfo: null)); - - // Act - synchronizer.ProjectConfigurationFileChanged(changedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_Changed_UntrackedProject_CallsUpdate() - { - // Arrange - var changedArgs = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Changed, - deserializer: CreateDeserializer(projectInfo: null)); - - var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(changedArgs.ConfigurationFilePath); - var projectKey = new ProjectKey(intermediateOutputPath); - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.UpdateProjectAsync( - projectKey, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - // Act - synchronizer.ProjectConfigurationFileChanged(changedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_RemoveThenAdd_Updates() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath = projectInfo.ProjectKey.Id + "project.razor.bin"; - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var deserializer = CreateDeserializer(projectInfo); - var removedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Removed, deserializer); - var addedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Added, deserializer); - - // Act - synchronizer.ProjectConfigurationFileChanged(removedArgs); - synchronizer.ProjectConfigurationFileChanged(addedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.Verify(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_AddThenRemove_AddsAndRemoves() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath = projectInfo.ProjectKey.Id + "project.razor.bin"; - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.UpdateProjectAsync( - projectInfo.ProjectKey, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var deserializer = CreateDeserializer(projectInfo); - var addedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Added, deserializer); - var removedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Removed, deserializer); - - // Act - synchronizer.ProjectConfigurationFileChanged(addedArgs); - synchronizer.ProjectConfigurationFileChanged(removedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.Verify(p => p.UpdateProjectAsync( - projectInfo.ProjectKey, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_AddThenRemoveThenAdd_AddsAndUpdates() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath = projectInfo.ProjectKey.Id + "project.razor.bin"; - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var deserializer = CreateDeserializer(projectInfo); - var addedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Added, deserializer); - var removedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Removed, deserializer); - - // Act - synchronizer.ProjectConfigurationFileChanged(addedArgs); - synchronizer.ProjectConfigurationFileChanged(removedArgs); - synchronizer.ProjectConfigurationFileChanged(addedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock - .Verify(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_RemoveThenRemoveThenAdd_UpdatesTwice() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath = projectInfo.ProjectKey.Id + "project.razor.bin"; - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var deserializer = CreateDeserializer(projectInfo); - var addedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Added, deserializer); - var removedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Removed, deserializer); - - // Act - synchronizer.ProjectConfigurationFileChanged(removedArgs); - synchronizer.ProjectConfigurationFileChanged(removedArgs); - synchronizer.ProjectConfigurationFileChanged(addedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.Verify(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_AddThenRemoveThenAddThenUpdate_AddsAndUpdates() - { - // Arrange - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath = projectInfo.ProjectKey.Id + "project.razor.bin"; - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var deserializer = CreateDeserializer(projectInfo); - var addedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Added, deserializer); - var removedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Removed, deserializer); - var changedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath, RazorFileChangeKind.Changed, deserializer); - - // Act - synchronizer.ProjectConfigurationFileChanged(addedArgs); - synchronizer.ProjectConfigurationFileChanged(removedArgs); - synchronizer.ProjectConfigurationFileChanged(addedArgs); - synchronizer.ProjectConfigurationFileChanged(changedArgs); - synchronizer.ProjectConfigurationFileChanged(changedArgs); - synchronizer.ProjectConfigurationFileChanged(changedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - // Update is only called twice because the Remove-then-Add is changed to an Update, - // then that Update is deduped with the one following - projectServiceMock - .Verify(p => p.AddOrUpdateProjectAsync( - projectInfo.ProjectKey, - projectInfo.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - - projectServiceMock.VerifyAll(); - } - - [Fact] - public async Task ProjectConfigurationFileChanged_RemoveThenAddDifferentProjects_RemovesAndAdds() - { - // Arrange - var projectInfo1 = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "path/to/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath1 = projectInfo1.ProjectKey.Id + "project.razor.bin"; - - var projectInfo2 = new RazorProjectInfo( - new ProjectKey("/path/other/obj/"), - "path/other/project.csproj", - RazorConfiguration.Default, - rootNamespace: "TestRootNamespace", - displayName: "project", - ProjectWorkspaceState.Create(LanguageVersion.CSharp5), - documents: []); - - var configurationFilePath2 = projectInfo2.ProjectKey.Id + "project.razor.bin"; - - var projectServiceMock = new StrictMock(); - projectServiceMock - .Setup(p => p.UpdateProjectAsync( - projectInfo1.ProjectKey, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - projectServiceMock - .Setup(p => p.AddOrUpdateProjectAsync( - projectInfo2.ProjectKey, - projectInfo2.FilePath, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.CompletedTask); - - using var synchronizer = GetSynchronizer(projectServiceMock.Object); - var synchronizerAccessor = synchronizer.GetTestAccessor(); - - var removedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath1, RazorFileChangeKind.Removed, CreateDeserializer(projectInfo1)); - var addedArgs = new ProjectConfigurationFileChangeEventArgs(configurationFilePath2, RazorFileChangeKind.Added, CreateDeserializer(projectInfo2)); - - // Act - synchronizer.ProjectConfigurationFileChanged(removedArgs); - synchronizer.ProjectConfigurationFileChanged(addedArgs); - - await synchronizerAccessor.WaitUntilCurrentBatchCompletesAsync(); - - // Assert - projectServiceMock.VerifyAll(); - } - - private TestProjectConfigurationStateSynchronizer GetSynchronizer(IRazorProjectService razorProjectService) - => new(razorProjectService, LoggerFactory, TestLanguageServerFeatureOptions.Instance, TimeSpan.FromMilliseconds(50)); - - private static IRazorProjectInfoDeserializer CreateDeserializer(RazorProjectInfo? projectInfo) - { - var deserializerMock = new StrictMock(); - deserializerMock - .Setup(x => x.DeserializeFromFile(It.IsAny())) - .Returns(projectInfo); - - return deserializerMock.Object; - } - - private sealed class TestProjectConfigurationStateSynchronizer( - IRazorProjectService projectService, - ILoggerFactory loggerFactory, - LanguageServerFeatureOptions options, - TimeSpan delay) - : ProjectConfigurationStateSynchronizer(projectService, loggerFactory, options, delay); -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs index 786eb4e32e1..d50ba15bf29 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs @@ -14,8 +14,6 @@ using Xunit; using Xunit.Abstractions; -using RazorLanguageServerConstants = Microsoft.CodeAnalysis.Razor.Protocol.LanguageServerConstants; - namespace Microsoft.AspNetCore.Razor.LanguageServer.Test; public class RazorLanguageServerTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) @@ -65,9 +63,6 @@ public void AllHandlersRegisteredAsync() // CLaSP will throw if two handlers register for the same method, so if THAT doesn't hold it's a CLaSP bug, not a Razor bug. var typeMethods = handlerTypes.Select(t => GetMethodFromType(t)).ToHashSet(); - // razor/projectInfo is currently behind a feature flag, so ignore it for now - typeMethods.Remove(RazorLanguageServerConstants.RazorProjectInfoEndpoint); - if (registeredMethods.Length != typeMethods.Count) { var unregisteredHandlers = typeMethods.Where(t => !registeredMethods.Any(m => m.MethodName == t)); From a61c4e1a509f3d3cf8432833f4f0638457e71a67 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:09:56 -0700 Subject: [PATCH 24/45] Remove ProjectConfigurationFileChangeDetector --- .../ProjectConfigurationFileChangeDetector.cs | 145 ------------------ ...jectConfigurationFileChangeDetectorTest.cs | 84 ---------- 2 files changed, 229 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeDetector.cs delete mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeDetectorTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeDetector.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeDetector.cs deleted file mode 100644 index f91f0f93913..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeDetector.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal class ProjectConfigurationFileChangeDetector( - IEnumerable listeners, - LanguageServerFeatureOptions options, - ILoggerFactory loggerFactory) : IFileChangeDetector -{ - private static readonly ImmutableArray s_ignoredDirectories = - [ - "node_modules", - "bin", - ".vs", - ]; - - private readonly ImmutableArray _listeners = listeners.ToImmutableArray(); - private readonly LanguageServerFeatureOptions _options = options; - private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - - private FileSystemWatcher? _watcher; - - public Task StartAsync(string workspaceDirectory, CancellationToken cancellationToken) - { - // Dive through existing project configuration files and fabricate "added" events so listeners can accurately listen to state changes for them. - - workspaceDirectory = FilePathNormalizer.Normalize(workspaceDirectory); - var existingConfigurationFiles = GetExistingConfigurationFiles(workspaceDirectory); - - _logger.LogDebug($"Triggering events for existing project configuration files"); - - foreach (var configurationFilePath in existingConfigurationFiles) - { - NotifyListeners(new(configurationFilePath, RazorFileChangeKind.Added)); - } - - // This is an entry point for testing - if (!InitializeFileWatchers) - { - return Task.CompletedTask; - } - - try - { - // FileSystemWatcher will throw if the folder we want to watch doesn't exist yet. - if (!Directory.Exists(workspaceDirectory)) - { - _logger.LogInformation($"Workspace directory '{workspaceDirectory}' does not exist yet, so Razor is going to create it."); - Directory.CreateDirectory(workspaceDirectory); - } - } - catch (Exception ex) - { - // Directory.Exists will throw on things like long paths - _logger.LogError(ex, $"Failed validating that file watcher would be successful for '{workspaceDirectory}'"); - - // No point continuing because the FileSystemWatcher constructor would just throw too. - return Task.FromException(ex); - } - - if (cancellationToken.IsCancellationRequested) - { - // Client cancelled connection, no need to setup any file watchers. Server is about to tear down. - return Task.FromCanceled(cancellationToken); - } - - _logger.LogInformation($"Starting configuration file change detector for '{workspaceDirectory}'"); - _watcher = new RazorFileSystemWatcher(workspaceDirectory, _options.ProjectConfigurationFileName) - { - NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime, - IncludeSubdirectories = true, - }; - - _watcher.Created += (sender, args) => NotifyListeners(args.FullPath, RazorFileChangeKind.Added); - _watcher.Deleted += (sender, args) => NotifyListeners(args.FullPath, RazorFileChangeKind.Removed); - _watcher.Changed += (sender, args) => NotifyListeners(args.FullPath, RazorFileChangeKind.Changed); - _watcher.Renamed += (sender, args) => - { - // Translate file renames into remove / add - - if (args.OldFullPath.EndsWith(_options.ProjectConfigurationFileName, FilePathComparison.Instance)) - { - // Renaming from project configuration file to something else. Just remove the configuration file. - NotifyListeners(args.OldFullPath, RazorFileChangeKind.Removed); - } - else if (args.FullPath.EndsWith(_options.ProjectConfigurationFileName, FilePathComparison.Instance)) - { - // Renaming from a non-project configuration file file to a real one. Just add the configuration file. - NotifyListeners(args.FullPath, RazorFileChangeKind.Added); - } - }; - - _watcher.EnableRaisingEvents = true; - - return Task.CompletedTask; - } - - public void Stop() - { - // We're relying on callers to synchronize start/stops so we don't need to ensure one happens before the other. - - _watcher?.Dispose(); - _watcher = null; - } - - // Protected virtual for testing - protected virtual bool InitializeFileWatchers => true; - - // Protected virtual for testing - protected virtual ImmutableArray GetExistingConfigurationFiles(string workspaceDirectory) - { - return DirectoryHelper.GetFilteredFiles( - workspaceDirectory, - _options.ProjectConfigurationFileName, - s_ignoredDirectories, - logger: _logger).ToImmutableArray(); - } - - private void NotifyListeners(string physicalFilePath, RazorFileChangeKind kind) - { - NotifyListeners(new(physicalFilePath, kind)); - } - - private void NotifyListeners(ProjectConfigurationFileChangeEventArgs args) - { - foreach (var listener in _listeners) - { - _logger.LogDebug($"Notifying listener '{listener}' of config file path '{args.ConfigurationFilePath}' change with kind '{args.Kind}'"); - listener.ProjectConfigurationFileChanged(args); - } - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeDetectorTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeDetectorTest.cs deleted file mode 100644 index 8987b488c47..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeDetectorTest.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Microsoft.CodeAnalysis.Razor.Logging; -using Moq; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -public class ProjectConfigurationFileChangeDetectorTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) -{ - [Fact] - public async Task StartAsync_NotifiesListenersOfExistingConfigurationFiles() - { - // Arrange - var eventArgs1 = new List(); - var listenerMock1 = new StrictMock(); - listenerMock1 - .Setup(l => l.ProjectConfigurationFileChanged(It.IsAny())) - .Callback(eventArgs1.Add); - - var eventArgs2 = new List(); - var listenerMock2 = new StrictMock(); - listenerMock2 - .Setup(l => l.ProjectConfigurationFileChanged(It.IsAny())) - .Callback(eventArgs2.Add); - - ImmutableArray existingConfigurationFiles = ["c:/path/to/project.razor.json", "c:/other/path/project.razor.bin"]; - - var detector = new TestProjectConfigurationFileChangeDetector( - [listenerMock1.Object, listenerMock2.Object], - existingConfigurationFiles, - LoggerFactory); - - // Act - await detector.StartAsync("/some/workspace+directory", DisposalToken); - - // Assert - Assert.Collection(eventArgs1, - args => - { - Assert.Equal(RazorFileChangeKind.Added, args.Kind); - Assert.Equal(existingConfigurationFiles[0], args.ConfigurationFilePath); - }, - args => - { - Assert.Equal(RazorFileChangeKind.Added, args.Kind); - Assert.Equal(existingConfigurationFiles[1], args.ConfigurationFilePath); - }); - Assert.Collection(eventArgs2, - args => - { - Assert.Equal(RazorFileChangeKind.Added, args.Kind); - Assert.Equal(existingConfigurationFiles[0], args.ConfigurationFilePath); - }, - args => - { - Assert.Equal(RazorFileChangeKind.Added, args.Kind); - Assert.Equal(existingConfigurationFiles[1], args.ConfigurationFilePath); - }); - } - - private class TestProjectConfigurationFileChangeDetector( - IEnumerable listeners, - ImmutableArray existingConfigurationFiles, - ILoggerFactory loggerFactory) - : ProjectConfigurationFileChangeDetector(listeners, TestLanguageServerFeatureOptions.Instance, loggerFactory) - { - private readonly ImmutableArray _existingConfigurationFiles = existingConfigurationFiles; - - protected override bool InitializeFileWatchers => false; - - protected override ImmutableArray GetExistingConfigurationFiles(string workspaceDirectory) - => _existingConfigurationFiles; - } -} From 8bf003147bb2d8fba0a061752bf46bec7a2a3c4c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:21:38 -0700 Subject: [PATCH 25/45] Log errors in FileWatcherBasedRazorProjectInfoDriver --- .../FileWatcherBasedRazorProjectInfoDriver.cs | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index c15c503a433..76250efce75 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -73,11 +73,12 @@ public async Task InitializeAsync(CancellationToken cancellationToken) { using var stream = File.OpenRead(filePath); - var razorProjectInfo = await RazorProjectInfo - .DeserializeFromAsync(stream, cancellationToken) - .ConfigureAwait(false); + var razorProjectInfo = await TryDeserializeAsync(filePath, cancellationToken).ConfigureAwait(false); - EnqueueUpdate(razorProjectInfo); + if (razorProjectInfo is not null) + { + EnqueueUpdate(razorProjectInfo); + } } if (!Directory.Exists(workspaceDirectoryPath)) @@ -115,11 +116,12 @@ private async ValueTask ProcessBatchAsync(ImmutableArray<(string FilePath, Chang case ChangeKind.AddOrUpdate: using (var stream = File.OpenRead(filePath)) { - var razorProjectInfo = await RazorProjectInfo - .DeserializeFromAsync(stream, token) - .ConfigureAwait(false); + var razorProjectInfo = await TryDeserializeAsync(filePath, token).ConfigureAwait(false); - EnqueueUpdate(razorProjectInfo); + if (razorProjectInfo is not null) + { + EnqueueUpdate(razorProjectInfo); + } } break; @@ -155,4 +157,22 @@ private void EnqueueRename(string oldFilePath, string newFilePath) EnqueueRemove(oldFilePath); EnqueueAddOrChange(newFilePath); } + + private async ValueTask TryDeserializeAsync(string filePath, CancellationToken cancellationToken) + { + try + { + using var stream = File.OpenRead(filePath); + + return await RazorProjectInfo + .DeserializeFromAsync(stream, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogError(ex, $"Error occurred while reading and deserializing '{filePath}'"); + } + + return null; + } } From cb0923ffdbfe83876c13dc7075b91c48842ced69 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:27:34 -0700 Subject: [PATCH 26/45] Remove ProjectConfigurationStateSynchronizer --- .../IServiceCollectionExtensions.cs | 1 - ...IProjectConfigurationFileChangeListener.cs | 9 - ...ConfigurationStateSynchronizer.Comparer.cs | 32 ---- ...igurationStateSynchronizer.TestAccessor.cs | 17 -- .../ProjectConfigurationStateSynchronizer.cs | 174 ------------------ 5 files changed, 233 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectConfigurationFileChangeListener.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.Comparer.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.TestAccessor.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index fac0483d1ae..03aedcac027 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -219,7 +219,6 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectConfigurationFileChangeListener.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectConfigurationFileChangeListener.cs deleted file mode 100644 index 8dd5892d369..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectConfigurationFileChangeListener.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal interface IProjectConfigurationFileChangeListener -{ - void ProjectConfigurationFileChanged(ProjectConfigurationFileChangeEventArgs args); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.Comparer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.Comparer.cs deleted file mode 100644 index 5300496aa69..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.Comparer.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal partial class ProjectConfigurationStateSynchronizer -{ - private sealed class Comparer : IEqualityComparer - { - public static readonly Comparer Instance = new(); - - private Comparer() - { - } - - public bool Equals(Work? x, Work? y) - => (x, y) switch - { - (Work(var keyX), Work(var keyY)) => keyX == keyY, - (null, null) => true, - - _ => false - }; - - public int GetHashCode(Work obj) - => obj.ProjectKey.GetHashCode(); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.TestAccessor.cs deleted file mode 100644 index 5be787a4d8b..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.TestAccessor.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal partial class ProjectConfigurationStateSynchronizer -{ - internal TestAccessor GetTestAccessor() => new(this); - - internal sealed class TestAccessor(ProjectConfigurationStateSynchronizer instance) - { - public Task WaitUntilCurrentBatchCompletesAsync() - => instance._workQueue.WaitUntilCurrentBatchCompletesAsync(); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs deleted file mode 100644 index eae4ecc0393..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal partial class ProjectConfigurationStateSynchronizer : IProjectConfigurationFileChangeListener, IDisposable -{ - private abstract record Work(ProjectKey ProjectKey); - private sealed record ResetProject(ProjectKey ProjectKey) : Work(ProjectKey); - private sealed record UpdateProject(ProjectKey ProjectKey, RazorProjectInfo ProjectInfo) : Work(ProjectKey); - - private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); - - private readonly IRazorProjectService _projectService; - private readonly LanguageServerFeatureOptions _options; - private readonly ILogger _logger; - - private readonly CancellationTokenSource _disposeTokenSource; - private readonly AsyncBatchingWorkQueue _workQueue; - - private readonly Dictionary _resetProjectMap = new(); - - public ProjectConfigurationStateSynchronizer( - IRazorProjectService projectService, - ILoggerFactory loggerFactory, - LanguageServerFeatureOptions options) - : this(projectService, loggerFactory, options, s_delay) - { - } - - protected ProjectConfigurationStateSynchronizer( - IRazorProjectService projectService, - ILoggerFactory loggerFactory, - LanguageServerFeatureOptions options, - TimeSpan delay) - { - _projectService = projectService; - _options = options; - _logger = loggerFactory.GetOrCreateLogger(); - - _disposeTokenSource = new(); - _workQueue = new(delay, ProcessBatchAsync, _disposeTokenSource.Token); - } - - public void Dispose() - { - _disposeTokenSource.Cancel(); - _disposeTokenSource.Dispose(); - } - - private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) - { - foreach (var item in items.GetMostRecentUniqueItems(Comparer.Instance)) - { - if (token.IsCancellationRequested) - { - return; - } - - var itemTask = item switch - { - ResetProject(var projectKey) => ResetProjectAsync(projectKey, token), - UpdateProject(var projectKey, var projectInfo) => UpdateProjectAsync(projectKey, projectInfo, token), - _ => Assumed.Unreachable() - }; - - await itemTask.ConfigureAwait(false); - } - - Task ResetProjectAsync(ProjectKey projectKey, CancellationToken token) - { - _logger.LogInformation($"Resetting {projectKey.Id}."); - - return _projectService - .UpdateProjectAsync( - projectKey, - configuration: null, - rootNamespace: null, - displayName: "", - ProjectWorkspaceState.Default, - documents: [], - token); - } - - Task UpdateProjectAsync(ProjectKey projectKey, RazorProjectInfo projectInfo, CancellationToken token) - { - _logger.LogInformation($"Updating {projectKey.Id}."); - - return _projectService - .AddOrUpdateProjectAsync( - projectKey, - projectInfo.FilePath, - projectInfo.Configuration, - projectInfo.RootNamespace, - projectInfo.DisplayName, - projectInfo.ProjectWorkspaceState, - projectInfo.Documents, - token); - } - } - - public void ProjectConfigurationFileChanged(ProjectConfigurationFileChangeEventArgs args) - { - switch (args.Kind) - { - case RazorFileChangeKind.Changed: - { - if (args.TryDeserialize(_options, out var projectInfo)) - { - var projectKey = projectInfo.ProjectKey; - _logger.LogInformation($"Configuration file changed for project '{projectKey.Id}'."); - - _workQueue.AddWork(new UpdateProject(projectKey, projectInfo)); - } - else - { - var projectKey = args.GetProjectKey(); - _logger.LogWarning($"Failed to deserialize after change to configuration file for project '{projectKey.Id}'."); - - // We found the last associated project file for the configuration file. Reset the project since we can't - // accurately determine its configurations. - _workQueue.AddWork(new ResetProject(projectKey)); - } - } - - break; - - case RazorFileChangeKind.Added: - { - if (args.TryDeserialize(_options, out var projectInfo)) - { - var projectKey = projectInfo.ProjectKey; - _logger.LogInformation($"Configuration file added for project '{projectKey.Id}'."); - - // Update will add the project if it doesn't exist - _workQueue.AddWork(new UpdateProject(projectKey, projectInfo)); - } - else - { - // This is the first time we've seen this configuration file, but we can't deserialize it. - // The only thing we can really do is issue a warning. - _logger.LogWarning($"Failed to deserialize previously unseen configuration file '{args.ConfigurationFilePath}'"); - } - } - - break; - - case RazorFileChangeKind.Removed: - { - var projectKey = args.GetProjectKey(); - _logger.LogInformation($"Configuration file removed for project '{projectKey}'."); - - _workQueue.AddWork(new ResetProject(projectKey)); - } - - break; - } - } -} From 25bc05dcd9b4f699b84a3f1d235c78e1fa32b349 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:27:54 -0700 Subject: [PATCH 27/45] Remove unused IProjectFileChangeListener interface --- .../IProjectFileChangeListener.cs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectFileChangeListener.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectFileChangeListener.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectFileChangeListener.cs deleted file mode 100644 index cbd57d8dac1..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectFileChangeListener.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Razor.LanguageServer.Common; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal interface IProjectFileChangeListener -{ - void ProjectFileChanged(string filePath, RazorFileChangeKind kind); -} From 7699f2600e76c8b984c40df03ef3ecc8e40a8f92 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:31:22 -0700 Subject: [PATCH 28/45] Remove ProjectConfigurationFileChangeEventArgs --- ...ProjectConfigurationFileChangeEventArgs.cs | 85 ----------- ...ectConfigurationFileChangeEventArgsTest.cs | 143 ------------------ 2 files changed, 228 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeEventArgs.cs delete mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeEventArgsTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeEventArgs.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeEventArgs.cs deleted file mode 100644 index 626abb642b2..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationFileChangeEventArgs.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using Microsoft.AspNetCore.Razor.LanguageServer.Serialization; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal sealed class ProjectConfigurationFileChangeEventArgs( - string configurationFilePath, - RazorFileChangeKind kind, - IRazorProjectInfoDeserializer? deserializer = null) : EventArgs -{ - public string ConfigurationFilePath { get; } = configurationFilePath; - public RazorFileChangeKind Kind { get; } = kind; - - private readonly IRazorProjectInfoDeserializer _deserializer = deserializer ?? RazorProjectInfoDeserializer.Instance; - private RazorProjectInfo? _projectInfo; - private readonly object _gate = new(); - private bool _deserialized; - - public bool TryDeserialize(LanguageServerFeatureOptions options, [NotNullWhen(true)] out RazorProjectInfo? projectInfo) - { - if (Kind == RazorFileChangeKind.Removed) - { - // There's no file to represent the snapshot handle. - projectInfo = null; - return false; - } - - lock (_gate) - { - if (!_deserialized) - { - // We use a deserialized flag instead of checking if _projectSnapshotHandle is null because if we're reading an old snapshot - // handle that doesn't deserialize properly it could be expected that it would be null. - _deserialized = true; - var deserializedProjectInfo = _deserializer.DeserializeFromFile(ConfigurationFilePath); - if (deserializedProjectInfo is null) - { - projectInfo = null; - return false; - } - - if (FilePathNormalizer.AreDirectoryPathsEquivalent(deserializedProjectInfo.ProjectKey.Id, ConfigurationFilePath)) - { - // Modify the feature flags on the configuration before storing - deserializedProjectInfo = deserializedProjectInfo with - { - Configuration = deserializedProjectInfo.Configuration with - { - LanguageServerFlags = options.ToLanguageServerFlags() - } - }; - - _projectInfo = deserializedProjectInfo; - } - else - { - // Stale project configuration file, most likely a user copy & pasted the project configuration file and it hasn't - // been re-computed yet. Fail deserialization. - projectInfo = null; - return false; - } - } - - projectInfo = _projectInfo; - } - - return projectInfo is not null; - } - - internal ProjectKey GetProjectKey() - { - var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(ConfigurationFilePath); - return new ProjectKey(intermediateOutputPath); - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeEventArgsTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeEventArgsTest.cs deleted file mode 100644 index 78fa93d8d2d..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectConfigurationFileChangeEventArgsTest.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; -using Microsoft.AspNetCore.Razor.LanguageServer.Serialization; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Moq; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -public class ProjectConfigurationFileChangeEventArgsTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) -{ - [Fact] - public void TryDeserialize_RemovedKind_ReturnsFalse() - { - // Arrange - var deserializerMock = new StrictMock(); - deserializerMock - .Setup(x => x.DeserializeFromFile(It.IsAny())) - .Returns(new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "c:/path/to/project.csproj", - configuration: RazorConfiguration.Default, - rootNamespace: null, - displayName: "project", - projectWorkspaceState: ProjectWorkspaceState.Default, - documents: [])); - - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Removed, - deserializer: deserializerMock.Object); - - // Act - var result = args.TryDeserialize(TestLanguageServerFeatureOptions.Instance, out var handle); - - // Assert - Assert.False(result); - Assert.Null(handle); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5907")] - public void TryDeserialize_DifferingSerializationPaths_ReturnsFalse() - { - // Arrange - var deserializerMock = new StrictMock(); - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/ORIGINAL/obj/"), - "c:/path/to/project.csproj", - configuration: RazorConfiguration.Default, - rootNamespace: null, - displayName: "project", - projectWorkspaceState: ProjectWorkspaceState.Default, - documents: []); - - deserializerMock - .Setup(x => x.DeserializeFromFile(It.IsAny())) - .Returns(projectInfo); - - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/DIFFERENT/obj/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: deserializerMock.Object); - - // Act - var result = args.TryDeserialize(TestLanguageServerFeatureOptions.Instance, out var deserializedProjectInfo); - - // Assert - Assert.False(result); - Assert.Null(deserializedProjectInfo); - } - - [Fact] - public void TryDeserialize_MemoizesResults() - { - // Arrange - var deserializerMock = new StrictMock(); - var projectInfo = new RazorProjectInfo( - new ProjectKey("/path/to/obj/"), - "c:/path/to/project.csproj", - configuration: RazorConfiguration.Default with { LanguageServerFlags = TestLanguageServerFeatureOptions.Instance.ToLanguageServerFlags() }, - rootNamespace: null, - displayName: "project", - projectWorkspaceState: ProjectWorkspaceState.Default, - documents: []); - - deserializerMock - .Setup(x => x.DeserializeFromFile(It.IsAny())) - .Returns(projectInfo); - - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Added, - deserializer: deserializerMock.Object); - - // Act - var result1 = args.TryDeserialize(TestLanguageServerFeatureOptions.Instance, out var projectInfo1); - var result2 = args.TryDeserialize(TestLanguageServerFeatureOptions.Instance, out var projectInfo2); - - // Assert - Assert.True(result1); - Assert.True(result2); - - // Deserialization will cause the LanguageServerFlags to be updated on the configuration, so reference equality will not hold. - // Test equality, and that retrieving same instance on repeat calls works by reference equality of projectInfo1 and projectInfo2. - Assert.Equal(projectInfo, projectInfo1); - Assert.Same(projectInfo1, projectInfo2); - } - - [Fact] - public void TryDeserialize_NullFileDeserialization_MemoizesResults_ReturnsFalse() - { - // Arrange - var deserializerMock = new StrictMock(); - var callCount = 0; - deserializerMock - .Setup(x => x.DeserializeFromFile(It.IsAny())) - .Callback(() => callCount++) - .Returns(null); - - var args = new ProjectConfigurationFileChangeEventArgs( - configurationFilePath: "/path/to/obj/project.razor.bin", - kind: RazorFileChangeKind.Changed, - deserializer: deserializerMock.Object); - - // Act - var result1 = args.TryDeserialize(TestLanguageServerFeatureOptions.Instance, out var handle1); - var result2 = args.TryDeserialize(TestLanguageServerFeatureOptions.Instance, out var handle2); - - // Assert - Assert.False(result1); - Assert.False(result2); - Assert.Null(handle1); - Assert.Null(handle2); - Assert.Equal(1, callCount); - } -} From 9860d0e6906675768312891e6760658ebd0f3b97 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:36:29 -0700 Subject: [PATCH 29/45] Remove UseProjectConfigurationEndpoint option and feature flag --- .../DefaultLanguageServerFeatureOptions.cs | 2 -- .../ConfigurableLanguageServerFeatureOptions.cs | 3 --- .../LanguageServerFeatureOptions.cs | 5 ----- .../RemoteLanguageServerFeatureOptions.cs | 2 -- .../VisualStudioLanguageServerFeatureOptions.cs | 12 ------------ ...crosoft.VisualStudio.RazorExtension.Custom.pkgdef | 6 ------ .../Workspaces/TestLanguageServerFeatureOptions.cs | 2 -- 7 files changed, 32 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs index 6d678af65c6..8395bd453c5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs @@ -45,6 +45,4 @@ public override bool ReturnCodeActionAndRenamePathsWithPrefixedSlash public override bool DisableRazorLanguageServer => false; public override bool ForceRuntimeCodeGeneration => false; - - public override bool UseProjectConfigurationEndpoint => false; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs index 8f1e6f198ca..daf5d3dfe38 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs @@ -26,7 +26,6 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO private readonly bool? _useRazorCohostServer; private readonly bool? _disableRazorLanguageServer; private readonly bool? _forceRuntimeCodeGeneration; - private readonly bool? _useProjectConfigurationEndpoint; public override bool SupportsFileManipulation => _supportsFileManipulation ?? _defaults.SupportsFileManipulation; public override string ProjectConfigurationFileName => _projectConfigurationFileName ?? _defaults.ProjectConfigurationFileName; @@ -44,7 +43,6 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO public override bool UseRazorCohostServer => _useRazorCohostServer ?? _defaults.UseRazorCohostServer; public override bool DisableRazorLanguageServer => _disableRazorLanguageServer ?? _defaults.DisableRazorLanguageServer; public override bool ForceRuntimeCodeGeneration => _forceRuntimeCodeGeneration ?? _defaults.ForceRuntimeCodeGeneration; - public override bool UseProjectConfigurationEndpoint => _useProjectConfigurationEndpoint ?? _defaults.UseProjectConfigurationEndpoint; public ConfigurableLanguageServerFeatureOptions(string[] args) { @@ -71,7 +69,6 @@ public ConfigurableLanguageServerFeatureOptions(string[] args) TryProcessBoolOption(nameof(UseRazorCohostServer), ref _useRazorCohostServer, option, args, i); TryProcessBoolOption(nameof(DisableRazorLanguageServer), ref _disableRazorLanguageServer, option, args, i); TryProcessBoolOption(nameof(ForceRuntimeCodeGeneration), ref _forceRuntimeCodeGeneration, option, args, i); - TryProcessBoolOption(nameof(UseProjectConfigurationEndpoint), ref _useProjectConfigurationEndpoint, option, args, i); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs index 4247c907299..2610384eca1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs @@ -57,9 +57,4 @@ internal abstract class LanguageServerFeatureOptions public LanguageServerFlags ToLanguageServerFlags() => new(ForceRuntimeCodeGeneration); - - /// - /// When enabled, project information will be sent to the server using endpoint instead of file. - /// - public abstract bool UseProjectConfigurationEndpoint { get; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs index a8249101f5f..f856b382aa5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs @@ -48,6 +48,4 @@ internal class RemoteLanguageServerFeatureOptions : LanguageServerFeatureOptions public override bool DisableRazorLanguageServer => throw new InvalidOperationException("This option has not been synced to OOP."); public override bool ForceRuntimeCodeGeneration => throw new InvalidOperationException("This option has not been synced to OOP."); - - public override bool UseProjectConfigurationEndpoint => throw new InvalidOperationException("This option has not been synced to OOP."); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs index b7a832a5786..758da6424b3 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs @@ -18,7 +18,6 @@ internal class VisualStudioLanguageServerFeatureOptions : LanguageServerFeatureO private const string UseRazorCohostServerFeatureFlag = "Razor.LSP.UseRazorCohostServer"; private const string DisableRazorLanguageServerFeatureFlag = "Razor.LSP.DisableRazorLanguageServer"; private const string ForceRuntimeCodeGenerationFeatureFlag = "Razor.LSP.ForceRuntimeCodeGeneration"; - private const string UseProjectConfigurationEndpointFeatureFlag = "Razor.LSP.UseProjectConfigurationEndpoint"; private readonly LSPEditorFeatureDetector _lspEditorFeatureDetector; private readonly Lazy _showAllCSharpCodeActions; @@ -27,7 +26,6 @@ internal class VisualStudioLanguageServerFeatureOptions : LanguageServerFeatureO private readonly Lazy _useRazorCohostServer; private readonly Lazy _disableRazorLanguageServer; private readonly Lazy _forceRuntimeCodeGeneration; - private readonly Lazy _useProjectConfigurationEndpoint; [ImportingConstructor] public VisualStudioLanguageServerFeatureOptions(LSPEditorFeatureDetector lspEditorFeatureDetector) @@ -80,13 +78,6 @@ public VisualStudioLanguageServerFeatureOptions(LSPEditorFeatureDetector lspEdit var forceRuntimeCodeGeneration = featureFlags.IsFeatureEnabled(ForceRuntimeCodeGenerationFeatureFlag, defaultValue: false); return forceRuntimeCodeGeneration; }); - - _useProjectConfigurationEndpoint = new Lazy(() => - { - var featureFlags = (IVsFeatureFlags)Package.GetGlobalService(typeof(SVsFeatureFlags)); - var useProjectConfigurationEndpoint = featureFlags.IsFeatureEnabled(UseProjectConfigurationEndpointFeatureFlag, defaultValue: false); - return useProjectConfigurationEndpoint; - }); } // We don't currently support file creation operations on VS Codespaces or VS Liveshare @@ -125,7 +116,4 @@ public VisualStudioLanguageServerFeatureOptions(LSPEditorFeatureDetector lspEdit /// public override bool ForceRuntimeCodeGeneration => _forceRuntimeCodeGeneration.Value; - - /// - public override bool UseProjectConfigurationEndpoint => _useProjectConfigurationEndpoint.Value; } diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef index b1bdc0a07ab..9b1c0111007 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef @@ -80,9 +80,3 @@ "Value"=dword:00000000 "Title"="Force use of runtime code generation for Razor (requires restart)" "PreviewPaneChannels"="IntPreview,int.main" - -[$RootKey$\FeatureFlags\Razor\LSP\UseProjectConfigurationEndpoint] -"Description"="Use endpoint instead of file to send project information" -"Value"=dword:00000000 -"Title"="Use project configuration LSP endpoint instead of a binary file for Razor (requires restart)" -"PreviewPaneChannels"="IntPreview,int.main" \ No newline at end of file diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs index 18a3810e07d..03342ad8fd7 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs @@ -43,6 +43,4 @@ internal class TestLanguageServerFeatureOptions( public override bool DisableRazorLanguageServer => false; public override bool ForceRuntimeCodeGeneration => forceRuntimeCodeGeneration; - - public override bool UseProjectConfigurationEndpoint => false; } From b448b99ebd4a238cab7dd4d049456928b84d038b Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:39:59 -0700 Subject: [PATCH 30/45] Remove MonitorWorkspaceFolderForConfigurationFiles option --- .../DefaultLanguageServerFeatureOptions.cs | 2 -- .../Hosting/ConfigurableLanguageServerFeatureOptions.cs | 3 --- .../LanguageServerFeatureOptions.cs | 9 --------- .../Initialization/RemoteLanguageServerFeatureOptions.cs | 2 -- .../VisualStudioLanguageServerFeatureOptions.cs | 2 -- .../Workspaces/TestLanguageServerFeatureOptions.cs | 3 --- 6 files changed, 21 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs index 8395bd453c5..e1385602245 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs @@ -38,8 +38,6 @@ public override bool ReturnCodeActionAndRenamePathsWithPrefixedSlash public override bool UsePreciseSemanticTokenRanges => false; - public override bool MonitorWorkspaceFolderForConfigurationFiles => true; - public override bool UseRazorCohostServer => false; public override bool DisableRazorLanguageServer => false; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs index daf5d3dfe38..530d8e1fde3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs @@ -22,7 +22,6 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO private readonly bool? _usePreciseSemanticTokenRanges; private readonly bool? _updateBuffersForClosedDocuments; private readonly bool? _includeProjectKeyInGeneratedFilePath; - private readonly bool? _monitorWorkspaceFolderForConfigurationFiles; private readonly bool? _useRazorCohostServer; private readonly bool? _disableRazorLanguageServer; private readonly bool? _forceRuntimeCodeGeneration; @@ -39,7 +38,6 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO public override bool UsePreciseSemanticTokenRanges => _usePreciseSemanticTokenRanges ?? _defaults.UsePreciseSemanticTokenRanges; public override bool UpdateBuffersForClosedDocuments => _updateBuffersForClosedDocuments ?? _defaults.UpdateBuffersForClosedDocuments; public override bool IncludeProjectKeyInGeneratedFilePath => _includeProjectKeyInGeneratedFilePath ?? _defaults.IncludeProjectKeyInGeneratedFilePath; - public override bool MonitorWorkspaceFolderForConfigurationFiles => _monitorWorkspaceFolderForConfigurationFiles ?? _defaults.MonitorWorkspaceFolderForConfigurationFiles; public override bool UseRazorCohostServer => _useRazorCohostServer ?? _defaults.UseRazorCohostServer; public override bool DisableRazorLanguageServer => _disableRazorLanguageServer ?? _defaults.DisableRazorLanguageServer; public override bool ForceRuntimeCodeGeneration => _forceRuntimeCodeGeneration ?? _defaults.ForceRuntimeCodeGeneration; @@ -65,7 +63,6 @@ public ConfigurableLanguageServerFeatureOptions(string[] args) TryProcessBoolOption(nameof(UsePreciseSemanticTokenRanges), ref _usePreciseSemanticTokenRanges, option, args, i); TryProcessBoolOption(nameof(UpdateBuffersForClosedDocuments), ref _updateBuffersForClosedDocuments, option, args, i); TryProcessBoolOption(nameof(IncludeProjectKeyInGeneratedFilePath), ref _includeProjectKeyInGeneratedFilePath, option, args, i); - TryProcessBoolOption(nameof(MonitorWorkspaceFolderForConfigurationFiles), ref _monitorWorkspaceFolderForConfigurationFiles, option, args, i); TryProcessBoolOption(nameof(UseRazorCohostServer), ref _useRazorCohostServer, option, args, i); TryProcessBoolOption(nameof(DisableRazorLanguageServer), ref _disableRazorLanguageServer, option, args, i); TryProcessBoolOption(nameof(ForceRuntimeCodeGeneration), ref _forceRuntimeCodeGeneration, option, args, i); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs index 2610384eca1..7f45f166eb2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs @@ -37,15 +37,6 @@ internal abstract class LanguageServerFeatureOptions /// public abstract bool IncludeProjectKeyInGeneratedFilePath { get; } - /// - /// Whether to monitor the entire workspace folder for any project.razor.bin files - /// - /// - /// When this is off, the language server won't have any project knowledge unless the - /// razor/monitorProjectConfigurationFilePath notification is sent. - /// - public abstract bool MonitorWorkspaceFolderForConfigurationFiles { get; } - public abstract bool UseRazorCohostServer { get; } public abstract bool DisableRazorLanguageServer { get; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs index f856b382aa5..29cacab06ac 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs @@ -41,8 +41,6 @@ internal class RemoteLanguageServerFeatureOptions : LanguageServerFeatureOptions public override bool IncludeProjectKeyInGeneratedFilePath => s_options.IncludeProjectKeyInGeneratedFilePath; - public override bool MonitorWorkspaceFolderForConfigurationFiles => throw new InvalidOperationException("This option has not been synced to OOP."); - public override bool UseRazorCohostServer => s_options.UseRazorCohostServer; public override bool DisableRazorLanguageServer => throw new InvalidOperationException("This option has not been synced to OOP."); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs index 758da6424b3..bb8ecc3b96d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioLanguageServerFeatureOptions.cs @@ -108,8 +108,6 @@ public VisualStudioLanguageServerFeatureOptions(LSPEditorFeatureDetector lspEdit public override bool UsePreciseSemanticTokenRanges => _usePreciseSemanticTokenRanges.Value; - public override bool MonitorWorkspaceFolderForConfigurationFiles => false; - public override bool UseRazorCohostServer => _useRazorCohostServer.Value; public override bool DisableRazorLanguageServer => _disableRazorLanguageServer.Value; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs index 03342ad8fd7..7d15c6ba522 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs @@ -7,7 +7,6 @@ namespace Microsoft.AspNetCore.Razor.Test.Common.Workspaces; internal class TestLanguageServerFeatureOptions( bool includeProjectKeyInGeneratedFilePath = false, - bool monitorWorkspaceFolderForConfigurationFiles = true, bool forceRuntimeCodeGeneration = false) : LanguageServerFeatureOptions { public static readonly LanguageServerFeatureOptions Instance = new TestLanguageServerFeatureOptions(); @@ -36,8 +35,6 @@ internal class TestLanguageServerFeatureOptions( public override bool IncludeProjectKeyInGeneratedFilePath => includeProjectKeyInGeneratedFilePath; - public override bool MonitorWorkspaceFolderForConfigurationFiles => monitorWorkspaceFolderForConfigurationFiles; - public override bool UseRazorCohostServer => false; public override bool DisableRazorLanguageServer => false; From 06644b96d4e0f3f38a02dbba641323173917b5e3 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:42:26 -0700 Subject: [PATCH 31/45] Remove RazorProjectInfoDeserializer --- .../IRazorProjectInfoDeserializer.cs | 13 ------- .../RazorProjectInfoDeserializer.cs | 37 ------------------- 2 files changed, 50 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/IRazorProjectInfoDeserializer.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/RazorProjectInfoDeserializer.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/IRazorProjectInfoDeserializer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/IRazorProjectInfoDeserializer.cs deleted file mode 100644 index 1f51d6cd2c9..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/IRazorProjectInfoDeserializer.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.IO; -using Microsoft.AspNetCore.Razor.ProjectSystem; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Serialization; - -internal interface IRazorProjectInfoDeserializer -{ - RazorProjectInfo? DeserializeFromFile(string filePath); - RazorProjectInfo? DeserializeFromStream(Stream stream); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/RazorProjectInfoDeserializer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/RazorProjectInfoDeserializer.cs deleted file mode 100644 index 083969e2bd4..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Serialization/RazorProjectInfoDeserializer.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.IO; -using Microsoft.AspNetCore.Razor.ProjectSystem; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Serialization; - -internal sealed class RazorProjectInfoDeserializer : IRazorProjectInfoDeserializer -{ - public static readonly IRazorProjectInfoDeserializer Instance = new RazorProjectInfoDeserializer(); - - private RazorProjectInfoDeserializer() - { - } - - public RazorProjectInfo? DeserializeFromFile(string filePath) - { - using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - - return DeserializeFromStream(stream); - } - - public RazorProjectInfo? DeserializeFromStream(Stream stream) - { - try - { - return RazorProjectInfo.DeserializeFrom(stream); - } - catch - { - // Swallow deserialization exceptions. There's many reasons they can happen, all out of our control. - return null; - } - } -} From 72ed38a6a13fc29b1661123a95d5614956785166 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 14:43:51 -0700 Subject: [PATCH 32/45] Don't add extra IWorkspaceRootPathProvider to the service collection --- .../RazorLanguageServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index ed65b3e327f..d8fdaecc815 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -160,7 +160,6 @@ protected override ILspServices ConstructLspServices() services.AddSingleton(); // Other - services.AddSingleton(); services.AddSingleton(); // Get the DefaultSession for telemetry. This is set by VS with From 34969dedb95f53b0bb3a49b7744306607c835af6 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 15:53:24 -0700 Subject: [PATCH 33/45] Make a couple of tweaks to FileWatcherBasedRazorProjectInfoDriver A couple of changes to match the original implementation in ProjectConfigurationFileChangeDetector --- .../FileWatcherBasedRazorProjectInfoDriver.cs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index 76250efce75..b41dde21823 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -62,6 +62,7 @@ public FileWatcherBasedRazorProjectInfoDriver( public async Task InitializeAsync(CancellationToken cancellationToken) { var workspaceDirectoryPath = _workspaceRootPathProvider.GetRootPath(); + workspaceDirectoryPath = FilePathNormalizer.Normalize(workspaceDirectoryPath); var existingConfigurationFiles = DirectoryHelper.GetFilteredFiles( workspaceDirectoryPath, @@ -87,8 +88,9 @@ public async Task InitializeAsync(CancellationToken cancellationToken) Directory.CreateDirectory(workspaceDirectoryPath); } - _logger.LogInformation($"Starting configuration file change detector for '{workspaceDirectoryPath}'"); - _fileSystemWatcher = new FileSystemWatcher(workspaceDirectoryPath, _options.ProjectConfigurationFileName) + _logger.LogInformation($"Starting {nameof(FileWatcherBasedRazorProjectInfoDriver)}: '{workspaceDirectoryPath}'"); + + _fileSystemWatcher = new RazorFileSystemWatcher(workspaceDirectoryPath, _options.ProjectConfigurationFileName) { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime, IncludeSubdirectories = true, @@ -100,6 +102,22 @@ public async Task InitializeAsync(CancellationToken cancellationToken) _fileSystemWatcher.Renamed += (_, args) => EnqueueRename(args.OldFullPath, args.FullPath); _fileSystemWatcher.EnableRaisingEvents = true; + + void EnqueueAddOrChange(string filePath) + { + _workQueue.AddWork((filePath, ChangeKind.AddOrUpdate)); + } + + void EnqueueRemove(string filePath) + { + _workQueue.AddWork((filePath, ChangeKind.Remove)); + } + + void EnqueueRename(string oldFilePath, string newFilePath) + { + EnqueueRemove(oldFilePath); + EnqueueAddOrChange(newFilePath); + } } private async ValueTask ProcessBatchAsync(ImmutableArray<(string FilePath, ChangeKind Kind)> items, CancellationToken token) @@ -142,22 +160,6 @@ private async ValueTask ProcessBatchAsync(ImmutableArray<(string FilePath, Chang } } - private void EnqueueAddOrChange(string filePath) - { - _workQueue.AddWork((filePath, ChangeKind.AddOrUpdate)); - } - - private void EnqueueRemove(string filePath) - { - _workQueue.AddWork((filePath, ChangeKind.Remove)); - } - - private void EnqueueRename(string oldFilePath, string newFilePath) - { - EnqueueRemove(oldFilePath); - EnqueueAddOrChange(newFilePath); - } - private async ValueTask TryDeserializeAsync(string filePath, CancellationToken cancellationToken) { try From 6f5076438359f8aab9a151d9536d209220d37e2d Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 16:41:24 -0700 Subject: [PATCH 34/45] Ensure that RazorProjectInfoService waits for driver to initializer --- .../FileWatcherBasedRazorProjectInfoDriver.cs | 2 +- .../ProjectSystem/RazorProjectService.cs | 2 ++ .../AbstractRazorProjectInfoDriver.cs | 22 ++++++++++++++++++- .../ProjectSystem/IRazorProjectInfoDriver.cs | 3 +++ .../ProjectSystem/RazorProjectInfoDriver.cs | 4 ++-- .../RazorLanguageServerClient.cs | 1 - .../RazorProjectInfoDriverTest.cs | 2 +- 7 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index b41dde21823..24cd64dede3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -59,7 +59,7 @@ public FileWatcherBasedRazorProjectInfoDriver( }); } - public async Task InitializeAsync(CancellationToken cancellationToken) + protected override async Task InitializeAsync(CancellationToken cancellationToken) { var workspaceDirectoryPath = _workspaceRootPathProvider.GetRootPath(); workspaceDirectoryPath = FilePathNormalizer.Normalize(workspaceDirectoryPath); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index e05fa8f5b9f..236549283df 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -72,6 +72,8 @@ private async Task InitializeAsync(CancellationToken cancellationToken) { _logger.LogTrace($"Initializing {nameof(RazorProjectService)}..."); + await _projectInfoDriver.WaitForInitializationAsync().ConfigureAwait(false); + // Register ourselves as a listener to the project driver. _projectInfoDriver.AddListener(this); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs index 80381ee823f..82e70e38311 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs @@ -26,9 +26,9 @@ private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); private readonly CancellationTokenSource _disposeTokenSource; private readonly AsyncBatchingWorkQueue _workQueue; - private readonly Dictionary _latestProjectInfoMap; private ImmutableArray _listeners; + private readonly Task _initializeTask; protected CancellationToken DisposalToken => _disposeTokenSource.Token; @@ -40,6 +40,7 @@ protected AbstractRazorProjectInfoDriver(ILoggerFactory loggerFactory, TimeSpan? _workQueue = new AsyncBatchingWorkQueue(delay ?? DefaultDelay, ProcessBatchAsync, _disposeTokenSource.Token); _latestProjectInfoMap = []; _listeners = []; + _initializeTask = InitializeAsync(_disposeTokenSource.Token); } public void Dispose() @@ -48,6 +49,15 @@ public void Dispose() _disposeTokenSource.Dispose(); } + public Task WaitForInitializationAsync() + { +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + return _initializeTask; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + } + + protected abstract Task InitializeAsync(CancellationToken cancellationToken); + private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) { foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance)) @@ -110,6 +120,11 @@ protected void EnqueueRemove(ProjectKey projectKey) public ImmutableArray GetLatestProjectInfo() { + if (!_initializeTask.IsCompleted) + { + throw new InvalidOperationException($"{nameof(GetLatestProjectInfo)} cannot be called before initialization is complete."); + } + lock (_latestProjectInfoMap) { using var builder = new PooledArrayBuilder(capacity: _latestProjectInfoMap.Count); @@ -125,6 +140,11 @@ public ImmutableArray GetLatestProjectInfo() public void AddListener(IRazorProjectInfoListener listener) { + if (!_initializeTask.IsCompleted) + { + throw new InvalidOperationException($"An {nameof(IRazorProjectInfoListener)} cannot be added before initialization is complete."); + } + ImmutableInterlocked.Update(ref _listeners, array => array.Add(listener)); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs index 654f6a05365..c465e309174 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IRazorProjectInfoDriver.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Immutable; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -11,6 +12,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; /// internal interface IRazorProjectInfoDriver { + Task WaitForInitializationAsync(); + ImmutableArray GetLatestProjectInfo(); void AddListener(IRazorProjectInfoListener listener); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs index 8e43a5ec312..e6d7dba2c6f 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs @@ -18,12 +18,12 @@ internal sealed partial class RazorProjectInfoDriver( { private readonly IProjectSnapshotManager _projectManager = projectManager; - public async Task InitializeAsync(CancellationToken cancellationToken) + protected override Task InitializeAsync(CancellationToken cancellationToken) { // Even though we aren't mutating the project snapshot manager, we call UpdateAsync(...) here to ensure // that we run on its dispatcher. That ensures that no changes will code in while we are iterating the // current set of projects and connected to the Changed event. - await _projectManager.UpdateAsync(updater => + return _projectManager.UpdateAsync(updater => { foreach (var project in updater.GetProjects()) { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs index bd8071dfd6c..07c511bbc7d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/RazorLanguageServerClient.cs @@ -98,7 +98,6 @@ public event AsyncEventHandler? StopAsync var lspOptions = RazorLSPOptions.From(_clientSettingsManager.GetClientSettings()); var projectInfoDriver = new RazorProjectInfoDriver(_projectManager, _loggerFactory); - await projectInfoDriver.InitializeAsync(token); _host = RazorLanguageServerHost.Create( serverStream, diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs index 24461078235..6ff9e9b0675 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs @@ -255,7 +255,7 @@ await projectManager.UpdateAsync(static updater => var testAccessor = driver.GetTestAccessor(); - await driver.InitializeAsync(DisposalToken); + await driver.WaitForInitializationAsync(); return (driver, testAccessor); } From d6eb60cd62378e2fbf82618de508c3373a0a95d1 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 11 Jun 2024 16:56:02 -0700 Subject: [PATCH 35/45] Make IWorkspaceRootPathProvider.GetRootPath(...) async --- .../CapabilitiesManager.cs | 24 +++++++++++++++++-- .../IWorkspaceRootPathProvider.cs | 5 +++- .../FileWatcherBasedRazorProjectInfoDriver.cs | 2 +- .../RazorFileChangeDetectorManager.cs | 2 +- ...faultWorkspaceDirectoryPathResolverTest.cs | 9 +++---- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs index 73581b62431..7138d134698 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -49,10 +51,28 @@ public InitializeResult GetInitializeResult() public void SetInitializeParams(InitializeParams request) { - _initializeParams = request; + _initializeParams = request ?? throw new ArgumentNullException(nameof(request)); } - public string GetRootPath() + public ValueTask GetRootPathAsync(CancellationToken cancellationToken) + { + return HasInitialized + ? new(GetRootPath()) + : new(GetRootPathCoreAsync(this, cancellationToken)); + + static async Task GetRootPathCoreAsync(CapabilitiesManager manager, CancellationToken cancellationToken) + { + while (!manager.HasInitialized) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(millisecondsDelay: 1, cancellationToken).ConfigureAwait(false); + } + + return manager.GetRootPath(); + } + } + + private string GetRootPath() { var initializeParams = GetInitializeParams(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs index 534887e750f..d27fac4b2d0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Threading; +using System.Threading.Tasks; + namespace Microsoft.AspNetCore.Razor.LanguageServer; internal interface IWorkspaceRootPathProvider { - string GetRootPath(); + ValueTask GetRootPathAsync(CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index 24cd64dede3..2574eb38911 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -61,7 +61,7 @@ public FileWatcherBasedRazorProjectInfoDriver( protected override async Task InitializeAsync(CancellationToken cancellationToken) { - var workspaceDirectoryPath = _workspaceRootPathProvider.GetRootPath(); + var workspaceDirectoryPath = await _workspaceRootPathProvider.GetRootPathAsync(cancellationToken).ConfigureAwait(false); workspaceDirectoryPath = FilePathNormalizer.Normalize(workspaceDirectoryPath); var existingConfigurationFiles = DirectoryHelper.GetFilteredFiles( diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs index bf33eae4f10..cbad18beb7c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs @@ -24,7 +24,7 @@ public async Task OnInitializedAsync(ILspServices services, CancellationToken ca { // Initialized request, this occurs once the server and client have agreed on what sort of features they both support. It only happens once. - var workspaceDirectoryPath = _workspaceRootPathProvider.GetRootPath(); + var workspaceDirectoryPath = await _workspaceRootPathProvider.GetRootPathAsync(cancellationToken).ConfigureAwait(false); foreach (var fileChangeDetector in _fileChangeDetectors) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs index 413347f3e7d..704de2c824a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultWorkspaceDirectoryPathResolverTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; public class DefaultWorkspaceDirectoryPathResolverTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { [Fact] - public void Resolve_RootUriUnavailable_UsesRootPath() + public async Task Resolve_RootUriUnavailable_UsesRootPath() { // Arrange var expectedWorkspaceDirectory = "/testpath"; @@ -28,14 +29,14 @@ public void Resolve_RootUriUnavailable_UsesRootPath() capabilitiesManager.SetInitializeParams(initializeParams); // Act - var workspaceDirectoryPath = capabilitiesManager.GetRootPath(); + var workspaceDirectoryPath = await capabilitiesManager.GetRootPathAsync(DisposalToken); // Assert Assert.Equal(expectedWorkspaceDirectory, workspaceDirectoryPath); } [Fact] - public void Resolve_RootUriPrefered() + public async Task Resolve_RootUriPrefered() { // Arrange var initialWorkspaceDirectory = "C:\\testpath"; @@ -58,7 +59,7 @@ public void Resolve_RootUriPrefered() capabilitiesManager.SetInitializeParams(initializeParams); // Act - var workspaceDirectoryPath = capabilitiesManager.GetRootPath(); + var workspaceDirectoryPath = await capabilitiesManager.GetRootPathAsync(DisposalToken); // Assert var expectedWorkspaceDirectory = "C:/testpath"; From 0d449f3bd51fcb3c1c77db02319de072043208f9 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 08:46:45 -0700 Subject: [PATCH 36/45] Don't open file for reading twice --- .../FileWatcherBasedRazorProjectInfoDriver.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index 2574eb38911..d38a4b983a3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -72,8 +72,6 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke foreach (var filePath in existingConfigurationFiles) { - using var stream = File.OpenRead(filePath); - var razorProjectInfo = await TryDeserializeAsync(filePath, cancellationToken).ConfigureAwait(false); if (razorProjectInfo is not null) @@ -132,14 +130,11 @@ private async ValueTask ProcessBatchAsync(ImmutableArray<(string FilePath, Chang switch (changeKind) { case ChangeKind.AddOrUpdate: - using (var stream = File.OpenRead(filePath)) - { - var razorProjectInfo = await TryDeserializeAsync(filePath, token).ConfigureAwait(false); + var razorProjectInfo = await TryDeserializeAsync(filePath, token).ConfigureAwait(false); - if (razorProjectInfo is not null) - { - EnqueueUpdate(razorProjectInfo); - } + if (razorProjectInfo is not null) + { + EnqueueUpdate(razorProjectInfo); } break; From 22e2511770fffa051f7715b1445f50c38f618b20 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 08:48:32 -0700 Subject: [PATCH 37/45] Trim "Microsoft.CodeAnalysis.Razor." from ILogger category name --- .../Logging/ILoggerFactoryExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs index 68125deb94b..d7dc82d67e8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Logging/ILoggerFactoryExtensions.cs @@ -21,7 +21,8 @@ public static ILogger GetOrCreateLogger(this ILoggerFactory factory, Type type) private static string TrimTypeName(string name) { if (TryTrim(name, "Microsoft.VisualStudio.", out var trimmedName) || - TryTrim(name, "Microsoft.AspNetCore.Razor.", out trimmedName)) + TryTrim(name, "Microsoft.AspNetCore.Razor.", out trimmedName) || + TryTrim(name, "Microsoft.CodeAnalysis.Razor.", out trimmedName)) { return trimmedName; } From cb203a189d7f0bac2c3615a128a039d80eb412e5 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 08:56:27 -0700 Subject: [PATCH 38/45] RazorProjectService shouldn't implement IOnInitialized `RazorProjectService` doesn't need to force itself to be initialized when the language server is initialized. It's registered as an `IRazorStartupService`, so it starts initializing right away. Since all `RazorProjectService` public entry points await initialization, we don't need to force it to finish initializing in `OnInitialized`. This is especially true if the driver itself implicitly waits for `OnInitialized`. --- .../Extensions/IServiceCollectionExtensions.cs | 1 - .../ProjectSystem/RazorProjectService.cs | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 03aedcac027..79602f20765 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -213,7 +213,6 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton(); services.AddSingleton(); services.AddSingleton((services) => (RazorProjectService)services.GetRequiredService()); - services.AddSingleton(sp => (RazorProjectService)sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index 236549283df..7895975b8cd 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -28,11 +28,10 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; /// Maintains the language server's with the semantics of Razor's project model. /// /// -/// This service implements both to ensure it is created early and -/// to ensure that its initialization completes when the language server -/// finishes initialization. +/// This service implements both to ensure it is created early being initializing itself +/// immediately. /// -internal partial class RazorProjectService : IRazorProjectService, IRazorProjectInfoListener, IRazorStartupService, IOnInitialized, IDisposable +internal partial class RazorProjectService : IRazorProjectService, IRazorProjectInfoListener, IRazorStartupService, IDisposable { private readonly IRazorProjectInfoDriver _projectInfoDriver; private readonly IProjectSnapshotManager _projectManager; @@ -96,11 +95,6 @@ await AddOrUpdateProjectCoreAsync( } - Task IOnInitialized.OnInitializedAsync(ILspServices services, CancellationToken cancellationToken) -#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks - => _initializeTask; -#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks - // Call to ensure that any public IRazorProjectService methods wait for initialization to complete. private ValueTask WaitForInitializationAsync() => _initializeTask is { IsCompleted: true } From 76ab7e465f1cec6e68c65c5d79553a17e55b638c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 10:32:26 -0700 Subject: [PATCH 39/45] Remove RazorProjectInfoFileSerializer --- .../IServiceCollectionExtensions.cs | 1 - .../IRazorProjectInfoFileSerializer.cs | 14 ---- .../RazorProjectInfoFileSerializer.cs | 82 ------------------- ...ualStudioRazorProjectInfoFileSerializer.cs | 15 ---- 4 files changed, 112 deletions(-) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/IRazorProjectInfoFileSerializer.cs delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/RazorProjectInfoFileSerializer.cs delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoFileSerializer.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 79602f20765..d38a773f624 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -26,7 +26,6 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.SemanticTokens; -using Microsoft.CodeAnalysis.Razor.Serialization; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/IRazorProjectInfoFileSerializer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/IRazorProjectInfoFileSerializer.cs deleted file mode 100644 index b5c5ed8e082..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/IRazorProjectInfoFileSerializer.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.ProjectSystem; - -namespace Microsoft.CodeAnalysis.Razor.Serialization; - -internal interface IRazorProjectInfoFileSerializer -{ - Task SerializeToTempFileAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken); - Task DeserializeFromFileAndDeleteAsync(string filePath, CancellationToken cancellationToken); -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/RazorProjectInfoFileSerializer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/RazorProjectInfoFileSerializer.cs deleted file mode 100644 index 793763ec7a0..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Serialization/RazorProjectInfoFileSerializer.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MessagePack; -using MessagePack.Resolvers; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Serialization.MessagePack.Resolvers; -using Microsoft.CodeAnalysis.Razor.Logging; - -namespace Microsoft.CodeAnalysis.Razor.Serialization; - -internal class RazorProjectInfoFileSerializer(ILoggerFactory loggerFactory) : IRazorProjectInfoFileSerializer, IDisposable -{ - private static readonly MessagePackSerializerOptions s_options = MessagePackSerializerOptions.Standard - .WithResolver(CompositeResolver.Create( - RazorProjectInfoResolver.Instance, - StandardResolver.Instance)); - - private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - private readonly List _filePathsToDelete = []; - - public void Dispose() - { - if (_filePathsToDelete.Count > 0) - { - foreach (var filePath in _filePathsToDelete) - { - DeleteFile(filePath); - } - } - } - - public async Task SerializeToTempFileAsync(RazorProjectInfo projectInfo, CancellationToken cancellationToken) - { - var filePath = Path.GetTempFileName(); - - using var stream = File.OpenWrite(filePath); - await MessagePackSerializer.SerializeAsync(stream, projectInfo, s_options, cancellationToken).ConfigureAwait(false); - - return filePath; - } - - public async Task DeserializeFromFileAndDeleteAsync(string filePath, CancellationToken cancellationToken) - { - RazorProjectInfo projectInfo; - - using (var stream = File.OpenRead(filePath)) - { - projectInfo = await MessagePackSerializer.DeserializeAsync(stream, s_options, cancellationToken).ConfigureAwait(false); - } - - if (!DeleteFile(filePath)) - { - _filePathsToDelete.Add(filePath); - } - - return projectInfo; - } - - private bool DeleteFile(string filePath) - { - try - { - if (File.Exists(filePath)) - { - File.Delete(filePath); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, $"{ex.GetType().FullName} encountered when attempting to delete '{filePath}'"); - } - - return false; - } -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoFileSerializer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoFileSerializer.cs deleted file mode 100644 index 909851fb644..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/VisualStudioRazorProjectInfoFileSerializer.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.ComponentModel.Composition; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Serialization; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; - -[Export(typeof(IRazorProjectInfoFileSerializer))] -[method: ImportingConstructor] -internal sealed class VisualStudioRazorProjectInfoFileSerializer(ILoggerFactory loggerFactory) - : RazorProjectInfoFileSerializer(loggerFactory) -{ -} From 6cfeab00ffd6c8045bfca2466990dfa937a6f602 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 12:36:02 -0700 Subject: [PATCH 40/45] Don't dispose services twice For types that have a `CancellationTokenSource` that is triggered during `Dispose()`, we should check to see if it has already been cancelled to avoid disposing twice. Implicitly, there's a race here if there are multiple threads calling `DIspose()`, but these are all handled by the DI containers we use. --- .../Diagnostics/RazorDiagnosticsPublisher.cs | 5 +++++ .../OpenDocumentGenerator.cs | 5 +++++ .../ProjectSystem/RazorProjectService.cs | 5 +++++ .../RazorFileChangeDetector.cs | 5 +++++ .../WorkspaceSemanticTokensRefreshNotifier.cs | 5 +++++ .../ProjectSystem/AbstractRazorProjectInfoDriver.cs | 5 +++++ .../ProjectSystem/ProjectSnapshotManager.Dispatcher.cs | 5 +++++ .../Documents/EditorDocumentManagerListener.cs | 5 +++++ .../DynamicFiles/BackgroundDocumentGenerator.cs | 5 +++++ .../Logging/RazorActivityLog.cs | 5 +++++ .../VsSolutionUpdatesProjectSnapshotChangeTrigger.cs | 5 +++++ .../WorkspaceProjectStateChangeDetector.cs | 5 +++++ 12 files changed, 60 insertions(+) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs index bd92d5ed7de..d6bb5beb5c1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs @@ -83,6 +83,11 @@ protected RazorDiagnosticsPublisher( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs index 27226f8bafb..133b5078afc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs @@ -57,6 +57,11 @@ public OpenDocumentGenerator( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index 7895975b8cd..d0b02bacc8e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -63,6 +63,11 @@ public RazorProjectService( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetector.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetector.cs index a6943ef7c38..e2d19d4f889 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetector.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetector.cs @@ -50,6 +50,11 @@ protected RazorFileChangeDetector(IEnumerable listener public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs index cf1d6e4c1c3..af8ae8a3d26 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs @@ -43,6 +43,11 @@ public WorkspaceSemanticTokensRefreshNotifier( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _optionsChangeListener.Dispose(); _disposeTokenSource.Cancel(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs index 82e70e38311..71749c4d26b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs @@ -45,6 +45,11 @@ protected AbstractRazorProjectInfoDriver(ILoggerFactory loggerFactory, TimeSpan? public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.Dispatcher.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.Dispatcher.cs index 7240d722778..8c208630b19 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.Dispatcher.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.Dispatcher.cs @@ -168,6 +168,11 @@ protected override IEnumerable GetScheduledTasks() public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _taskQueue.Complete(); _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/EditorDocumentManagerListener.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/EditorDocumentManagerListener.cs index 983e29ba694..04b3eab8f1c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/EditorDocumentManagerListener.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/EditorDocumentManagerListener.cs @@ -77,6 +77,11 @@ public EditorDocumentManagerListener( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/DynamicFiles/BackgroundDocumentGenerator.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/DynamicFiles/BackgroundDocumentGenerator.cs index 532091bac0d..25eea35708c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/DynamicFiles/BackgroundDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/DynamicFiles/BackgroundDocumentGenerator.cs @@ -59,6 +59,11 @@ protected BackgroundDocumentGenerator( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Logging/RazorActivityLog.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Logging/RazorActivityLog.cs index d730873c029..428f61bdd66 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Logging/RazorActivityLog.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Logging/RazorActivityLog.cs @@ -40,6 +40,11 @@ public RazorActivityLog( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs index b9a468b94c6..c1e9de5988f 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs @@ -66,6 +66,11 @@ public VsSolutionUpdatesProjectSnapshotChangeTrigger( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _disposeTokenSource.Cancel(); _disposeTokenSource.Dispose(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/WorkspaceProjectStateChangeDetector.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/WorkspaceProjectStateChangeDetector.cs index 71c34ec93b4..6da28b7d884 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/WorkspaceProjectStateChangeDetector.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/WorkspaceProjectStateChangeDetector.cs @@ -73,6 +73,11 @@ public WorkspaceProjectStateChangeDetector( public void Dispose() { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + _projectManager.Changed -= ProjectManager_Changed; _workspace.WorkspaceChanged -= Workspace_WorkspaceChanged; From 547ad382513c574dc1a5c077122f1f2829497b81 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 16:59:06 -0700 Subject: [PATCH 41/45] Fix ValidateMultipleJsonFiles integration test --- .../ProjectSystem/RazorProjectService.cs | 4 ++-- .../AbstractRazorEditorTest.cs | 10 +++++++--- .../InProcess/RazorProjectSystemInProcess.cs | 12 ++++++++++- .../MultiTargetProjectTests.cs | 20 +++++++++++-------- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index d0b02bacc8e..916d1a07c19 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -28,8 +28,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; /// Maintains the language server's with the semantics of Razor's project model. /// /// -/// This service implements both to ensure it is created early being initializing itself -/// immediately. +/// This service implements to ensure it is created early so it can begin +/// initializing immediately. /// internal partial class RazorProjectService : IRazorProjectService, IRazorProjectInfoListener, IRazorStartupService, IDisposable { diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/AbstractRazorEditorTest.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/AbstractRazorEditorTest.cs index 5d25a4893ca..851d3aaec12 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/AbstractRazorEditorTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/AbstractRazorEditorTest.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.Internal.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Settings; @@ -26,6 +27,7 @@ public abstract class AbstractRazorEditorTest(ITestOutputHelper testOutputHelper private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; private ILogger? _testLogger; + private string? _projectFilePath; protected virtual bool ComponentClassificationExpected => true; @@ -33,6 +35,8 @@ public abstract class AbstractRazorEditorTest(ITestOutputHelper testOutputHelper protected virtual string TargetFrameworkElement => $"""{TargetFramework}"""; + protected string ProjectFilePath => _projectFilePath.AssumeNotNull(); + public override async Task InitializeAsync() { await base.InitializeAsync(); @@ -43,15 +47,15 @@ public override async Task InitializeAsync() VisualStudioLogging.AddCustomLoggers(); - var projectFilePath = await CreateAndOpenBlazorProjectAsync(ControlledHangMitigatingCancellationToken); + _projectFilePath = await CreateAndOpenBlazorProjectAsync(ControlledHangMitigatingCancellationToken); await TestServices.SolutionExplorer.RestoreNuGetPackagesAsync(ControlledHangMitigatingCancellationToken); await TestServices.Workspace.WaitForProjectSystemAsync(ControlledHangMitigatingCancellationToken); - await TestServices.RazorProjectSystem.WaitForProjectFileAsync(projectFilePath, ControlledHangMitigatingCancellationToken); + await TestServices.RazorProjectSystem.WaitForProjectFileAsync(_projectFilePath, ControlledHangMitigatingCancellationToken); var razorFilePath = await TestServices.SolutionExplorer.GetAbsolutePathForProjectRelativeFilePathAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.IndexRazorFile, ControlledHangMitigatingCancellationToken); - await TestServices.RazorProjectSystem.WaitForRazorFileInProjectAsync(projectFilePath, razorFilePath, ControlledHangMitigatingCancellationToken); + await TestServices.RazorProjectSystem.WaitForRazorFileInProjectAsync(_projectFilePath, razorFilePath, ControlledHangMitigatingCancellationToken); // We open the Index.razor file, and wait for 3 RazorComponentElement's to be classified, as that // way we know the LSP server is up, running, and has processed both local and library-sourced Components diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs index 04265cf350c..168c4c5c0fd 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs @@ -11,6 +11,7 @@ using Xunit; using Microsoft.VisualStudio.Razor.LanguageClient; using Microsoft.AspNetCore.Razor.Threading; +using System.Collections.Immutable; namespace Microsoft.VisualStudio.Extensibility.Testing; @@ -71,6 +72,16 @@ await Helper.RetryAsync(ct => }, TimeSpan.FromMilliseconds(100), cancellationToken); } + public async Task> GetProjectKeyIdsForProjectAsync(string projectFilePath, CancellationToken cancellationToken) + { + var projectManager = await TestServices.Shell.GetComponentModelServiceAsync(cancellationToken); + Assert.NotNull(projectManager); + + var projectKeys = projectManager.GetAllProjectKeys(projectFilePath); + + return projectKeys.SelectAsArray(key => key.Id); + } + public async Task WaitForCSharpVirtualDocumentAsync(string razorFilePath, CancellationToken cancellationToken) { var documentManager = await TestServices.Shell.GetComponentModelServiceAsync(cancellationToken); @@ -93,4 +104,3 @@ await Helper.RetryAsync(ct => }, TimeSpan.FromMilliseconds(100), cancellationToken); } } - diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs index e13bd2417a9..a498690669b 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs @@ -11,19 +11,23 @@ namespace Microsoft.VisualStudio.Razor.IntegrationTests; public class MultiTargetProjectTests(ITestOutputHelper testOutputHelper) : AbstractRazorEditorTest(testOutputHelper) { - protected override string TargetFrameworkElement => $"""net7.0;{TargetFramework}"""; + private const string OtherTargetFramework = "net7.0"; + + protected override string TargetFrameworkElement => $"""{OtherTargetFramework};{TargetFramework}"""; [IdeFact] - public async Task ValidateMultipleJsonFiles() + public async Task ValidateMultipleProjects() { - var solutionPath = await TestServices.SolutionExplorer.GetDirectoryNameAsync(ControlledHangMitigatingCancellationToken); + // This just verifies that there are actually two projects present with the same file path: + // one for each target framework. + + var projectKeyIds = await TestServices.RazorProjectSystem.GetProjectKeyIdsForProjectAsync(ProjectFilePath, ControlledHangMitigatingCancellationToken); - // This is a little odd, but there is no "real" way to check this via VS, and one of the most important things this test can do - // is ensure that each target framework gets its own project.razor.bin file, and doesn't share one from a cache or anything. - Assert.Equal(2, GetProjectRazorJsonFileCount()); + projectKeyIds = projectKeyIds.Sort(); - int GetProjectRazorJsonFileCount() - => Directory.EnumerateFiles(solutionPath, "project.razor.*.bin", SearchOption.AllDirectories).Count(); + Assert.Equal(2, projectKeyIds.Length); + Assert.Contains(OtherTargetFramework, projectKeyIds[0]); + Assert.Contains(TargetFramework, projectKeyIds[1]); } [IdeFact] From b8808996e466e5606a5aea4ac5b90eb12eec2bc8 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 17:10:30 -0700 Subject: [PATCH 42/45] Remove OpenExistingProject_WithReopenedFile_NoProjectRazorJson integration test --- .../ProjectTests.cs | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs index 00431964460..ad744cb6260 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs @@ -114,36 +114,4 @@ public async Task OpenExistingProject_WithReopenedFile() await TestServices.Editor.CloseCodeFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ErrorCshtmlFile, saveFile: false, ControlledHangMitigatingCancellationToken); } - - [IdeFact] - public async Task OpenExistingProject_WithReopenedFile_NoProjectRazorJson() - { - var solutionPath = await TestServices.SolutionExplorer.GetDirectoryNameAsync(ControlledHangMitigatingCancellationToken); - var expectedProjectFileName = await TestServices.SolutionExplorer.GetAbsolutePathForProjectRelativeFilePathAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ProjectFile, ControlledHangMitigatingCancellationToken); - - // Open SurveyPrompt and make sure its all up and running - await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ErrorCshtmlFile, ControlledHangMitigatingCancellationToken); - await TestServices.Editor.WaitForSemanticClassificationAsync("class name", ControlledHangMitigatingCancellationToken, count: 1); - - await TestServices.SolutionExplorer.CloseSolutionAsync(ControlledHangMitigatingCancellationToken); - - // Clear out the project.razor.bin file which ensures our restored file will have to be in the Misc Project - var projectRazorJsonFileName = Directory.EnumerateFiles(solutionPath, "project.razor.*.bin", SearchOption.AllDirectories).First(); - File.Delete(projectRazorJsonFileName); - - var solutionFileName = Path.Combine(solutionPath, RazorProjectConstants.BlazorSolutionName + ".sln"); - await TestServices.SolutionExplorer.OpenSolutionAsync(solutionFileName, ControlledHangMitigatingCancellationToken); - - await TestServices.Workspace.WaitForProjectSystemAsync(ControlledHangMitigatingCancellationToken); - - await TestServices.Editor.WaitForSemanticClassificationAsync("class name", ControlledHangMitigatingCancellationToken, count: 1); - - TestServices.Input.Send("1"); - - // Make sure the test framework didn't do something weird and create new project - var actualProjectFileName = await TestServices.SolutionExplorer.GetAbsolutePathForProjectRelativeFilePathAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ProjectFile, ControlledHangMitigatingCancellationToken); - Assert.Equal(expectedProjectFileName, actualProjectFileName); - - await TestServices.Editor.CloseCodeFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ErrorCshtmlFile, saveFile: false, ControlledHangMitigatingCancellationToken); - } } From af6e0d8735ee455bdbd6b39d40fbeb34a966e201 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 12 Jun 2024 17:11:57 -0700 Subject: [PATCH 43/45] Remove version of OpenExistingProject_WithReopenedFile integration test that deleted bin file --- .../MultiTargetProjectTests.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs index a498690669b..006bccb5f92 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.IO; -using System.Linq; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -57,9 +56,8 @@ public async Task OpenExistingProject() Assert.Equal(expectedProjectFileName, actualProjectFileName); } - [IdeTheory] - [CombinatorialData] - public async Task OpenExistingProject_WithReopenedFile(bool deleteProjectRazorJson) + [IdeFact] + public async Task OpenExistingProject_WithReopenedFile() { var solutionPath = await TestServices.SolutionExplorer.GetDirectoryNameAsync(ControlledHangMitigatingCancellationToken); var expectedProjectFileName = await TestServices.SolutionExplorer.GetAbsolutePathForProjectRelativeFilePathAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ProjectFile, ControlledHangMitigatingCancellationToken); @@ -70,13 +68,6 @@ public async Task OpenExistingProject_WithReopenedFile(bool deleteProjectRazorJs await TestServices.SolutionExplorer.CloseSolutionAsync(ControlledHangMitigatingCancellationToken); - if (deleteProjectRazorJson) - { - // Clear out the project.razor.bin file which ensures our restored file will have to be in the Misc Project - var projectRazorJsonFileName = Directory.EnumerateFiles(solutionPath, "project.razor.*.bin", SearchOption.AllDirectories).First(); - File.Delete(projectRazorJsonFileName); - } - var solutionFileName = Path.Combine(solutionPath, RazorProjectConstants.BlazorSolutionName + ".sln"); await TestServices.SolutionExplorer.OpenSolutionAsync(solutionFileName, ControlledHangMitigatingCancellationToken); From 8e085c4e665b5eef47d8871155132da8160d23a4 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Jun 2024 10:36:01 -0700 Subject: [PATCH 44/45] Fix initialization contract for RazorProjectInfo drivers `AbstractRazorProjectInfoDriver`can't call `InitializeAsync(...)` in its constructor because the driver will only be partially constructed. To address that, add a `StartInitialization` method that drivers call from their constructor. This will kick off initialization and set the result of a `TaskCompletionSource` when it finishes. --- .../FileWatcherBasedRazorProjectInfoDriver.cs | 2 ++ .../AbstractRazorProjectInfoDriver.cs | 30 +++++++++++++++---- .../ProjectSystem/RazorProjectInfoDriver.cs | 18 +++++++---- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs index d38a4b983a3..3fb31f160aa 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/FileWatcherBasedRazorProjectInfoDriver.cs @@ -57,6 +57,8 @@ public FileWatcherBasedRazorProjectInfoDriver( _fileSystemWatcher?.Dispose(); _fileSystemWatcher = null; }); + + StartInitialization(); } protected override async Task InitializeAsync(CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs index 71749c4d26b..e8cac61438f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Utilities; +using Microsoft.VisualStudio.Threading; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -28,7 +29,7 @@ private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey); private readonly AsyncBatchingWorkQueue _workQueue; private readonly Dictionary _latestProjectInfoMap; private ImmutableArray _listeners; - private readonly Task _initializeTask; + private readonly TaskCompletionSource _initializationTaskSource; protected CancellationToken DisposalToken => _disposeTokenSource.Token; @@ -40,7 +41,7 @@ protected AbstractRazorProjectInfoDriver(ILoggerFactory loggerFactory, TimeSpan? _workQueue = new AsyncBatchingWorkQueue(delay ?? DefaultDelay, ProcessBatchAsync, _disposeTokenSource.Token); _latestProjectInfoMap = []; _listeners = []; - _initializeTask = InitializeAsync(_disposeTokenSource.Token); + _initializationTaskSource = new(); } public void Dispose() @@ -57,10 +58,29 @@ public void Dispose() public Task WaitForInitializationAsync() { #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks - return _initializeTask; + return _initializationTaskSource.Task; #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks } + /// + /// MUST be called in the constructor of any descendent + /// to kick off initialization. + /// + protected void StartInitialization() + { + // Kick off initialization asynchronously and call TrySetResult(true) in the continuation. + InitializeAsync(_disposeTokenSource.Token) + .ContinueWith( + _ => + { + _initializationTaskSource.TrySetResult(true); + }, + _disposeTokenSource.Token, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default) + .Forget(); + } + protected abstract Task InitializeAsync(CancellationToken cancellationToken); private async ValueTask ProcessBatchAsync(ImmutableArray items, CancellationToken token) @@ -125,7 +145,7 @@ protected void EnqueueRemove(ProjectKey projectKey) public ImmutableArray GetLatestProjectInfo() { - if (!_initializeTask.IsCompleted) + if (!_initializationTaskSource.Task.IsCompleted) { throw new InvalidOperationException($"{nameof(GetLatestProjectInfo)} cannot be called before initialization is complete."); } @@ -145,7 +165,7 @@ public ImmutableArray GetLatestProjectInfo() public void AddListener(IRazorProjectInfoListener listener) { - if (!_initializeTask.IsCompleted) + if (!_initializationTaskSource.Task.IsCompleted) { throw new InvalidOperationException($"An {nameof(IRazorProjectInfoListener)} cannot be added before initialization is complete."); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs index e6d7dba2c6f..dcce7b2f649 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/ProjectSystem/RazorProjectInfoDriver.cs @@ -10,13 +10,19 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; -internal sealed partial class RazorProjectInfoDriver( - IProjectSnapshotManager projectManager, - ILoggerFactory loggerFactory, - TimeSpan? delay = null) - : AbstractRazorProjectInfoDriver(loggerFactory, delay) +internal sealed partial class RazorProjectInfoDriver : AbstractRazorProjectInfoDriver { - private readonly IProjectSnapshotManager _projectManager = projectManager; + private readonly IProjectSnapshotManager _projectManager; + + public RazorProjectInfoDriver( + IProjectSnapshotManager projectManager, + ILoggerFactory loggerFactory, + TimeSpan? delay = null) : base(loggerFactory, delay) + { + _projectManager = projectManager; + + StartInitialization(); + } protected override Task InitializeAsync(CancellationToken cancellationToken) { From c58e948ab70797e5bf997cabdc9f20a5dbaa3473 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Jun 2024 11:15:47 -0700 Subject: [PATCH 45/45] Don't spin in GetRootPathAsync waiting for initialize Instead of spinning, use a TaskCompletionSource to track initialization and an AsyncLazy for the root path. --- .../CapabilitiesManager.cs | 69 ++++++++++--------- .../IWorkspaceRootPathProvider.cs | 2 +- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs index 7138d134698..46635c91ffb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs @@ -4,30 +4,44 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.RpcContracts.Settings; +using Microsoft.VisualStudio.Threading; namespace Microsoft.AspNetCore.Razor.LanguageServer; -internal sealed class CapabilitiesManager(ILspServices lspServices) - : IInitializeManager, IClientCapabilitiesService, IWorkspaceRootPathProvider +internal sealed class CapabilitiesManager : IInitializeManager, IClientCapabilitiesService, IWorkspaceRootPathProvider { - private readonly ILspServices _lspServices = lspServices; - private InitializeParams? _initializeParams; + private readonly ILspServices _lspServices; + private readonly TaskCompletionSource _initializeParamsTaskSource; + private readonly AsyncLazy _lazyRootPath; - public bool HasInitialized => _initializeParams is not null; + public bool HasInitialized => _initializeParamsTaskSource.Task.IsCompleted; public bool CanGetClientCapabilities => HasInitialized; public VSInternalClientCapabilities ClientCapabilities => GetInitializeParams().Capabilities.ToVSInternalClientCapabilities(); + public CapabilitiesManager(ILspServices lspServices) + { + _lspServices = lspServices; + + _initializeParamsTaskSource = new(); + +#pragma warning disable VSTHRD012 // Provide JoinableTaskFactory where allowed + _lazyRootPath = new(ComputeRootPathAsync); +#pragma warning restore VSTHRD012 + } + public InitializeParams GetInitializeParams() - => _initializeParams ?? - throw new InvalidOperationException($"{nameof(GetInitializeParams)} was called before '{Methods.InitializeName}'"); + { + return _initializeParamsTaskSource.Task.VerifyCompleted(); + } public InitializeResult GetInitializeResult() { @@ -51,39 +65,32 @@ public InitializeResult GetInitializeResult() public void SetInitializeParams(InitializeParams request) { - _initializeParams = request ?? throw new ArgumentNullException(nameof(request)); + if (_initializeParamsTaskSource.Task.IsCompleted) + { + throw new InvalidOperationException($"{nameof(SetInitializeParams)} already called."); + } + + _initializeParamsTaskSource.TrySetResult(request); } - public ValueTask GetRootPathAsync(CancellationToken cancellationToken) + private async Task ComputeRootPathAsync() { - return HasInitialized - ? new(GetRootPath()) - : new(GetRootPathCoreAsync(this, cancellationToken)); +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + var initializeParams = await _initializeParamsTaskSource.Task.ConfigureAwaitRunInline(); +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks - static async Task GetRootPathCoreAsync(CapabilitiesManager manager, CancellationToken cancellationToken) + if (initializeParams.RootUri is Uri rootUri) { - while (!manager.HasInitialized) - { - cancellationToken.ThrowIfCancellationRequested(); - await Task.Delay(millisecondsDelay: 1, cancellationToken).ConfigureAwait(false); - } - - return manager.GetRootPath(); + return rootUri.GetAbsoluteOrUNCPath(); } - } - private string GetRootPath() - { - var initializeParams = GetInitializeParams(); + // RootUri was added in LSP3, fall back to RootPath - if (initializeParams.RootUri is null) - { #pragma warning disable CS0618 // Type or member is obsolete - // RootUri was added in LSP3, fallback to RootPath - return initializeParams.RootPath.AssumeNotNull(); + return initializeParams.RootPath.AssumeNotNull(); #pragma warning restore CS0618 // Type or member is obsolete - } - - return initializeParams.RootUri.GetAbsoluteOrUNCPath(); } + + public Task GetRootPathAsync(CancellationToken cancellationToken) + => _lazyRootPath.GetValueAsync(cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs index d27fac4b2d0..eff10da1cb8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IWorkspaceRootPathProvider.cs @@ -8,5 +8,5 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; internal interface IWorkspaceRootPathProvider { - ValueTask GetRootPathAsync(CancellationToken cancellationToken); + Task GetRootPathAsync(CancellationToken cancellationToken); }