Skip to content

Commit 8f1aa3e

Browse files
No more project.razor.bin files (in VS, at least) (#10475)
This is a pretty substantial pull request, so I'm getting it out there for review. I have not fully tested the change with VS Code, but Visual Studio is working ok. This change completely refreshes all of the machinery around how `RazorProjectInfo` instances are passed from the Razor language client to the Razor language server. This is generally a complicated process with a lot of potential for reliability issues. The language client is expected to serialize a `RazorProjectInfo` instance representing a project update to a known location within the project's folder on disk. The language server watches for writes to that file (the dreaded `project.razor.bin` file, deserializes it and updates the server's project system. Here's how things work after this change: - There is a new `IRazorProjectInfoDriver` abstraction that is optionally passed into `RazorLanguageServer` when it is created. - The language server connects an `IRazorProjectInfoListener` to the driver and is notified of project updates and removes. For an update, the listener is provided a `RazorProjectInfo` instance. For a remove, it's just given a `ProjectKey`. - In the language server, `RazorProjectService` provides the `IRazorProjectInfoListener` implementation. This results in a much simpler system with less machinery, allowing several "file detectors" and "state syncrhronizers" to be deleted. - For Visual Studio, an `IRazorProjectInfoDriver` is provided to the language server performs no serialization/deserialization of `RazorProjectInfo` instances. Instead, the language client and server just shared the same `RazorProjectInfo` instances. I'm expecting this to lead to performance and memory allocation wins. - For Visual Studio Code, no `IRazorProjectInfoDriver` implementation is provided. So, the language server falls back to a special `FileWatcherBasedRazorProjectInfoDriver` implementation that watches the workspace folder. The `RazorWorkspaceListener` in `ExernalAcess.RoslynWorkspace` still serializes `RazorProjectInfo` instances for VS Code. So, that should work as it did before, though I still need to test it. (I'd happily accept any help on this.) Significant deletions from the language server in this change: - `ProjectConfigurationFileChangeDetector` - `ProjectConfigurationFileChangeEventArgs` - `ProjectConfigurationStateSynchronizer` - `MonitorProjectConfigurationFilePathEndpoint` - `ProjectConfigurationStateManager` - `ProjectInfoEndpoint` - `RazorProjectInfoDeserializer` Significant deletions from VS in this change: - `ProejctConfigurationFilePathChangedEventsArgs` - `ProjectConfigurationFilePathStore` - `RazorProjectInfoEndpointPublisher` - `RazorProjectInfoFileSerializer` - `RazorProjectInfoPublisher` Razor-CI for PR Validation: https://dev.azure.com/dnceng/internal/_build/results?buildId=2473027&view=results
2 parents 9f10012 + c58e948 commit 8f1aa3e

File tree

94 files changed

+1430
-5322
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+1430
-5322
lines changed

src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorLanguageServerBenchmarkBase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public RazorLanguageServerBenchmarkBase()
3535
serverStream,
3636
razorLoggerFactory,
3737
NoOpTelemetryReporter.Instance,
38-
configure: (collection) =>
38+
configureServices: (collection) =>
3939
{
4040
collection.AddSingleton<IOnInitialized, NoopClientNotifierService>();
4141
collection.AddSingleton<IClientConnection, NoopClientNotifierService>();

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CapabilitiesManager.cs

+45-13
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Razor.Threading;
8+
using Microsoft.CodeAnalysis.Razor;
59
using Microsoft.CodeAnalysis.Razor.Protocol;
610
using Microsoft.CodeAnalysis.Razor.Workspaces;
711
using Microsoft.CommonLanguageServerProtocol.Framework;
812
using Microsoft.VisualStudio.LanguageServer.Protocol;
13+
using Microsoft.VisualStudio.RpcContracts.Settings;
14+
using Microsoft.VisualStudio.Threading;
915

1016
namespace Microsoft.AspNetCore.Razor.LanguageServer;
1117

12-
internal class CapabilitiesManager : IInitializeManager<InitializeParams, InitializeResult>, IClientCapabilitiesService
18+
internal sealed class CapabilitiesManager : IInitializeManager<InitializeParams, InitializeResult>, IClientCapabilitiesService, IWorkspaceRootPathProvider
1319
{
14-
private InitializeParams? _initializeParams;
1520
private readonly ILspServices _lspServices;
21+
private readonly TaskCompletionSource<InitializeParams> _initializeParamsTaskSource;
22+
private readonly AsyncLazy<string> _lazyRootPath;
1623

17-
public bool HasInitialized => _initializeParams is not null;
24+
public bool HasInitialized => _initializeParamsTaskSource.Task.IsCompleted;
1825

1926
public bool CanGetClientCapabilities => HasInitialized;
2027

@@ -23,16 +30,17 @@ internal class CapabilitiesManager : IInitializeManager<InitializeParams, Initia
2330
public CapabilitiesManager(ILspServices lspServices)
2431
{
2532
_lspServices = lspServices;
33+
34+
_initializeParamsTaskSource = new();
35+
36+
#pragma warning disable VSTHRD012 // Provide JoinableTaskFactory where allowed
37+
_lazyRootPath = new(ComputeRootPathAsync);
38+
#pragma warning restore VSTHRD012
2639
}
2740

2841
public InitializeParams GetInitializeParams()
2942
{
30-
if (_initializeParams is null)
31-
{
32-
throw new InvalidOperationException($"{nameof(GetInitializeParams)} was called before '{Methods.InitializeName}'");
33-
}
34-
35-
return _initializeParams;
43+
return _initializeParamsTaskSource.Task.VerifyCompleted();
3644
}
3745

3846
public InitializeResult GetInitializeResult()
@@ -49,16 +57,40 @@ public InitializeResult GetInitializeResult()
4957
provider.ApplyCapabilities(serverCapabilities, vsClientCapabilities);
5058
}
5159

52-
var initializeResult = new InitializeResult
60+
return new InitializeResult
5361
{
5462
Capabilities = serverCapabilities,
5563
};
56-
57-
return initializeResult;
5864
}
5965

6066
public void SetInitializeParams(InitializeParams request)
6167
{
62-
_initializeParams = request;
68+
if (_initializeParamsTaskSource.Task.IsCompleted)
69+
{
70+
throw new InvalidOperationException($"{nameof(SetInitializeParams)} already called.");
71+
}
72+
73+
_initializeParamsTaskSource.TrySetResult(request);
74+
}
75+
76+
private async Task<string> ComputeRootPathAsync()
77+
{
78+
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
79+
var initializeParams = await _initializeParamsTaskSource.Task.ConfigureAwaitRunInline();
80+
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
81+
82+
if (initializeParams.RootUri is Uri rootUri)
83+
{
84+
return rootUri.GetAbsoluteOrUNCPath();
85+
}
86+
87+
// RootUri was added in LSP3, fall back to RootPath
88+
89+
#pragma warning disable CS0618 // Type or member is obsolete
90+
return initializeParams.RootPath.AssumeNotNull();
91+
#pragma warning restore CS0618 // Type or member is obsolete
6392
}
93+
94+
public Task<string> GetRootPathAsync(CancellationToken cancellationToken)
95+
=> _lazyRootPath.GetValueAsync(cancellationToken);
6496
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs

-4
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,9 @@ public override bool ReturnCodeActionAndRenamePathsWithPrefixedSlash
3838

3939
public override bool UsePreciseSemanticTokenRanges => false;
4040

41-
public override bool MonitorWorkspaceFolderForConfigurationFiles => true;
42-
4341
public override bool UseRazorCohostServer => false;
4442

4543
public override bool DisableRazorLanguageServer => false;
4644

4745
public override bool ForceRuntimeCodeGeneration => false;
48-
49-
public override bool UseProjectConfigurationEndpoint => false;
5046
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultWorkspaceDirectoryPathResolver.cs

-40
This file was deleted.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs

+5
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ protected RazorDiagnosticsPublisher(
8383

8484
public void Dispose()
8585
{
86+
if (_disposeTokenSource.IsCancellationRequested)
87+
{
88+
return;
89+
}
90+
8691
_disposeTokenSource.Cancel();
8792
_disposeTokenSource.Dispose();
8893
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

+2-20
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
2727
using Microsoft.CodeAnalysis.Razor.Protocol;
2828
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
29-
using Microsoft.CodeAnalysis.Razor.Serialization;
3029
using Microsoft.CodeAnalysis.Razor.Workspaces;
3130
using Microsoft.CommonLanguageServerProtocol.Framework;
3231
using Microsoft.Extensions.DependencyInjection;
@@ -48,6 +47,7 @@ public static void AddLifeCycleServices(this IServiceCollection services, RazorL
4847
services.AddSingleton<CapabilitiesManager>();
4948
services.AddSingleton<IInitializeManager<InitializeParams, InitializeResult>, CapabilitiesManager>(sp => sp.GetRequiredService<CapabilitiesManager>());
5049
services.AddSingleton<IClientCapabilitiesService>(sp => sp.GetRequiredService<CapabilitiesManager>());
50+
services.AddSingleton<IWorkspaceRootPathProvider>(sp => sp.GetRequiredService<CapabilitiesManager>());
5151
services.AddSingleton<AbstractRequestContextFactory<RazorRequestContext>, RazorRequestContextFactory>();
5252

5353
services.AddSingleton<ICapabilitiesProvider, RazorLanguageServerCapability>();
@@ -211,31 +211,13 @@ public static void AddDocumentManagementServices(this IServiceCollection service
211211

212212
services.AddSingleton<RemoteTextLoaderFactory, DefaultRemoteTextLoaderFactory>();
213213
services.AddSingleton<IRazorProjectService, RazorProjectService>();
214+
services.AddSingleton<IRazorStartupService>((services) => (RazorProjectService)services.GetRequiredService<IRazorProjectService>());
214215
services.AddSingleton<IRazorStartupService, OpenDocumentGenerator>();
215216
services.AddSingleton<IRazorDocumentMappingService, RazorDocumentMappingService>();
216217
services.AddSingleton<RazorFileChangeDetectorManager>();
217218
services.AddSingleton<IOnInitialized>(sp => sp.GetRequiredService<RazorFileChangeDetectorManager>());
218219

219-
if (featureOptions.UseProjectConfigurationEndpoint)
220-
{
221-
services.AddSingleton<IRazorProjectInfoFileSerializer, RazorProjectInfoFileSerializer>();
222-
services.AddSingleton<ProjectConfigurationStateManager>();
223-
}
224-
else
225-
{
226-
services.AddSingleton<IProjectConfigurationFileChangeListener, ProjectConfigurationStateSynchronizer>();
227-
}
228-
229220
services.AddSingleton<IRazorFileChangeListener, RazorFileSynchronizer>();
230-
231-
// If we're not monitoring the whole workspace folder for configuration changes, then we don't actually need the the file change
232-
// detector wired up via DI, as the razor/monitorProjectConfigurationFilePath endpoint will directly construct one. This means
233-
// it can be a little simpler, and doesn't need to worry about which folders it's told to listen to.
234-
if (featureOptions.MonitorWorkspaceFolderForConfigurationFiles)
235-
{
236-
services.AddSingleton<IFileChangeDetector, ProjectConfigurationFileChangeDetector>();
237-
}
238-
239221
services.AddSingleton<IFileChangeDetector, RazorFileChangeDetector>();
240222

241223
// Document processed listeners

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs

-6
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO
2222
private readonly bool? _usePreciseSemanticTokenRanges;
2323
private readonly bool? _updateBuffersForClosedDocuments;
2424
private readonly bool? _includeProjectKeyInGeneratedFilePath;
25-
private readonly bool? _monitorWorkspaceFolderForConfigurationFiles;
2625
private readonly bool? _useRazorCohostServer;
2726
private readonly bool? _disableRazorLanguageServer;
2827
private readonly bool? _forceRuntimeCodeGeneration;
29-
private readonly bool? _useProjectConfigurationEndpoint;
3028

3129
public override bool SupportsFileManipulation => _supportsFileManipulation ?? _defaults.SupportsFileManipulation;
3230
public override string ProjectConfigurationFileName => _projectConfigurationFileName ?? _defaults.ProjectConfigurationFileName;
@@ -40,11 +38,9 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO
4038
public override bool UsePreciseSemanticTokenRanges => _usePreciseSemanticTokenRanges ?? _defaults.UsePreciseSemanticTokenRanges;
4139
public override bool UpdateBuffersForClosedDocuments => _updateBuffersForClosedDocuments ?? _defaults.UpdateBuffersForClosedDocuments;
4240
public override bool IncludeProjectKeyInGeneratedFilePath => _includeProjectKeyInGeneratedFilePath ?? _defaults.IncludeProjectKeyInGeneratedFilePath;
43-
public override bool MonitorWorkspaceFolderForConfigurationFiles => _monitorWorkspaceFolderForConfigurationFiles ?? _defaults.MonitorWorkspaceFolderForConfigurationFiles;
4441
public override bool UseRazorCohostServer => _useRazorCohostServer ?? _defaults.UseRazorCohostServer;
4542
public override bool DisableRazorLanguageServer => _disableRazorLanguageServer ?? _defaults.DisableRazorLanguageServer;
4643
public override bool ForceRuntimeCodeGeneration => _forceRuntimeCodeGeneration ?? _defaults.ForceRuntimeCodeGeneration;
47-
public override bool UseProjectConfigurationEndpoint => _useProjectConfigurationEndpoint ?? _defaults.UseProjectConfigurationEndpoint;
4844

4945
public ConfigurableLanguageServerFeatureOptions(string[] args)
5046
{
@@ -67,11 +63,9 @@ public ConfigurableLanguageServerFeatureOptions(string[] args)
6763
TryProcessBoolOption(nameof(UsePreciseSemanticTokenRanges), ref _usePreciseSemanticTokenRanges, option, args, i);
6864
TryProcessBoolOption(nameof(UpdateBuffersForClosedDocuments), ref _updateBuffersForClosedDocuments, option, args, i);
6965
TryProcessBoolOption(nameof(IncludeProjectKeyInGeneratedFilePath), ref _includeProjectKeyInGeneratedFilePath, option, args, i);
70-
TryProcessBoolOption(nameof(MonitorWorkspaceFolderForConfigurationFiles), ref _monitorWorkspaceFolderForConfigurationFiles, option, args, i);
7166
TryProcessBoolOption(nameof(UseRazorCohostServer), ref _useRazorCohostServer, option, args, i);
7267
TryProcessBoolOption(nameof(DisableRazorLanguageServer), ref _disableRazorLanguageServer, option, args, i);
7368
TryProcessBoolOption(nameof(ForceRuntimeCodeGeneration), ref _forceRuntimeCodeGeneration, option, args, i);
74-
TryProcessBoolOption(nameof(UseProjectConfigurationEndpoint), ref _useProjectConfigurationEndpoint, option, args, i);
7569
}
7670
}
7771

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLanguageServerHost.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading.Tasks;
88
using Microsoft.AspNetCore.Razor.Telemetry;
99
using Microsoft.CodeAnalysis.Razor.Logging;
10+
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
1011
using Microsoft.CodeAnalysis.Razor.Workspaces;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.VisualStudio.LanguageServer.Protocol;
@@ -42,10 +43,11 @@ public static RazorLanguageServerHost Create(
4243
Stream output,
4344
ILoggerFactory loggerFactory,
4445
ITelemetryReporter telemetryReporter,
45-
Action<IServiceCollection>? configure = null,
46+
Action<IServiceCollection>? configureServices = null,
4647
LanguageServerFeatureOptions? featureOptions = null,
4748
RazorLSPOptions? razorLSPOptions = null,
4849
ILspServerActivationTracker? lspServerActivationTracker = null,
50+
IRazorProjectInfoDriver? projectInfoDriver = null,
4951
TraceSource? traceSource = null)
5052
{
5153
var (jsonRpc, jsonSerializer) = CreateJsonRpc(input, output);
@@ -61,9 +63,10 @@ public static RazorLanguageServerHost Create(
6163
jsonSerializer,
6264
loggerFactory,
6365
featureOptions,
64-
configure,
66+
configureServices,
6567
razorLSPOptions,
6668
lspServerActivationTracker,
69+
projectInfoDriver,
6770
telemetryReporter);
6871

6972
var host = new RazorLanguageServerHost(server);

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IProjectFileChangeListener.cs

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
47
namespace Microsoft.AspNetCore.Razor.LanguageServer;
58

6-
internal interface IProjectConfigurationFileChangeListener
9+
internal interface IWorkspaceRootPathProvider
710
{
8-
void ProjectConfigurationFileChanged(ProjectConfigurationFileChangeEventArgs args);
11+
Task<string> GetRootPathAsync(CancellationToken cancellationToken);
912
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public OpenDocumentGenerator(
5757

5858
public void Dispose()
5959
{
60+
if (_disposeTokenSource.IsCancellationRequested)
61+
{
62+
return;
63+
}
64+
6065
_disposeTokenSource.Cancel();
6166
_disposeTokenSource.Dispose();
6267
}

0 commit comments

Comments
 (0)