Skip to content

Commit bddbba5

Browse files
authored
Add another UI context to control Razor cohost pre-init (#79875)
~Putting this up as draft for now, gonna see if I can do a dual test insertion to confirm this makes RPS happy. The UI context is defined in dotnet/razor#12079 so its a little bit painful to validate everything together :)~ This PR does two things: 1. Makes the lifetime service Lazy, which it should have always been but I forgot. Not being lazy means Razor can't move to it, as just MEF construction causes RPS regressions. Oops! 2. Adds a ui context to control pre-initialization. This allows Razor to control whether that occurs, when, and whether it actually does anything. Until Razor is in this won't do anything, but runs in the dual test insertion (https://devdiv.visualstudio.com/DevDiv/_git/VS/pullrequest/664449) look good so far.
2 parents 5fc0475 + e260767 commit bddbba5

File tree

2 files changed

+44
-21
lines changed

2 files changed

+44
-21
lines changed

src/Tools/ExternalAccess/Razor/Features/Cohost/Constants.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ internal static class Constants
1111
{
1212
public const string RazorLanguageName = LanguageInfoProvider.RazorLanguageName;
1313

14-
// The UI context is provided by Razor, so this guid must match the one in https://github.com/dotnet/razor/blob/main/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorConstants.cs
14+
// These UI contexts are provided by Razor, so must match https://github.com/dotnet/razor/blob/main/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorConstants.cs
1515
public static readonly Guid RazorCohostingUIContext = new Guid("6d5b86dc-6b8a-483b-ae30-098a3c7d6774");
16+
17+
public static readonly Guid RazorCapabilityPresentUIContext = new Guid("2077a158-ee71-484c-be76-350a1d49eaea");
1618
}

src/Tools/ExternalAccess/Razor/Features/Cohost/RazorStartupServiceFactory.cs

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
2323
internal sealed class RazorStartupServiceFactory(
2424
[Import(AllowDefault = true)] IUIContextActivationService? uIContextActivationService,
2525
[Import(AllowDefault = true)] Lazy<ICohostStartupService>? cohostStartupService,
26-
[Import(AllowDefault = true)] AbstractRazorCohostLifecycleService? razorCohostLifecycleService) : ILspServiceFactory
26+
[Import(AllowDefault = true)] Lazy<AbstractRazorCohostLifecycleService>? razorCohostLifecycleService) : ILspServiceFactory
2727
{
2828
public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind)
2929
{
@@ -35,58 +35,79 @@ private class RazorStartupService(
3535
#pragma warning disable CS0618 // Type or member is obsolete
3636
Lazy<ICohostStartupService>? cohostStartupService,
3737
#pragma warning restore CS0618 // Type or member is obsolete
38-
AbstractRazorCohostLifecycleService? razorCohostLifecycleService) : ILspService, IOnInitialized, IDisposable
38+
Lazy<AbstractRazorCohostLifecycleService>? razorCohostLifecycleService) : ILspService, IOnInitialized, IDisposable
3939
{
4040
private readonly CancellationTokenSource _disposalTokenSource = new();
41-
private IDisposable? _activation;
41+
private IDisposable? _cohostActivation;
42+
private IDisposable? _razorFilePresentActivation;
4243

4344
public void Dispose()
4445
{
45-
razorCohostLifecycleService?.Dispose();
46+
if (razorCohostLifecycleService is { IsValueCreated: true, Value: var service })
47+
{
48+
service.Dispose();
49+
}
4650

47-
_activation?.Dispose();
48-
_activation = null;
51+
_razorFilePresentActivation?.Dispose();
52+
_razorFilePresentActivation = null;
53+
_cohostActivation?.Dispose();
54+
_cohostActivation = null;
4955
_disposalTokenSource.Cancel();
5056
}
5157

52-
public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken)
58+
public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken)
5359
{
5460
if (context.ServerKind is not (WellKnownLspServerKinds.AlwaysActiveVSLspServer or WellKnownLspServerKinds.CSharpVisualBasicLspServer))
5561
{
5662
// We have to register this class for Any server, but only want to run in the C# server in VS or VS Code
57-
return;
63+
return Task.CompletedTask;
5864
}
5965

6066
if (cohostStartupService is null && razorCohostLifecycleService is null)
6167
{
62-
return;
63-
}
64-
65-
if (razorCohostLifecycleService is not null)
66-
{
67-
// If we have a cohost lifecycle service, fire pre-initialization, which happens when the LSP server starts up, but before
68-
// the UIContext is activated.
69-
await razorCohostLifecycleService.LspServerIntializedAsync(cancellationToken).ConfigureAwait(false);
68+
return Task.CompletedTask;
7069
}
7170

7271
if (uIContextActivationService is null)
7372
{
74-
// Outside of VS, we want to initialize immediately.. I think?
73+
PreinitializeRazor();
7574
InitializeRazor();
7675
}
7776
else
7877
{
79-
_activation = uIContextActivationService.ExecuteWhenActivated(Constants.RazorCohostingUIContext, InitializeRazor);
78+
// There are two initialization methods for Razor, which looks odd here, but are really controlled by UI contexts.
79+
// This method fires for any Roslyn project, but not all Roslyn projects are Razor projects, so the first UI context
80+
// triggers where there is a project with a Razor capability present in the solution, and the next is when a Razor file
81+
// is opened in the editor. ie these two lines look the same, but really they do different levels of initialization.
82+
_razorFilePresentActivation = uIContextActivationService.ExecuteWhenActivated(Constants.RazorCapabilityPresentUIContext, PreinitializeRazor);
83+
_cohostActivation = uIContextActivationService.ExecuteWhenActivated(Constants.RazorCohostingUIContext, InitializeRazor);
8084
}
8185

82-
return;
86+
return Task.CompletedTask;
87+
88+
void PreinitializeRazor()
89+
{
90+
this.PreinitializeRazorAsync(_disposalTokenSource.Token).ReportNonFatalErrorAsync();
91+
}
8392

8493
void InitializeRazor()
8594
{
8695
this.InitializeRazorAsync(clientCapabilities, context, _disposalTokenSource.Token).ReportNonFatalErrorAsync();
8796
}
8897
}
8998

99+
private async Task PreinitializeRazorAsync(CancellationToken cancellationToken)
100+
{
101+
if (cancellationToken.IsCancellationRequested) return;
102+
103+
await TaskScheduler.Default.SwitchTo(alwaysYield: true);
104+
105+
if (razorCohostLifecycleService is not null)
106+
{
107+
await razorCohostLifecycleService.Value.LspServerIntializedAsync(cancellationToken).ConfigureAwait(false);
108+
}
109+
}
110+
90111
private async Task InitializeRazorAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken)
91112
{
92113
// The LSP server will dispose us when the server exits, but VS could decide to activate us later.
@@ -103,7 +124,7 @@ private async Task InitializeRazorAsync(ClientCapabilities clientCapabilities, R
103124
if (razorCohostLifecycleService is not null)
104125
{
105126
// If we have a cohost lifecycle service, fire post-initialization, which happens when the UIContext is activated.
106-
await razorCohostLifecycleService.RazorActivatedAsync(clientCapabilities, requestContext, cancellationToken).ConfigureAwait(false);
127+
await razorCohostLifecycleService.Value.RazorActivatedAsync(clientCapabilities, requestContext, cancellationToken).ConfigureAwait(false);
107128
}
108129

109130
if (cohostStartupService is not null)

0 commit comments

Comments
 (0)