Skip to content

Commit c7ae769

Browse files
committed
Cleanup and fix races in server activation and restart
1 parent b637e84 commit c7ae769

21 files changed

+601
-505
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
"omnisharp.autoStart": false,
2121
"editor.formatOnSave": false,
2222
"eslint.lintTask.enable": true,
23-
"dotnet.defaultSolution": "disable"
23+
"dotnet.defaultSolution": "disable",
24+
"jest.autoRun": "off"
2425
}

src/common.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,9 @@ export function findPowerShell(): string | undefined {
226226
}
227227
}
228228
}
229+
230+
export function isNotNull<T>(value: T): asserts value is NonNullable<T> {
231+
if (value === null || value === undefined) {
232+
throw new Error('value is null or undefined.');
233+
}
234+
}

src/csharpExtensionExports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { EventStream } from './eventStream';
99
import TestManager from './features/dotnetTest';
1010
import { GlobalBrokeredServiceContainer } from '@microsoft/servicehub-framework';
1111
import { RequestType } from 'vscode-languageclient/node';
12+
import { LanguageServerEvents } from './lsptoolshost/languageServerEvents';
1213

1314
export interface OmnisharpExtensionExports {
1415
initializationFinished: () => Promise<void>;
@@ -32,4 +33,5 @@ export interface CSharpExtensionExperimentalExports {
3233
params: Params,
3334
token: vscode.CancellationToken
3435
) => Promise<Response>;
36+
languageServerEvents: LanguageServerEvents;
3537
}

src/lsptoolshost/debugger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@ import OptionProvider from '../shared/observers/optionProvider';
1414
import { ServerStateChange } from './serverStateChange';
1515
import { DotnetConfigurationResolver } from '../shared/dotnetConfigurationProvider';
1616
import { getCSharpDevKit } from '../utils/getCSharpDevKit';
17+
import { RoslynLanguageServerEvents } from './languageServerEvents';
1718

1819
export function registerDebugger(
1920
context: vscode.ExtensionContext,
2021
languageServer: RoslynLanguageServer,
22+
languageServerEvents: RoslynLanguageServerEvents,
2123
platformInfo: PlatformInformation,
2224
optionProvider: OptionProvider,
2325
csharpOutputChannel: vscode.OutputChannel
2426
) {
2527
const workspaceInformationProvider: IWorkspaceDebugInformationProvider =
26-
new RoslynWorkspaceDebugInformationProvider(languageServer);
28+
new RoslynWorkspaceDebugInformationProvider(languageServer, csharpOutputChannel);
2729

28-
const disposable = languageServer.registerStateChangeEvent(async (state) => {
30+
const disposable = languageServerEvents.onServerStateChange(async (state) => {
2931
if (state === ServerStateChange.ProjectInitializationComplete) {
3032
const csharpDevkitExtension = getCSharpDevKit();
3133
if (!csharpDevkitExtension) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 * as vscode from 'vscode';
7+
import { ServerStateChange } from './serverStateChange';
8+
import { IDisposable } from '../disposable';
9+
10+
/**
11+
* Defines events that are fired by the language server.
12+
* These events can be consumed to wait for the server to reach a certain state.
13+
*/
14+
export interface LanguageServerEvents {
15+
readonly onServerStateChange: vscode.Event<ServerStateChange>;
16+
}
17+
18+
/**
19+
* Implementation that fires events when the language server reaches a certain state.
20+
* This is intentionally separate from the language server itself, so consumers can
21+
* register for events without having to know about the specific current state of the language server.
22+
*/
23+
export class RoslynLanguageServerEvents implements LanguageServerEvents, IDisposable {
24+
public readonly onServerStateChangeEmitter = new vscode.EventEmitter<ServerStateChange>();
25+
26+
public get onServerStateChange(): vscode.Event<ServerStateChange> {
27+
return this.onServerStateChangeEmitter.event;
28+
}
29+
30+
dispose(): void {
31+
this.onServerStateChangeEmitter.dispose();
32+
}
33+
}

src/lsptoolshost/onAutoInsert.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 * as vscode from 'vscode';
7+
import { UriConverter } from './uriConverter';
8+
9+
import { FormattingOptions, TextDocumentIdentifier } from 'vscode-languageclient/node';
10+
import OptionProvider from '../shared/observers/optionProvider';
11+
import * as RoslynProtocol from './roslynProtocol';
12+
import { RoslynLanguageServer } from './roslynLanguageServer';
13+
14+
export function registerOnAutoInsert(optionsProvider: OptionProvider, languageServer: RoslynLanguageServer) {
15+
let source = new vscode.CancellationTokenSource();
16+
vscode.workspace.onDidChangeTextDocument(async (e) => {
17+
const options = optionsProvider.GetLatestOptions();
18+
if (!options.languageServerOptions.documentSelector.includes(e.document.languageId)) {
19+
return;
20+
}
21+
22+
if (e.contentChanges.length > 1 || e.contentChanges.length === 0) {
23+
return;
24+
}
25+
26+
const change = e.contentChanges[0];
27+
28+
if (!change.range.isEmpty) {
29+
return;
30+
}
31+
32+
const capabilities = await languageServer.getServerCapabilities();
33+
34+
if (capabilities._vs_onAutoInsertProvider) {
35+
// Regular expression to match all whitespace characters except the newline character
36+
const changeTrimmed = change.text.replace(/[^\S\n]+/g, '');
37+
38+
if (!capabilities._vs_onAutoInsertProvider._vs_triggerCharacters.includes(changeTrimmed)) {
39+
return;
40+
}
41+
42+
source.cancel();
43+
source = new vscode.CancellationTokenSource();
44+
await applyAutoInsertEdit(e, changeTrimmed, languageServer, source.token);
45+
}
46+
});
47+
}
48+
49+
async function applyAutoInsertEdit(
50+
e: vscode.TextDocumentChangeEvent,
51+
changeTrimmed: string,
52+
languageServer: RoslynLanguageServer,
53+
token: vscode.CancellationToken
54+
) {
55+
const change = e.contentChanges[0];
56+
// The server expects the request position to represent the caret position in the text after the change has already been applied.
57+
// We need to calculate what that position would be after the change is applied and send that to the server.
58+
const position = new vscode.Position(
59+
change.range.start.line,
60+
change.range.start.character + (change.text.length - change.rangeLength)
61+
);
62+
const uri = UriConverter.serialize(e.document.uri);
63+
const textDocument = TextDocumentIdentifier.create(uri);
64+
const formattingOptions = getFormattingOptions();
65+
const request: RoslynProtocol.OnAutoInsertParams = {
66+
_vs_textDocument: textDocument,
67+
_vs_position: position,
68+
_vs_ch: changeTrimmed,
69+
_vs_options: formattingOptions,
70+
};
71+
72+
const response = await languageServer.sendRequest(RoslynProtocol.OnAutoInsertRequest.type, request, token);
73+
if (response) {
74+
const textEdit = response._vs_textEdit;
75+
const startPosition = new vscode.Position(textEdit.range.start.line, textEdit.range.start.character);
76+
const endPosition = new vscode.Position(textEdit.range.end.line, textEdit.range.end.character);
77+
const docComment = new vscode.SnippetString(textEdit.newText);
78+
const code: any = vscode;
79+
const textEdits = [new code.SnippetTextEdit(new vscode.Range(startPosition, endPosition), docComment)];
80+
const edit = new vscode.WorkspaceEdit();
81+
edit.set(e.document.uri, textEdits);
82+
83+
const applied = vscode.workspace.applyEdit(edit);
84+
if (!applied) {
85+
throw new Error('Tried to insert a comment but an error occurred.');
86+
}
87+
}
88+
}
89+
90+
function getFormattingOptions(): FormattingOptions {
91+
const editorConfig = vscode.workspace.getConfiguration('editor');
92+
const tabSize = editorConfig.get<number>('tabSize') ?? 4;
93+
const insertSpaces = editorConfig.get<boolean>('insertSpaces') ?? true;
94+
return FormattingOptions.create(tabSize, insertSpaces);
95+
}

src/lsptoolshost/razorCommands.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 { RoslynLanguageServer } from './roslynLanguageServer';
7+
import * as vscode from 'vscode';
8+
import {
9+
DidChangeTextDocumentNotification,
10+
DidCloseTextDocumentNotification,
11+
DidCloseTextDocumentParams,
12+
DidChangeTextDocumentParams,
13+
DocumentDiagnosticParams,
14+
RequestType,
15+
DocumentDiagnosticRequest,
16+
DocumentDiagnosticReport,
17+
CancellationToken,
18+
CodeAction,
19+
CodeActionParams,
20+
CodeActionRequest,
21+
CodeActionResolveRequest,
22+
CompletionParams,
23+
CompletionRequest,
24+
CompletionResolveRequest,
25+
CompletionItem,
26+
} from 'vscode-languageclient/node';
27+
import SerializableSimplifyMethodParams from '../razor/src/simplify/serializableSimplifyMethodParams';
28+
import { TextEdit } from 'vscode-html-languageservice';
29+
30+
// These are commands that are invoked by the Razor extension, and are used to send LSP requests to the Roslyn LSP server
31+
export const roslynDidChangeCommand = 'roslyn.changeRazorCSharp';
32+
export const roslynDidCloseCommand = 'roslyn.closeRazorCSharp';
33+
export const roslynPullDiagnosticCommand = 'roslyn.pullDiagnosticRazorCSharp';
34+
export const provideCodeActionsCommand = 'roslyn.provideCodeActions';
35+
export const resolveCodeActionCommand = 'roslyn.resolveCodeAction';
36+
export const provideCompletionsCommand = 'roslyn.provideCompletions';
37+
export const resolveCompletionsCommand = 'roslyn.resolveCompletion';
38+
export const roslynSimplifyMethodCommand = 'roslyn.simplifyMethod';
39+
export const razorInitializeCommand = 'razor.initialize';
40+
41+
export function registerRazorCommands(context: vscode.ExtensionContext, languageServer: RoslynLanguageServer) {
42+
// Razor will call into us (via command) for generated file didChange/didClose notifications. We'll then forward these
43+
// notifications along to Roslyn. didOpen notifications are handled separately via the vscode.openTextDocument method.
44+
context.subscriptions.push(
45+
vscode.commands.registerCommand(roslynDidChangeCommand, async (notification: DidChangeTextDocumentParams) => {
46+
await languageServer.sendNotification(DidChangeTextDocumentNotification.method, notification);
47+
})
48+
);
49+
context.subscriptions.push(
50+
vscode.commands.registerCommand(roslynDidCloseCommand, async (notification: DidCloseTextDocumentParams) => {
51+
await languageServer.sendNotification(DidCloseTextDocumentNotification.method, notification);
52+
})
53+
);
54+
context.subscriptions.push(
55+
vscode.commands.registerCommand(roslynPullDiagnosticCommand, async (request: DocumentDiagnosticParams) => {
56+
const diagnosticRequestType = new RequestType<DocumentDiagnosticParams, DocumentDiagnosticReport, any>(
57+
DocumentDiagnosticRequest.method
58+
);
59+
return await languageServer.sendRequest(diagnosticRequestType, request, CancellationToken.None);
60+
})
61+
);
62+
context.subscriptions.push(
63+
vscode.commands.registerCommand(
64+
roslynSimplifyMethodCommand,
65+
async (request: SerializableSimplifyMethodParams) => {
66+
const simplifyMethodRequestType = new RequestType<SerializableSimplifyMethodParams, TextEdit[], any>(
67+
'roslyn/simplifyMethod'
68+
);
69+
return await languageServer.sendRequest(simplifyMethodRequestType, request, CancellationToken.None);
70+
}
71+
)
72+
);
73+
74+
// The VS Code API for code actions (and the vscode.CodeAction type) doesn't support everything that LSP supports,
75+
// namely the data property, which Razor needs to identify which code actions are on their allow list, so we need
76+
// to expose a command for them to directly invoke our code actions LSP endpoints, rather than use built-in commands.
77+
context.subscriptions.push(
78+
vscode.commands.registerCommand(provideCodeActionsCommand, async (request: CodeActionParams) => {
79+
return await languageServer.sendRequest(CodeActionRequest.type, request, CancellationToken.None);
80+
})
81+
);
82+
context.subscriptions.push(
83+
vscode.commands.registerCommand(resolveCodeActionCommand, async (request: CodeAction) => {
84+
return await languageServer.sendRequest(CodeActionResolveRequest.type, request, CancellationToken.None);
85+
})
86+
);
87+
88+
context.subscriptions.push(
89+
vscode.commands.registerCommand(provideCompletionsCommand, async (request: CompletionParams) => {
90+
return await languageServer.sendRequest(CompletionRequest.type, request, CancellationToken.None);
91+
})
92+
);
93+
context.subscriptions.push(
94+
vscode.commands.registerCommand(resolveCompletionsCommand, async (request: CompletionItem) => {
95+
return await languageServer.sendRequest(CompletionResolveRequest.type, request, CancellationToken.None);
96+
})
97+
);
98+
99+
// Roslyn is responsible for producing a json file containing information for Razor, that comes from the compilation for
100+
// a project. We want to defer this work until necessary, so this command is called by the Razor document manager to tell
101+
// us when they need us to initialize the Razor things.
102+
context.subscriptions.push(
103+
vscode.commands.registerCommand(razorInitializeCommand, async () => {
104+
await languageServer.sendNotification('razor/initialize', {});
105+
})
106+
);
107+
}

0 commit comments

Comments
 (0)