Skip to content

Commit 6b34e40

Browse files
committed
Ensure server is loaded and unloaded with the solution
1 parent 8aa0e2e commit 6b34e40

File tree

2 files changed

+35
-5
lines changed

2 files changed

+35
-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: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,25 @@ 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+
}
55+
else if (e.Kind is WorkspaceChangeKind.SolutionRemoved)
56+
{
57+
// VS will unload the language client when the solution is closed, but sometimes its a little slow to do so,
58+
// and we can end up trying to load it, above, before it has been asked to unload. We wait for the previous
59+
// instance to shutdown when we load, but we have to ensure that it at least gets signaled to do so first.
60+
Unload();
61+
}
4562
}
4663

4764
public void StopListening(Workspace workspace)
@@ -56,7 +73,6 @@ private void Load()
5673

5774
async Task LoadAsync()
5875
{
59-
6076
// Explicitly switch to the bg so that if this causes any expensive work (like mef loads) it
6177
// doesn't block the UI thread. Note, we always yield because sometimes our caller starts
6278
// on the threadpool thread but is indirectly blocked on by the UI thread.
@@ -71,6 +87,14 @@ await _languageClientBroker.Value.LoadAsync(new LanguageClientMetadata(
7187
}
7288
}
7389

90+
private void Unload()
91+
{
92+
// We just want to signal that an unload should happen, in case the above call to Load comes in quick.
93+
// We don't want to wait for it to complete, not do we care about errors that may occur during the unload.
94+
// The language client/server does its own error reporting as necessary.
95+
_languageClient.StopServerAsync().Forget();
96+
}
97+
7498
/// <summary>
7599
/// The <see cref="ILanguageClientBroker.LoadAsync(ILanguageClientMetadata, ILanguageClient)"/>
76100
/// requires that we pass the <see cref="ILanguageClientMetadata"/> along with the language client instance.

0 commit comments

Comments
 (0)