Skip to content

Commit d00532b

Browse files
authored
Ensure the LSP server is loaded after a solution is closed (#79048)
Needed for dotnet/razor#9519 Previously Roslyn would load its language server when the first solution is opened in VS so that workspace diagnostics and other such functionality works. When the solution is closed however, VS would shut down the server and the user has to manually open a `.cs` file to get it to re-load. This ensures it is reloaded on solution open. This is also needed to unblock cohosting, and cohosting integration tests as well, as without this, when a 2nd solution is opened in VS the user has to open a `.cs` file before they'll get any tooling in a `.razor` file, because Razor uses dynamic registration which can only happen after the LSP server has been loaded.
2 parents a8287cc + 5e3a302 commit d00532b

File tree

2 files changed

+24
-5
lines changed

2 files changed

+24
-5
lines changed

src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,16 @@ internal abstract partial class AbstractInProcLanguageClient(
110110

111111
public event AsyncEventHandler<EventArgs>? StartAsync;
112112

113+
public event AsyncEventHandler<EventArgs>? StopAsync;
114+
113115
/// <summary>
114-
/// Unused, implementing <see cref="ILanguageClient"/>
116+
/// Stops the server if it has been started.
115117
/// </summary>
116-
public event AsyncEventHandler<EventArgs>? StopAsync { add { } remove { } }
118+
/// <remarks>
119+
/// Per the documentation on <see cref="ILanguageClient.StopAsync"/>, the event is ignored if the server has not been started.
120+
/// </remarks>
121+
public Task StopServerAsync()
122+
=> StopAsync?.InvokeAsync(this, EventArgs.Empty) ?? Task.CompletedTask;
117123

118124
public async Task<Connection?> ActivateAsync(CancellationToken cancellationToken)
119125
{

src/EditorFeatures/Core/LanguageServer/AlwaysActiveLanguageClientEventListener.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,18 @@ internal sealed class AlwaysActiveLanguageClientEventListener(
4040
/// </summary>
4141
public void StartListening(Workspace workspace)
4242
{
43-
// Trigger a fire and forget request to the VS LSP client to load our ILanguageClient.
44-
Load();
43+
_ = workspace.RegisterWorkspaceChangedHandler(Workspace_WorkspaceChanged);
44+
}
45+
46+
private void Workspace_WorkspaceChanged(WorkspaceChangeEventArgs e)
47+
{
48+
if (e.Kind == WorkspaceChangeKind.SolutionAdded)
49+
{
50+
// Normally VS will load the language client when an editor window is created for one of our content types,
51+
// but we want to load it as soon as a solution is loaded so workspace diagnostics work, and so 3rd parties
52+
// like Razor can use dynamic registration.
53+
Load();
54+
}
4555
}
4656

4757
public void StopListening(Workspace workspace)
@@ -56,12 +66,15 @@ private void Load()
5666

5767
async Task LoadAsync()
5868
{
59-
6069
// Explicitly switch to the bg so that if this causes any expensive work (like mef loads) it
6170
// doesn't block the UI thread. Note, we always yield because sometimes our caller starts
6271
// on the threadpool thread but is indirectly blocked on by the UI thread.
6372
await TaskScheduler.Default.SwitchTo(alwaysYield: true);
6473

74+
// Sometimes the editor can be slow to stop the old server instance when the old solution is closed, so we force it here.
75+
// This will no-op if the server hasn't been started yet.
76+
await _languageClient.StopServerAsync().ConfigureAwait(false);
77+
6578
await _languageClientBroker.Value.LoadAsync(new LanguageClientMetadata(
6679
[
6780
ContentTypeNames.CSharpContentType,

0 commit comments

Comments
 (0)