From 4ab10520dfb3489faa98a9b53e27357ccfa35894 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Tue, 17 Nov 2020 16:20:30 +0000 Subject: [PATCH 1/7] Add support for middleware in onProgress Fixes #671. --- client-node-tests/src/integration.test.ts | 42 +++++++++++++++++++++ client-node-tests/src/servers/testServer.ts | 12 +++++- client/src/common/client.ts | 14 ++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index d794dd4d4..86c1cd376 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -332,6 +332,48 @@ suite('Client integration', () => { assert.ok(middlewareCalled); }); + test('Progress', async () => { + const progressToken = 'TEST-PROGRESS-TOKEN'; + const middlewareEvents: Array = []; + const handlerEvents: Array = []; + await new Promise((resolve) => { + // Set up middleware that captures progress events. + middleware.handleProgress = (type, token, params, next) => { + if (token === progressToken) { + middlewareEvents.push(type); + // TODO(dantup): Should be when we get end event! + if (params.kind === 'end') + setImmediate(resolve); + } + return next(type, token, params); + }; + + // Register a handler for the real progress events. + client.onProgress(lsclient.WorkDoneProgress.type, progressToken, (params) => { + handlerEvents.push(params); + }); + + // Send a request to the server that triggers sample events. + client.sendRequest( + new lsclient.ProtocolRequestType('testing/sendSampleProgress'), + {}, + tokenSource.token, + ); + }); + + middleware.handleProgress = undefined; + + // Ensure all events were handled. + assert.strictEqual( + middlewareEvents.map((p) => p.kind), + ['begin', 'update', 'end'], + ); + assert.strictEqual( + handlerEvents.map((p) => p.kind), + ['begin', 'update', 'end'], + ); + }); + test('Document Formatting', async () => { const provider = client.getFeature(lsclient.DocumentFormattingRequest.method).getProvider(document); isDefined(provider); diff --git a/client-node-tests/src/servers/testServer.ts b/client-node-tests/src/servers/testServer.ts index 30cbd180c..6b32662e0 100644 --- a/client-node-tests/src/servers/testServer.ts +++ b/client-node-tests/src/servers/testServer.ts @@ -8,7 +8,7 @@ import { createConnection, Connection, InitializeParams, ServerCapabilities, CompletionItemKind, ResourceOperationKind, FailureHandlingKind, DiagnosticTag, CompletionItemTag, TextDocumentSyncKind, MarkupKind, SignatureHelp, SignatureInformation, ParameterInformation, Location, Range, DocumentHighlight, DocumentHighlightKind, CodeAction, Command, TextEdit, Position, DocumentLink, - ColorInformation, Color, ColorPresentation, FoldingRange, SelectionRange, SymbolKind + ColorInformation, Color, ColorPresentation, FoldingRange, SelectionRange, SymbolKind, ProtocolRequestType, WorkDoneProgress } from '../../../server/node'; import { URI } from 'vscode-uri'; @@ -284,5 +284,15 @@ connection.languages.onOnTypeRename(() => { }; }); +connection.onRequest( + new ProtocolRequestType('testing/sendSampleProgress'), + (_, __) => { + const progressToken = 'TEST-PROGRESS-TOKEN'; + connection.sendProgress(WorkDoneProgress.type, progressToken, { kind: 'begin', title: 'Test Progress' }); + connection.sendProgress(WorkDoneProgress.type, progressToken, { kind: 'report', percentage: 50, message: 'Halfway!' }); + connection.sendProgress(WorkDoneProgress.type, progressToken, { kind: 'end', message: 'Completed!' }); + }, +); + // Listen on the connection connection.listen(); \ No newline at end of file diff --git a/client/src/common/client.ts b/client/src/common/client.ts index afaea8ce4..15b62acff 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -347,6 +347,10 @@ export interface HandleDiagnosticsSignature { (this: void, uri: Uri, diagnostics: VDiagnostic[]): void; } +export interface HandleProgressSignature

{ + (this: void, type: ProgressType

, token: ProgressToken, params: P): void; +} + export interface ProvideCompletionItemsSignature { (this: void, document: TextDocument, position: VPosition, context: VCompletionContext, token: CancellationToken): ProviderResult; } @@ -469,6 +473,7 @@ export interface _Middleware { didClose?: NextSignature; handleDiagnostics?: (this: void, uri: Uri, diagnostics: VDiagnostic[], next: HandleDiagnosticsSignature) => void; + handleProgress?:

(this: void, type: ProgressType

, token: ProgressToken, params: P, next: HandleProgressSignature

) => void; provideCompletionItem?: (this: void, document: TextDocument, position: VPosition, context: VCompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => ProviderResult; resolveCompletionItem?: (this: void, item: VCompletionItem, token: CancellationToken, next: ResolveCompletionItemSignature) => ProviderResult; provideHover?: (this: void, document: TextDocument, position: VPosition, token: CancellationToken, next: ProvideHoverSignature) => ProviderResult; @@ -2812,7 +2817,14 @@ export abstract class BaseLanguageClient { throw new Error('Language client is not ready yet'); } try { - return this._resolvedConnection!.onProgress(type, token, handler); + const handleProgress = this._clientOptions.middleware!.handleProgress; + if (handleProgress !== undefined) { + return this._resolvedConnection!.onProgress(type, token, (params: P) => { + handleProgress(type, token, params, () => handler(params)); + }); + } else { + return this._resolvedConnection!.onProgress(type, token, handler); + } } catch (error) { this.error(`Registering progress handler for token ${token} failed.`, error); throw error; From 990aeb8bb5cb890c471a1b0eaeb5be25f30669c0 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Wed, 18 Nov 2020 18:15:29 +0000 Subject: [PATCH 2/7] Fix compile error + tests --- client-node-tests/src/integration.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index 86c1cd376..c23b6a573 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -338,15 +338,15 @@ suite('Client integration', () => { const handlerEvents: Array = []; await new Promise((resolve) => { // Set up middleware that captures progress events. - middleware.handleProgress = (type, token, params, next) => { + middleware.handleProgress = ((type: typeof lsclient.WorkDoneProgress.type, token: lsclient.ProgressToken, params: lsclient.WorkDoneProgressBegin | lsclient.WorkDoneProgressReport | lsclient.WorkDoneProgressEnd, next: lsclient.HandleProgressSignature) => { if (token === progressToken) { - middlewareEvents.push(type); + middlewareEvents.push(params); // TODO(dantup): Should be when we get end event! if (params.kind === 'end') setImmediate(resolve); } return next(type, token, params); - }; + }) as any; // Register a handler for the real progress events. client.onProgress(lsclient.WorkDoneProgress.type, progressToken, (params) => { @@ -364,13 +364,13 @@ suite('Client integration', () => { middleware.handleProgress = undefined; // Ensure all events were handled. - assert.strictEqual( + assert.deepStrictEqual( middlewareEvents.map((p) => p.kind), - ['begin', 'update', 'end'], + ['begin', 'report', 'end'], ); - assert.strictEqual( + assert.deepStrictEqual( handlerEvents.map((p) => p.kind), - ['begin', 'update', 'end'], + ['begin', 'report', 'end'], ); }); From 8e0ea085ccb7413a940f9bd503e8957fd0345565 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Wed, 18 Nov 2020 18:19:28 +0000 Subject: [PATCH 3/7] Remove out-of-date comment/TODO --- client-node-tests/src/integration.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index c23b6a573..d787a9454 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -6,8 +6,8 @@ import * as assert from 'assert'; import * as path from 'path'; -import * as lsclient from 'vscode-languageclient/node'; import * as vscode from 'vscode'; +import * as lsclient from 'vscode-languageclient/node'; suite('Client integration', () => { @@ -271,7 +271,7 @@ suite('Client integration', () => { isArray(result, vscode.Location, 2); for (let i = 0; i < result.length; i++) { const location = result[i]; - rangeEqual(location.range, i, i, i ,i); + rangeEqual(location.range, i, i, i, i); assert.strictEqual(location.uri.toString(), document.uri.toString()); } @@ -341,7 +341,6 @@ suite('Client integration', () => { middleware.handleProgress = ((type: typeof lsclient.WorkDoneProgress.type, token: lsclient.ProgressToken, params: lsclient.WorkDoneProgressBegin | lsclient.WorkDoneProgressReport | lsclient.WorkDoneProgressEnd, next: lsclient.HandleProgressSignature) => { if (token === progressToken) { middlewareEvents.push(params); - // TODO(dantup): Should be when we get end event! if (params.kind === 'end') setImmediate(resolve); } @@ -513,7 +512,7 @@ suite('Client integration', () => { await provider.provideDocumentColors(document, tokenSource.token); middleware.provideDocumentColors = undefined; - const presentations = await provider.provideColorPresentations(color.color, { document, range}, tokenSource.token); + const presentations = await provider.provideColorPresentations(color.color, { document, range }, tokenSource.token); isArray(presentations, vscode.ColorPresentation); const presentation = presentations[0]; @@ -523,7 +522,7 @@ suite('Client integration', () => { middlewareCalled++; return n(c, x, t); }; - await provider.provideColorPresentations(color.color, { document, range}, tokenSource.token); + await provider.provideColorPresentations(color.color, { document, range }, tokenSource.token); middleware.provideColorPresentations = undefined; assert.strictEqual(middlewareCalled, 2); }); @@ -604,7 +603,7 @@ suite('Client integration', () => { assert.strictEqual(middlewareCalled, true); }); - test('Type Definition', async() => { + test('Type Definition', async () => { const provider = client.getFeature(lsclient.TypeDefinitionRequest.method).getProvider(document); isDefined(provider); const result = (await provider.provideTypeDefinition(document, position, tokenSource.token)) as vscode.Location; @@ -719,4 +718,4 @@ suite('Client integration', () => { middleware.provideTypeDefinition = undefined; assert.strictEqual(middlewareCalled, true); }); -}); \ No newline at end of file +}); From 33f3375addda6d5b2434bf25b1e74331c6f08937 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Thu, 19 Nov 2020 15:30:29 +0000 Subject: [PATCH 4/7] Add more explicitly typing of handleWorkDoneProgress middleware Still has one remaining type error. --- client-node-tests/src/integration.test.ts | 8 ++++---- client/src/common/client.ts | 24 ++++++++++++----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index d787a9454..8a2283718 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -338,14 +338,14 @@ suite('Client integration', () => { const handlerEvents: Array = []; await new Promise((resolve) => { // Set up middleware that captures progress events. - middleware.handleProgress = ((type: typeof lsclient.WorkDoneProgress.type, token: lsclient.ProgressToken, params: lsclient.WorkDoneProgressBegin | lsclient.WorkDoneProgressReport | lsclient.WorkDoneProgressEnd, next: lsclient.HandleProgressSignature) => { + middleware.handleWorkDoneProgress = (token: lsclient.ProgressToken, params, next) => { if (token === progressToken) { middlewareEvents.push(params); if (params.kind === 'end') setImmediate(resolve); } - return next(type, token, params); - }) as any; + return next(token, params); + }; // Register a handler for the real progress events. client.onProgress(lsclient.WorkDoneProgress.type, progressToken, (params) => { @@ -360,7 +360,7 @@ suite('Client integration', () => { ); }); - middleware.handleProgress = undefined; + middleware.handleWorkDoneProgress = undefined; // Ensure all events were handled. assert.deepStrictEqual( diff --git a/client/src/common/client.ts b/client/src/common/client.ts index 15b62acff..92893103c 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -49,7 +49,7 @@ import { DocumentRangeFormattingOptions, DocumentOnTypeFormattingOptions, RenameOptions, DocumentLinkOptions, CompletionItemTag, DiagnosticTag, DocumentColorRequest, DeclarationRequest, FoldingRangeRequest, ImplementationRequest, SelectionRangeRequest, TypeDefinitionRequest, SymbolTag, CallHierarchyPrepareRequest, CancellationStrategy, SaveOptions, LSPErrorCodes, CodeActionResolveRequest, RegistrationType, SemanticTokensRegistrationType, InsertTextMode, ShowDocumentRequest, - ShowDocumentParams, ShowDocumentResult, OnTypeRenameRequest + ShowDocumentParams, ShowDocumentResult, OnTypeRenameRequest, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport } from 'vscode-languageserver-protocol'; import type { ColorProviderMiddleware } from './colorProvider'; @@ -347,8 +347,8 @@ export interface HandleDiagnosticsSignature { (this: void, uri: Uri, diagnostics: VDiagnostic[]): void; } -export interface HandleProgressSignature

{ - (this: void, type: ProgressType

, token: ProgressToken, params: P): void; +export interface HandleWorkDoneProgressSignature { + (this: void, token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd): void; } export interface ProvideCompletionItemsSignature { @@ -473,7 +473,7 @@ export interface _Middleware { didClose?: NextSignature; handleDiagnostics?: (this: void, uri: Uri, diagnostics: VDiagnostic[], next: HandleDiagnosticsSignature) => void; - handleProgress?:

(this: void, type: ProgressType

, token: ProgressToken, params: P, next: HandleProgressSignature

) => void; + handleWorkDoneProgress?: (this: void, token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd, next: HandleWorkDoneProgressSignature) => void; provideCompletionItem?: (this: void, document: TextDocument, position: VPosition, context: VCompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => ProviderResult; resolveCompletionItem?: (this: void, item: VCompletionItem, token: CancellationToken, next: ResolveCompletionItemSignature) => ProviderResult; provideHover?: (this: void, document: TextDocument, position: VPosition, token: CancellationToken, next: ProvideHoverSignature) => ProviderResult; @@ -2817,14 +2817,16 @@ export abstract class BaseLanguageClient { throw new Error('Language client is not ready yet'); } try { - const handleProgress = this._clientOptions.middleware!.handleProgress; - if (handleProgress !== undefined) { - return this._resolvedConnection!.onProgress(type, token, (params: P) => { - handleProgress(type, token, params, () => handler(params)); - }); - } else { - return this._resolvedConnection!.onProgress(type, token, handler); + if (type == WorkDoneProgress.type) { + const handleWorkDoneProgress = this._clientOptions.middleware!.handleWorkDoneProgress; + if (handleWorkDoneProgress !== undefined) { + return this._resolvedConnection!.onProgress(type, token, (params) => { + handleWorkDoneProgress(token, params, () => handler(params)); + }); + } } + + return this._resolvedConnection!.onProgress(type, token, handler); } catch (error) { this.error(`Registering progress handler for token ${token} failed.`, error); throw error; From ce7986ee9217e4d9ce57faf4ee3976ada17541c7 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Thu, 19 Nov 2020 16:42:03 +0000 Subject: [PATCH 5/7] Fix type issues --- client/src/common/client.ts | 4 ++-- protocol/src/common/protocol.progress.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/common/client.ts b/client/src/common/client.ts index 92893103c..b84d265e2 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -2817,11 +2817,11 @@ export abstract class BaseLanguageClient { throw new Error('Language client is not ready yet'); } try { - if (type == WorkDoneProgress.type) { + if (WorkDoneProgress.is(type)) { const handleWorkDoneProgress = this._clientOptions.middleware!.handleWorkDoneProgress; if (handleWorkDoneProgress !== undefined) { return this._resolvedConnection!.onProgress(type, token, (params) => { - handleWorkDoneProgress(token, params, () => handler(params)); + handleWorkDoneProgress(token, params, () => handler(params as unknown as P)); }); } } diff --git a/protocol/src/common/protocol.progress.ts b/protocol/src/common/protocol.progress.ts index 2617c7cc6..a423dbb6e 100644 --- a/protocol/src/common/protocol.progress.ts +++ b/protocol/src/common/protocol.progress.ts @@ -107,6 +107,10 @@ export interface WorkDoneProgressEnd { export namespace WorkDoneProgress { export const type = new ProgressType(); + + export function is(value: ProgressType): value is typeof type { + return value === type; + } } export interface WorkDoneProgressCreateParams { From b3bf5ddb10a3300b97fae1f53301644066441d91 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Sat, 21 Nov 2020 12:57:32 +0000 Subject: [PATCH 6/7] Change tests to create multiple progress tokens + remove standard handler as the WorkDoneProgressCreateRequest call installs its own --- client-node-tests/src/integration.test.ts | 53 ++++++++++----------- client-node-tests/src/servers/testServer.ts | 5 +- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index 8a2283718..4c22d118b 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -332,44 +332,39 @@ suite('Client integration', () => { assert.ok(middlewareCalled); }); - test('Progress', async () => { + test.only('Progress', async () => { const progressToken = 'TEST-PROGRESS-TOKEN'; const middlewareEvents: Array = []; - const handlerEvents: Array = []; - await new Promise((resolve) => { - // Set up middleware that captures progress events. - middleware.handleWorkDoneProgress = (token: lsclient.ProgressToken, params, next) => { - if (token === progressToken) { - middlewareEvents.push(params); - if (params.kind === 'end') - setImmediate(resolve); - } - return next(token, params); - }; - - // Register a handler for the real progress events. - client.onProgress(lsclient.WorkDoneProgress.type, progressToken, (params) => { - handlerEvents.push(params); + let currentProgressResolver: () => void | undefined; + + // Set up middleware that calls the current resolve function when it gets its 'end' progress event. + middleware.handleWorkDoneProgress = (token: lsclient.ProgressToken, params, next) => { + if (token === progressToken) { + middlewareEvents.push(params); + if (params.kind === 'end') + setImmediate(currentProgressResolver); + } + return next(token, params); + }; + + // Trigger multiple sample progress events. + for (let i = 0; i < 2; i++) { + await new Promise((resolve) => { + currentProgressResolver = resolve; + client.sendRequest( + new lsclient.ProtocolRequestType('testing/sendSampleProgress'), + {}, + tokenSource.token, + ); }); - - // Send a request to the server that triggers sample events. - client.sendRequest( - new lsclient.ProtocolRequestType('testing/sendSampleProgress'), - {}, - tokenSource.token, - ); - }); + } middleware.handleWorkDoneProgress = undefined; // Ensure all events were handled. assert.deepStrictEqual( middlewareEvents.map((p) => p.kind), - ['begin', 'report', 'end'], - ); - assert.deepStrictEqual( - handlerEvents.map((p) => p.kind), - ['begin', 'report', 'end'], + ['begin', 'report', 'end', 'begin', 'report', 'end'], ); }); diff --git a/client-node-tests/src/servers/testServer.ts b/client-node-tests/src/servers/testServer.ts index 6b32662e0..7eeaa15ed 100644 --- a/client-node-tests/src/servers/testServer.ts +++ b/client-node-tests/src/servers/testServer.ts @@ -8,7 +8,7 @@ import { createConnection, Connection, InitializeParams, ServerCapabilities, CompletionItemKind, ResourceOperationKind, FailureHandlingKind, DiagnosticTag, CompletionItemTag, TextDocumentSyncKind, MarkupKind, SignatureHelp, SignatureInformation, ParameterInformation, Location, Range, DocumentHighlight, DocumentHighlightKind, CodeAction, Command, TextEdit, Position, DocumentLink, - ColorInformation, Color, ColorPresentation, FoldingRange, SelectionRange, SymbolKind, ProtocolRequestType, WorkDoneProgress + ColorInformation, Color, ColorPresentation, FoldingRange, SelectionRange, SymbolKind, ProtocolRequestType, WorkDoneProgress, WorkDoneProgressCreateRequest, } from '../../../server/node'; import { URI } from 'vscode-uri'; @@ -286,8 +286,9 @@ connection.languages.onOnTypeRename(() => { connection.onRequest( new ProtocolRequestType('testing/sendSampleProgress'), - (_, __) => { + async (_, __) => { const progressToken = 'TEST-PROGRESS-TOKEN'; + await connection.sendRequest(WorkDoneProgressCreateRequest.type, { token: progressToken }); connection.sendProgress(WorkDoneProgress.type, progressToken, { kind: 'begin', title: 'Test Progress' }); connection.sendProgress(WorkDoneProgress.type, progressToken, { kind: 'report', percentage: 50, message: 'Halfway!' }); connection.sendProgress(WorkDoneProgress.type, progressToken, { kind: 'end', message: 'Completed!' }); From 6bb70ec717cbb5930aa2c80544d426a77e783ad1 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Sat, 21 Nov 2020 14:02:07 +0000 Subject: [PATCH 7/7] Remove .only() from test --- client-node-tests/src/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index 4c22d118b..865e14d89 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -332,7 +332,7 @@ suite('Client integration', () => { assert.ok(middlewareCalled); }); - test.only('Progress', async () => { + test('Progress', async () => { const progressToken = 'TEST-PROGRESS-TOKEN'; const middlewareEvents: Array = []; let currentProgressResolver: () => void | undefined;