Skip to content

Commit cd32d3e

Browse files
authored
Drop textDocument/didClose if dropping textDocument/didOpen (#1682)
* Drop textDocument/didClose if dropping textDocument/didOpen When using delayOpenNotifications=true, the open notifications are queued and dropped if a close notification immediately follows. However, the original implementation still sent the close notification. This does not match the spec: > A close notification requires a previous open notification to be sent This change also drops the close notification if the open notification was dropped. Fixes #1681 * Fix comment
1 parent 7e14d62 commit cd32d3e

File tree

4 files changed

+151
-3
lines changed

4 files changed

+151
-3
lines changed

client-node-tests/src/integration.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ namespace SetDiagnosticsNotification {
2727
export const type = new lsclient.NotificationType<proto.DocumentDiagnosticReport>(method);
2828
}
2929

30+
/**
31+
* A custom request to get a list of all text sync notifications that the server
32+
* has been sent.
33+
*
34+
* Implemented in textSyncServer.ts
35+
*/
36+
namespace GetNotificationsRequest {
37+
export const method: 'testing/getNotifications' = 'testing/getNotifications';
38+
export const type = new lsclient.RequestType0<string[], void>(method);
39+
}
40+
3041
async function revertAllDirty(): Promise<void> {
3142
return vscode.commands.executeCommand('_workbench.revertAllDirty');
3243
}
@@ -2227,3 +2238,73 @@ suite('Server activation', () => {
22272238
await client.stop();
22282239
});
22292240
});
2241+
2242+
suite('delayOpenNotifications', () => {
2243+
let client: lsclient.LanguageClient;
2244+
2245+
async function startClient(delayOpen: boolean): Promise<void> {
2246+
const serverModule = path.join(__dirname, './servers/textSyncServer.js');
2247+
const serverOptions: lsclient.ServerOptions = {
2248+
run: { module: serverModule, transport: lsclient.TransportKind.ipc },
2249+
debug: { module: serverModule, transport: lsclient.TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } }
2250+
};
2251+
2252+
const clientOptions: lsclient.LanguageClientOptions = {
2253+
documentSelector: [{ language: 'plaintext' }],
2254+
synchronize: {},
2255+
initializationOptions: {},
2256+
middleware: {},
2257+
textSynchronization: {
2258+
delayOpenNotifications: delayOpen
2259+
}
2260+
};
2261+
(clientOptions as ({ $testMode?: boolean })).$testMode = true;
2262+
2263+
client = new lsclient.LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions);
2264+
await client.start();
2265+
}
2266+
2267+
const fakeDocument = {
2268+
uri: 'untitled:test.txt',
2269+
languageId: 'plaintext',
2270+
getText: () => '',
2271+
} as any as vscode.TextDocument;
2272+
2273+
function sendDidOpen(document: vscode.TextDocument) {
2274+
return client.getFeature(lsclient.DidOpenTextDocumentNotification.method).getProvider(document)!.send(document);
2275+
}
2276+
2277+
function sendDidClose(document: vscode.TextDocument) {
2278+
return client.getFeature(lsclient.DidCloseTextDocumentNotification.method).getProvider(document)!.send(document);
2279+
}
2280+
2281+
teardown(async () => client.stop());
2282+
2283+
test('didOpen/didClose are not sent when delayOpenNotifications=true', async () => {
2284+
await startClient(true);
2285+
2286+
await sendDidOpen(fakeDocument);
2287+
await sendDidClose(fakeDocument);
2288+
2289+
// Ensure no notifications.
2290+
const notifications = await client.sendRequest(GetNotificationsRequest.type);
2291+
assert.deepStrictEqual(notifications, []);
2292+
});
2293+
2294+
test('didOpen/didClose are always sent when delayOpenNotifications=false', async () => {
2295+
await startClient(false);
2296+
2297+
await sendDidOpen(fakeDocument);
2298+
await sendDidClose(fakeDocument);
2299+
2300+
// Ensure both notifications.
2301+
const notifications = await client.sendRequest(GetNotificationsRequest.type);
2302+
assert.deepStrictEqual(
2303+
notifications,
2304+
[
2305+
'textDocument/didOpen',
2306+
'textDocument/didClose',
2307+
],
2308+
);
2309+
});
2310+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 {
7+
createConnection, Connection, InitializeParams, InitializeResult,
8+
TextDocumentSyncKind, RequestType0
9+
} from 'vscode-languageserver/node';
10+
11+
const connection: Connection = createConnection();
12+
13+
const receivedNotifications: string[] = [];
14+
15+
/**
16+
* A custom request to get a list of all text sync notifications that the server
17+
* has been sent.
18+
*/
19+
namespace GetNotificationsRequest {
20+
export const method: 'testing/getNotifications' = 'testing/getNotifications';
21+
export const type = new RequestType0<string[], void>(method);
22+
}
23+
24+
connection.onInitialize((_params: InitializeParams): InitializeResult => {
25+
return {
26+
capabilities: {
27+
textDocumentSync: {
28+
openClose: true,
29+
change: TextDocumentSyncKind.Incremental
30+
}
31+
}
32+
};
33+
});
34+
35+
connection.onDidOpenTextDocument(() => {
36+
receivedNotifications.push('textDocument/didOpen');
37+
});
38+
39+
connection.onDidChangeTextDocument(() => {
40+
receivedNotifications.push('textDocument/didChange');
41+
});
42+
43+
connection.onDidCloseTextDocument(() => {
44+
receivedNotifications.push('textDocument/didClose');
45+
});
46+
47+
connection.onRequest(GetNotificationsRequest.type, () => {
48+
return receivedNotifications;
49+
});
50+
51+
connection.listen();

client/src/common/client.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,8 +1004,13 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
10041004
// Ensure we have a connection before we force the document sync.
10051005
const connection = await this.$start();
10061006

1007-
// Send ony depending open notifications
1008-
await this._didOpenTextDocumentFeature!.sendPendingOpenNotifications(documentToClose);
1007+
// Send any depending open notifications
1008+
const didDropOpenNotification = await this._didOpenTextDocumentFeature!.sendPendingOpenNotifications(documentToClose);
1009+
if (didDropOpenNotification) {
1010+
// Don't forward this close notification if we dropped the
1011+
// corresponding open notification.
1012+
return;
1013+
}
10091014

10101015
// If any document is synced in full mode make sure we flush any pending
10111016
// full document syncs.

client/src/common/textSynchronization.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,26 @@ export class DidOpenTextDocumentFeature extends TextDocumentEventFeature<DidOpen
153153
}
154154
}
155155

156-
public async sendPendingOpenNotifications(closingDocument?: string): Promise<void> {
156+
/**
157+
* Sends any pending open notifications unless they are for the document
158+
* being closed.
159+
*
160+
* @param closingDocument The document being closed.
161+
* @returns Whether a pending open notification was dropped because it was
162+
* for the closing document.
163+
*/
164+
public async sendPendingOpenNotifications(closingDocument?: string): Promise<boolean> {
157165
const notifications = Array.from(this._pendingOpenNotifications.values());
158166
this._pendingOpenNotifications.clear();
167+
let didDropOpenNotification = false;
159168
for (const notification of notifications) {
160169
if (closingDocument !== undefined && notification.uri.toString() === closingDocument) {
170+
didDropOpenNotification = true;
161171
continue;
162172
}
163173
await super.callback(notification);
164174
}
175+
return didDropOpenNotification;
165176
}
166177

167178
protected getTextDocument(data: TextDocument): TextDocument {

0 commit comments

Comments
 (0)