Skip to content

Commit 6fa1cb2

Browse files
authored
Merge pull request #5834 from dibarbet/fix_dispose
Fix issue with Razor commands being registered twice
2 parents 8e4471d + 4c7a38f commit 6fa1cb2

File tree

3 files changed

+135
-73
lines changed

3 files changed

+135
-73
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node";
7+
import CompositeDisposable from "../CompositeDisposable";
8+
import { IDisposable } from "../Disposable";
9+
10+
/**
11+
* Implementation of the base LanguageClient type that allows for additional items to be disposed of
12+
* when the base LanguageClient instance is disposed.
13+
*/
14+
export class RoslynLanguageClient extends LanguageClient {
15+
16+
private readonly _disposables: CompositeDisposable;
17+
18+
constructor(
19+
id: string,
20+
name: string,
21+
serverOptions: ServerOptions,
22+
clientOptions: LanguageClientOptions,
23+
forceDebug?: boolean) {
24+
super(id, name, serverOptions, clientOptions, forceDebug);
25+
26+
this._disposables = new CompositeDisposable();
27+
}
28+
29+
override async dispose(timeout?: number | undefined): Promise<void> {
30+
this._disposables.dispose();
31+
return super.dispose(timeout);
32+
}
33+
34+
/**
35+
* Adds a disposable that should be disposed of when the LanguageClient instance gets disposed.
36+
*/
37+
public addDisposable(disposable: IDisposable) {
38+
this._disposables.add(disposable);
39+
}
40+
}

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 94 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { UriConverter } from './uriConverter';
1414
import {
1515
DidChangeTextDocumentNotification,
1616
DidCloseTextDocumentNotification,
17-
LanguageClient,
1817
LanguageClientOptions,
1918
ServerOptions,
2019
DidCloseTextDocumentParams,
@@ -54,15 +53,14 @@ import { Options } from '../shared/options';
5453
import { ServerStateChange } from './ServerStateChange';
5554
import TelemetryReporter from '@vscode/extension-telemetry';
5655
import CSharpIntelliCodeExports from '../CSharpIntelliCodeExports';
57-
import { csharpDevkitExtensionId, getCSharpDevKit } from '../utils/getCSharpDevKit';
56+
import { csharpDevkitExtensionId, csharpDevkitIntelliCodeExtensionId, getCSharpDevKit } from '../utils/getCSharpDevKit';
5857
import { randomUUID } from 'crypto';
58+
import { RoslynLanguageClient } from './roslynLanguageClient';
5959

6060
let _languageServer: RoslynLanguageServer;
6161
let _channel: vscode.OutputChannel;
6262
let _traceChannel: vscode.OutputChannel;
6363

64-
const csharpDevkitIntelliCodeExtensionId = "ms-dotnettools.vscodeintellicode-csharp";
65-
6664
export class RoslynLanguageServer {
6765

6866
// These are commands that are invoked by the Razor extension, and are used to send LSP requests to the Roslyn LSP server
@@ -89,7 +87,7 @@ export class RoslynLanguageServer {
8987
* The timeout for stopping the language server (in ms).
9088
*/
9189
private static _stopTimeout: number = 10000;
92-
private _languageClient: LanguageClient | undefined;
90+
private _languageClient: RoslynLanguageClient | undefined;
9391

9492
/**
9593
* Flag indicating if C# Devkit was installed the last time we activated.
@@ -117,38 +115,7 @@ export class RoslynLanguageServer {
117115
private optionProvider: OptionProvider,
118116
private context: vscode.ExtensionContext,
119117
private telemetryReporter: TelemetryReporter
120-
) {
121-
// subscribe to extension change events so that we can get notified if C# Dev Kit is added/removed later.
122-
this.context.subscriptions.push(vscode.extensions.onDidChange(async () => {
123-
let csharpDevkitExtension = getCSharpDevKit();
124-
125-
if (this._wasActivatedWithCSharpDevkit === undefined) {
126-
// Haven't activated yet.
127-
return;
128-
}
129-
130-
const title = 'Restart Language Server';
131-
const command = 'dotnet.restartServer';
132-
if (csharpDevkitExtension && !this._wasActivatedWithCSharpDevkit) {
133-
// We previously started without C# Dev Kit and its now installed.
134-
// Offer a prompt to restart the server to use C# Dev Kit.
135-
_channel.appendLine(`Detected new installation of ${csharpDevkitExtensionId}`);
136-
let message = `Detected installation of ${csharpDevkitExtensionId}. Would you like to relaunch the language server for added features?`;
137-
ShowInformationMessage(vscode, message, { title, command });
138-
} else {
139-
// Any other change to extensions is irrelevant - an uninstall requires a reload of the window
140-
// which will automatically restart this extension too.
141-
}
142-
}));
143-
144-
// Subscribe to telemetry events so we can enable/disable as needed
145-
this.context.subscriptions.push(vscode.env.onDidChangeTelemetryEnabled((isEnabled: boolean) => {
146-
const title = 'Restart Language Server';
147-
const command = 'dotnet.restartServer';
148-
const message = 'Detected change in telemetry settings. These will not take effect until the language server is restarted, would you like to restart?';
149-
ShowInformationMessage(vscode, message, { title, command });
150-
}));
151-
}
118+
) { }
152119

153120
/**
154121
* Resolves server options and starts the dotnet language server process. The process is started asynchronously and this method will not wait until
@@ -192,7 +159,7 @@ export class RoslynLanguageServer {
192159
};
193160

194161
// Create the language client and start the client.
195-
let client = new LanguageClient(
162+
let client = new RoslynLanguageClient(
196163
'microsoft-codeanalysis-languageserver',
197164
'Microsoft.CodeAnalysis.LanguageServer',
198165
serverOptions,
@@ -223,11 +190,14 @@ export class RoslynLanguageServer {
223190
this._eventBus.emit(RoslynLanguageServer.serverStateChangeEvent, ServerStateChange.ProjectInitializationComplete);
224191
});
225192

193+
this.registerExtensionsChanged(this._languageClient);
194+
this.registerTelemetryChanged(this._languageClient);
195+
226196
// Start the client. This will also launch the server
227197
this._languageClient.start();
228198

229199
// Register Razor dynamic file info handling
230-
this.registerRazor(this._languageClient);
200+
this.registerDynamicFileInfo(this._languageClient);
231201
}
232202

233203
public async stop(): Promise<void> {
@@ -286,6 +256,18 @@ export class RoslynLanguageServer {
286256
return response;
287257
}
288258

259+
/**
260+
* Sends an LSP notification to the server with a given method and parameters.
261+
*/
262+
public async sendNotification<Params>(method: string, params: Params): Promise<any> {
263+
if (!this.isRunning()) {
264+
throw new Error('Tried to send request while server is not started.');
265+
}
266+
267+
let response = await this._languageClient!.sendNotification(method, params);
268+
return response;
269+
}
270+
289271
public async registerSolutionSnapshot(token: vscode.CancellationToken) : Promise<SolutionSnapshotId> {
290272
let response = await _languageServer.sendRequest0(RegisterSolutionSnapshotRequest.type, token);
291273
if (response)
@@ -460,7 +442,7 @@ export class RoslynLanguageServer {
460442
return childProcess;
461443
}
462444

463-
private registerRazor(client: LanguageClient) {
445+
private registerDynamicFileInfo(client: RoslynLanguageClient) {
464446
// When the Roslyn language server sends a request for Razor dynamic file info, we forward that request along to Razor via
465447
// a command.
466448
client.onRequest(
@@ -469,43 +451,41 @@ export class RoslynLanguageServer {
469451
client.onNotification(
470452
RoslynLanguageServer.removeRazorDynamicFileInfoMethodName,
471453
async notification => vscode.commands.executeCommand(DynamicFileInfoHandler.removeDynamicFileInfoCommand, notification));
454+
}
472455

473-
// Razor will call into us (via command) for generated file didChange/didClose notifications. We'll then forward these
474-
// notifications along to Roslyn. didOpen notifications are handled separately via the vscode.openTextDocument method.
475-
vscode.commands.registerCommand(RoslynLanguageServer.roslynDidChangeCommand, (notification: DidChangeTextDocumentParams) => {
476-
client.sendNotification(DidChangeTextDocumentNotification.method, notification);
477-
});
478-
vscode.commands.registerCommand(RoslynLanguageServer.roslynDidCloseCommand, (notification: DidCloseTextDocumentParams) => {
479-
client.sendNotification(DidCloseTextDocumentNotification.method, notification);
480-
});
481-
vscode.commands.registerCommand(RoslynLanguageServer.roslynPullDiagnosticCommand, async (request: DocumentDiagnosticParams) => {
482-
let diagnosticRequestType = new RequestType<DocumentDiagnosticParams, DocumentDiagnosticReport, any>(DocumentDiagnosticRequest.method);
483-
return await this.sendRequest(diagnosticRequestType, request, CancellationToken.None);
484-
});
456+
private registerExtensionsChanged(languageClient: RoslynLanguageClient) {
457+
// subscribe to extension change events so that we can get notified if C# Dev Kit is added/removed later.
458+
languageClient.addDisposable(vscode.extensions.onDidChange(async () => {
459+
let csharpDevkitExtension = getCSharpDevKit();
485460

486-
// The VS Code API for code actions (and the vscode.CodeAction type) doesn't support everything that LSP supports,
487-
// namely the data property, which Razor needs to identify which code actions are on their allow list, so we need
488-
// to expose a command for them to directly invoke our code actions LSP endpoints, rather than use built-in commands.
489-
vscode.commands.registerCommand(RoslynLanguageServer.provideCodeActionsCommand, async (request: CodeActionParams) => {
490-
return await this.sendRequest(CodeActionRequest.type, request, CancellationToken.None);
491-
});
492-
vscode.commands.registerCommand(RoslynLanguageServer.resolveCodeActionCommand, async (request: CodeAction) => {
493-
return await this.sendRequest(CodeActionResolveRequest.type, request, CancellationToken.None);
494-
});
461+
if (this._wasActivatedWithCSharpDevkit === undefined) {
462+
// Haven't activated yet.
463+
return;
464+
}
495465

496-
vscode.commands.registerCommand(RoslynLanguageServer.provideCompletionsCommand, async (request: CompletionParams) => {
497-
return await this.sendRequest(CompletionRequest.type, request, CancellationToken.None);
498-
});
499-
vscode.commands.registerCommand(RoslynLanguageServer.resolveCompletionsCommand, async (request: CompletionItem) => {
500-
return await this.sendRequest(CompletionResolveRequest.type, request, CancellationToken.None);
501-
});
466+
const title = 'Restart Language Server';
467+
const command = 'dotnet.restartServer';
468+
if (csharpDevkitExtension && !this._wasActivatedWithCSharpDevkit) {
469+
// We previously started without C# Dev Kit and its now installed.
470+
// Offer a prompt to restart the server to use C# Dev Kit.
471+
_channel.appendLine(`Detected new installation of ${csharpDevkitExtensionId}`);
472+
let message = `Detected installation of ${csharpDevkitExtensionId}. Would you like to relaunch the language server for added features?`;
473+
ShowInformationMessage(vscode, message, { title, command });
474+
} else {
475+
// Any other change to extensions is irrelevant - an uninstall requires a reload of the window
476+
// which will automatically restart this extension too.
477+
}
478+
}));
479+
}
502480

503-
// Roslyn is responsible for producing a json file containing information for Razor, that comes from the compilation for
504-
// a project. We want to defer this work until necessary, so this command is called by the Razor document manager to tell
505-
// us when they need us to initialize the Razor things.
506-
vscode.commands.registerCommand(RoslynLanguageServer.razorInitializeCommand, () => {
507-
client.sendNotification("razor/initialize", { });
508-
});
481+
private registerTelemetryChanged(languageClient: RoslynLanguageClient) {
482+
// Subscribe to telemetry events so we can enable/disable as needed
483+
languageClient.addDisposable(vscode.env.onDidChangeTelemetryEnabled((isEnabled: boolean) => {
484+
const title = 'Restart Language Server';
485+
const command = 'dotnet.restartServer';
486+
const message = 'Detected change in telemetry settings. These will not take effect until the language server is restarted, would you like to restart?';
487+
ShowInformationMessage(vscode, message, { title, command });
488+
}));
509489
}
510490

511491
private getServerFileName() {
@@ -603,6 +583,8 @@ export async function activateRoslynLanguageServer(context: vscode.ExtensionCont
603583
// Register any commands that need to be handled by the extension.
604584
registerCommands(context, _languageServer);
605585

586+
registerRazorCommands(context, _languageServer);
587+
606588
// Register any needed debugger components that need to communicate with the language server.
607589
registerDebugger(context, _languageServer, platformInfo, optionProvider, _channel);
608590

@@ -641,6 +623,45 @@ export async function activateRoslynLanguageServer(context: vscode.ExtensionCont
641623
_languageServer.start();
642624
}
643625

626+
function registerRazorCommands(context: vscode.ExtensionContext, languageServer: RoslynLanguageServer) {
627+
// Razor will call into us (via command) for generated file didChange/didClose notifications. We'll then forward these
628+
// notifications along to Roslyn. didOpen notifications are handled separately via the vscode.openTextDocument method.
629+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.roslynDidChangeCommand, async (notification: DidChangeTextDocumentParams) => {
630+
await languageServer.sendNotification(DidChangeTextDocumentNotification.method, notification);
631+
}));
632+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.roslynDidCloseCommand, async (notification: DidCloseTextDocumentParams) => {
633+
await languageServer.sendNotification(DidCloseTextDocumentNotification.method, notification);
634+
}));
635+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.roslynPullDiagnosticCommand, async (request: DocumentDiagnosticParams) => {
636+
let diagnosticRequestType = new RequestType<DocumentDiagnosticParams, DocumentDiagnosticReport, any>(DocumentDiagnosticRequest.method);
637+
return await languageServer.sendRequest(diagnosticRequestType, request, CancellationToken.None);
638+
}));
639+
640+
// The VS Code API for code actions (and the vscode.CodeAction type) doesn't support everything that LSP supports,
641+
// namely the data property, which Razor needs to identify which code actions are on their allow list, so we need
642+
// to expose a command for them to directly invoke our code actions LSP endpoints, rather than use built-in commands.
643+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.provideCodeActionsCommand, async (request: CodeActionParams) => {
644+
return await languageServer.sendRequest(CodeActionRequest.type, request, CancellationToken.None);
645+
}));
646+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.resolveCodeActionCommand, async (request: CodeAction) => {
647+
return await languageServer.sendRequest(CodeActionResolveRequest.type, request, CancellationToken.None);
648+
}));
649+
650+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.provideCompletionsCommand, async (request: CompletionParams) => {
651+
return await languageServer.sendRequest(CompletionRequest.type, request, CancellationToken.None);
652+
}));
653+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.resolveCompletionsCommand, async (request: CompletionItem) => {
654+
return await languageServer.sendRequest(CompletionResolveRequest.type, request, CancellationToken.None);
655+
}));
656+
657+
// Roslyn is responsible for producing a json file containing information for Razor, that comes from the compilation for
658+
// a project. We want to defer this work until necessary, so this command is called by the Razor document manager to tell
659+
// us when they need us to initialize the Razor things.
660+
context.subscriptions.push(vscode.commands.registerCommand(RoslynLanguageServer.razorInitializeCommand, async () => {
661+
await languageServer.sendNotification("razor/initialize", { });
662+
}));
663+
}
664+
644665
async function applyAutoInsertEdit(e: vscode.TextDocumentChangeEvent, token: vscode.CancellationToken) {
645666
const change = e.contentChanges[0];
646667

src/utils/getCSharpDevKit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
77
import { CSharpDevKitExports } from "../CSharpDevKitExports";
88

99
export const csharpDevkitExtensionId = "ms-dotnettools.csdevkit";
10+
export const csharpDevkitIntelliCodeExtensionId = "ms-dotnettools.vscodeintellicode-csharp";
1011

1112
export function getCSharpDevKit(): vscode.Extension<CSharpDevKitExports> | undefined {
1213
return vscode.extensions.getExtension<CSharpDevKitExports>(csharpDevkitExtensionId);

0 commit comments

Comments
 (0)