diff --git a/CHANGELOG.md b/CHANGELOG.md index 71825ec5e..cab399961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - add ability to deactivate Kernel completions or LSP completion through the settings ([#586], thanks @Carreau) - allow to set a priority for LSP server, allowing to choose which server to use when multiple servers are installed ([#588]) - add auto-detection of pyright server ([#587], thanks @yuntan) + - log server messages in user-accessible console ([#606]) + - old emit-based API of lsp-ws-connection is new deprecated and will be removed in the next major version; please use `serverNotifications`, `clientNotifications`, `clientRequests` and `serverRequests` instead ([#606]) - bug fixes: @@ -14,11 +16,13 @@ - other changes: - drop Node 10 (EOL 2 weeks ago) testing on CI, add Node 15 ([#587]) + - update lsp-ws-connection dependencies ([#606]) [#586]: https://github.com/krassowski/jupyterlab-lsp/pull/586 [#587]: https://github.com/krassowski/jupyterlab-lsp/pull/587 [#588]: https://github.com/krassowski/jupyterlab-lsp/pull/588 [#599]: https://github.com/krassowski/jupyterlab-lsp/pull/599 +[#606]: https://github.com/krassowski/jupyterlab-lsp/pull/606 ### `jupyter-lsp 1.2.0` (2021-04-26) diff --git a/packages/jupyterlab-lsp/src/adapters/adapter.ts b/packages/jupyterlab-lsp/src/adapters/adapter.ts index 9108fe3e2..e5a0d65a4 100644 --- a/packages/jupyterlab-lsp/src/adapters/adapter.ts +++ b/packages/jupyterlab-lsp/src/adapters/adapter.ts @@ -23,6 +23,11 @@ import { IVirtualEditor } from '../virtual/editor'; import IEditor = CodeEditor.IEditor; +import { Dialog, showDialog } from '@jupyterlab/apputils'; + +import IButton = Dialog.IButton; +import createButton = Dialog.createButton; + export class StatusMessage { /** * The text message to be shown on the statusbar @@ -348,7 +353,7 @@ export abstract class WidgetAdapter { const loggerSourceName = virtual_document.uri; const logger = this.extension.user_console.getLogger(loggerSourceName); - data.connection.notifications.window.logMessage.connect( + data.connection.serverNotifications['window/logMessage'].connect( (connection, message) => { this.console.log( data.connection.serverIdentifier, @@ -362,22 +367,44 @@ export abstract class WidgetAdapter { } ); - data.connection.notifications.window.showMessage.connect( + data.connection.serverNotifications['window/showMessage'].connect( (connection, message) => { this.console.log( data.connection.serverIdentifier, virtual_document.uri, - message + message.message ); - logger.log({ - type: 'text', - data: connection.serverIdentifier + ': ' + message.message - } as ILogPayload); - this.extension.app.commands - .execute('logconsole:open', { - source: loggerSourceName - }) - .catch(console.log); + void showDialog({ + title: this.trans.__('Message from ') + connection.serverIdentifier, + body: message.message + }); + } + ); + + data.connection.serverRequests['window/showMessageRequest'].setHandler( + async params => { + this.console.log( + data.connection.serverIdentifier, + virtual_document.uri, + params + ); + const actionItems = params.actions; + const buttons = actionItems.map(action => { + return createButton({ + label: action.title + }); + }); + const result = await showDialog({ + title: + this.trans.__('Message from ') + data.connection.serverIdentifier, + body: params.message, + buttons: buttons + }); + const choice = buttons.indexOf(result.button); + if (choice === -1) { + return; + } + return actionItems[choice]; } ); } diff --git a/packages/jupyterlab-lsp/src/connection.ts b/packages/jupyterlab-lsp/src/connection.ts index 6a1ac81bf..1fff12ccb 100644 --- a/packages/jupyterlab-lsp/src/connection.ts +++ b/packages/jupyterlab-lsp/src/connection.ts @@ -2,15 +2,18 @@ // ISC licence is, quote, "functionally equivalent to the simplified BSD and MIT licenses, // but without language deemed unnecessary following the Berne Convention." (Wikipedia). // Introduced modifications are BSD licenced, copyright JupyterLab development team. -import { Signal } from '@lumino/signaling'; +import { ISignal, Signal } from '@lumino/signaling'; import { + AnyCompletion, + AnyLocation, IDocumentInfo, ILspOptions, IPosition, LspWsConnection } from 'lsp-ws-connection'; import type * as rpc from 'vscode-jsonrpc'; -import type * as lsProtocol from 'vscode-languageserver-protocol'; +import type * as lsp from 'vscode-languageserver-protocol'; +import type { MessageConnection } from 'vscode-ws-jsonrpc'; import { until_ready } from './utils'; @@ -18,47 +21,282 @@ interface ILSPOptions extends ILspOptions { serverIdentifier?: string; } -interface ConnectionSignal extends Signal { - // empty +/** + * Method strings are reproduced here because a non-typing import of + * `vscode-languageserver-protocol` is ridiculously expensive. + */ +export namespace Method { + /** Server notifications */ + export enum ServerNotification { + PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics', + SHOW_MESSAGE = 'window/showMessage', + LOG_TRACE = '$/logTrace', + LOG_MESSAGE = 'window/logMessage' + } + + /** Client notifications */ + export enum ClientNotification { + DID_CHANGE = 'textDocument/didChange', + DID_CHANGE_CONFIGURATION = 'workspace/didChangeConfiguration', + DID_OPEN = 'textDocument/didOpen', + DID_SAVE = 'textDocument/didSave', + INITIALIZED = 'initialized', + SET_TRACE = '$/setTrace' + } + + /** Server requests */ + export enum ServerRequests { + REGISTER_CAPABILITY = 'client/registerCapability', + SHOW_MESSAGE_REQUEST = 'window/showMessageRequest', + UNREGISTER_CAPABILITY = 'client/unregisterCapability' + } + + /** Client requests */ + export enum ClientRequest { + COMPLETION = 'textDocument/completion', + COMPLETION_ITEM_RESOLVE = 'completionItem/resolve', + DEFINITION = 'textDocument/definition', + DOCUMENT_HIGHLIGHT = 'textDocument/documentHighlight', + DOCUMENT_SYMBOL = 'textDocument/documentSymbol', + HOVER = 'textDocument/hover', + IMPLEMENTATION = 'textDocument/implementation', + INITIALIZE = 'initialize', + REFERENCES = 'textDocument/references', + RENAME = 'textDocument/rename', + SIGNATURE_HELP = 'textDocument/signatureHelp', + TYPE_DEFINITION = 'textDocument/typeDefinition' + } +} + +export interface IServerNotifyParams { + [Method.ServerNotification.LOG_MESSAGE]: lsp.LogMessageParams; + [Method.ServerNotification.LOG_TRACE]: rpc.LogTraceParams; + [Method.ServerNotification.PUBLISH_DIAGNOSTICS]: lsp.PublishDiagnosticsParams; + [Method.ServerNotification.SHOW_MESSAGE]: lsp.ShowMessageParams; +} + +export interface IClientNotifyParams { + [Method.ClientNotification + .DID_CHANGE_CONFIGURATION]: lsp.DidChangeConfigurationParams; + [Method.ClientNotification.DID_CHANGE]: lsp.DidChangeTextDocumentParams; + [Method.ClientNotification.DID_OPEN]: lsp.DidOpenTextDocumentParams; + [Method.ClientNotification.DID_SAVE]: lsp.DidSaveTextDocumentParams; + [Method.ClientNotification.INITIALIZED]: lsp.InitializedParams; + [Method.ClientNotification.SET_TRACE]: rpc.SetTraceParams; +} + +export interface IServerRequestParams { + [Method.ServerRequests.REGISTER_CAPABILITY]: lsp.RegistrationParams; + [Method.ServerRequests.SHOW_MESSAGE_REQUEST]: lsp.ShowMessageRequestParams; + [Method.ServerRequests.UNREGISTER_CAPABILITY]: lsp.UnregistrationParams; +} + +export interface IServerResult { + [Method.ServerRequests.REGISTER_CAPABILITY]: void; + [Method.ServerRequests.SHOW_MESSAGE_REQUEST]: lsp.MessageActionItem | null; + [Method.ServerRequests.UNREGISTER_CAPABILITY]: void; +} + +export interface IClientRequestParams { + [Method.ClientRequest.COMPLETION_ITEM_RESOLVE]: lsp.CompletionItem; + [Method.ClientRequest.COMPLETION]: lsp.CompletionParams; + [Method.ClientRequest.DEFINITION]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.DOCUMENT_HIGHLIGHT]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.DOCUMENT_SYMBOL]: lsp.DocumentSymbolParams; + [Method.ClientRequest.HOVER]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.IMPLEMENTATION]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.INITIALIZE]: lsp.InitializeParams; + [Method.ClientRequest.REFERENCES]: lsp.ReferenceParams; + [Method.ClientRequest.RENAME]: lsp.RenameParams; + [Method.ClientRequest.SIGNATURE_HELP]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.TYPE_DEFINITION]: lsp.TextDocumentPositionParams; } -interface INamespace { - [index: string]: ConnectionSignal | ((params: any) => void); +export interface IClientResult { + [Method.ClientRequest.COMPLETION_ITEM_RESOLVE]: lsp.CompletionItem; + [Method.ClientRequest.COMPLETION]: AnyCompletion; + [Method.ClientRequest.DEFINITION]: AnyLocation; + [Method.ClientRequest.DOCUMENT_HIGHLIGHT]: lsp.DocumentHighlight[]; + [Method.ClientRequest.DOCUMENT_SYMBOL]: lsp.DocumentSymbol[]; + [Method.ClientRequest.HOVER]: lsp.Hover; + [Method.ClientRequest.IMPLEMENTATION]: AnyLocation; + [Method.ClientRequest.INITIALIZE]: lsp.InitializeResult; + [Method.ClientRequest.REFERENCES]: Location[]; + [Method.ClientRequest.RENAME]: lsp.WorkspaceEdit; + [Method.ClientRequest.SIGNATURE_HELP]: lsp.SignatureHelp; + [Method.ClientRequest.TYPE_DEFINITION]: AnyLocation; } -interface ILSPNotifications { - $: { - logTrace: ConnectionSignal; - setTrace: (params: rpc.SetTraceParams) => void; - }; - window: { - showMessage: ConnectionSignal; - logMessage: ConnectionSignal; - }; - [index: string]: INamespace; +export type ServerNotifications< + T extends keyof IServerNotifyParams = keyof IServerNotifyParams +> = { + // ISignal does not have emit, which is intended - client cannot emit server notifications. + [key in T]: ISignal; +}; + +export type ClientNotifications< + T extends keyof IClientNotifyParams = keyof IClientNotifyParams +> = { + // Signal has emit. + [key in T]: Signal; +}; + +export interface IClientRequestHandler< + T extends keyof IClientRequestParams = keyof IClientRequestParams +> { + request(params: IClientRequestParams[T]): Promise; +} + +export interface IServerRequestHandler< + T extends keyof IServerRequestParams = keyof IServerRequestParams +> { + setHandler( + handler: (params: IServerRequestParams[T]) => Promise + ): void; + clearHandler(): void; +} + +export type ClientRequests< + T extends keyof IClientRequestParams = keyof IClientRequestParams +> = { + // has async request(params) returning a promise with result. + [key in T]: IClientRequestHandler; +}; + +export type ServerRequests< + T extends keyof IServerRequestParams = keyof IServerRequestParams +> = { + // has async request(params) returning a promise with result. + [key in T]: IServerRequestHandler; +}; + +class ClientRequestHandler< + T extends keyof IClientRequestParams = keyof IClientRequestParams +> implements IClientRequestHandler { + constructor(protected connection: MessageConnection, protected method: T) {} + request(params: IClientRequestParams[T]): Promise { + return this.connection.sendRequest(this.method, params); + } } +class ServerRequestHandler< + T extends keyof IServerRequestParams = keyof IServerRequestParams +> implements IServerRequestHandler { + private _handler: ( + params: IServerRequestParams[T] + ) => Promise; + + constructor(protected connection: MessageConnection, protected method: T) { + // on request accepts "thenable" + this.connection.onRequest(method, this.handle); + this._handler = null; + } + + private handle(request: IServerRequestParams[T]): Promise { + if (!this._handler) { + return; + } + return this._handler(request); + } + + setHandler( + handler: (params: IServerRequestParams[T]) => Promise + ) { + this._handler = handler; + } + + clearHandler() { + this._handler = null; + } +} + +export const Provider: { [key: string]: keyof lsp.ServerCapabilities } = { + TEXT_DOCUMENT_SYNC: 'textDocumentSync', + COMPLETION: 'completionProvider', + HOVER: 'hoverProvider', + SIGNATURE_HELP: 'signatureHelpProvider', + DECLARATION: 'declarationProvider', + DEFINITION: 'definitionProvider', + TYPE_DEFINITION: 'typeDefinitionProvider', + IMPLEMENTATION: 'implementationProvider', + REFERENCES: 'referencesProvider', + DOCUMENT_HIGHLIGHT: 'documentHighlightProvider', + DOCUMENT_SYMBOL: 'documentSymbolProvider', + CODE_ACTION: 'codeActionProvider', + CODE_LENS: 'codeLensProvider', + DOCUMENT_LINK: 'documentLinkProvider', + COLOR: 'colorProvider', + DOCUMENT_FORMATTING: 'documentFormattingProvider', + DOCUMENT_RANGE_FORMATTING: 'documentRangeFormattingProvider', + DOCUMENT_ON_TYPE_FORMATTING: 'documentOnTypeFormattingProvider', + RENAME: 'renameProvider', + FOLDING_RANGE: 'foldingRangeProvider', + EXECUTE_COMMAND: 'executeCommandProvider', + SELECTION_RANGE: 'selectionRangeProvider', + WORKSPACE_SYMBOL: 'workspaceSymbolProvider', + WORKSPACE: 'workspace' +}; + export class LSPConnection extends LspWsConnection { protected documentsToOpen: IDocumentInfo[]; public serverIdentifier: string; - public notifications: ILSPNotifications; + + public serverNotifications: ServerNotifications; + public clientNotifications: ClientNotifications; + public clientRequests: ClientRequests; + public serverRequests: ServerRequests; + + protected constructNotificationHandlers< + T extends ServerNotifications | ClientNotifications, + U extends keyof T = keyof T + >( + methods: typeof Method.ServerNotification | typeof Method.ClientNotification + ) { + const result: { [key in U]?: Signal } = {}; + for (let method of Object.values(methods)) { + result[method as U] = new Signal(this); + } + return result as T; + } + + protected constructClientRequestHandler< + T extends ClientRequests, + U extends keyof T = keyof T + >(methods: typeof Method.ClientRequest) { + const result: { [key in U]?: IClientRequestHandler } = {}; + for (let method of Object.values(methods)) { + result[method as U] = new ClientRequestHandler( + this.connection, + (method as U) as any + ); + } + return result as T; + } + + protected constructServerRequestHandler< + T extends ServerRequests, + U extends keyof T = keyof T + >(methods: typeof Method.ServerRequests) { + const result: { [key in U]?: IServerRequestHandler } = {}; + for (let method of Object.values(methods)) { + result[method as U] = new ServerRequestHandler( + this.connection, + (method as U) as any + ); + } + return result as T; + } constructor(options: ILSPOptions) { super(options); this.serverIdentifier = options?.serverIdentifier; this.documentsToOpen = []; - this.notifications = { - $: { - logTrace: new Signal(this), - setTrace: params => { - this.connection.sendNotification('$/setTrace', params); - } - }, - window: { - showMessage: new Signal(this), - logMessage: new Signal(this) - } - }; + this.serverNotifications = this.constructNotificationHandlers< + ServerNotifications + >(Method.ServerNotification); + this.clientNotifications = this.constructNotificationHandlers< + ClientNotifications + >(Method.ClientNotification); } sendOpenWhenReady(documentInfo: IDocumentInfo) { @@ -69,29 +307,40 @@ export class LSPConnection extends LspWsConnection { } } - protected onServerInitialized(params: lsProtocol.InitializeResult) { + protected onServerInitialized(params: lsp.InitializeResult) { super.onServerInitialized(params); while (this.documentsToOpen.length) { this.sendOpen(this.documentsToOpen.pop()); } - for (const namespaceName in this.notifications) { - const namespace = this.notifications[namespaceName]; - for (const memberName in namespace) { - const endpoint = namespace[memberName]; - if (endpoint instanceof Signal) { - this.connection.onNotification( - `${namespaceName}/${memberName}`, - params => { - endpoint.emit(params); - } - ); - } - } + + for (const method of Object.values( + Method.ServerNotification + ) as (keyof ServerNotifications)[]) { + const signal = this.serverNotifications[method] as Signal; + this.connection.onNotification(method, params => { + signal.emit(params); + }); + } + + for (const method of Object.values( + Method.ClientNotification + ) as (keyof ClientNotifications)[]) { + const signal = this.clientNotifications[method] as Signal; + signal.connect((emitter, params) => { + this.connection.sendNotification(method, params); + }); } + + this.clientRequests = this.constructClientRequestHandler( + Method.ClientRequest + ); + this.serverRequests = this.constructServerRequestHandler( + Method.ServerRequests + ); } public sendSelectiveChange( - changeEvent: lsProtocol.TextDocumentContentChangeEvent, + changeEvent: lsp.TextDocumentContentChangeEvent, documentInfo: IDocumentInfo ) { this._sendChange([changeEvent], documentInfo); @@ -101,23 +350,33 @@ export class LSPConnection extends LspWsConnection { this._sendChange([{ text }], documentInfo); } + /** + * @deprecated The method should not be used in new code. Use provides() instead. + */ public isRenameSupported() { return !!( this.serverCapabilities && this.serverCapabilities.renameProvider ); } + provides(provider: keyof lsp.ServerCapabilities): boolean { + return !!(this.serverCapabilities && this.serverCapabilities[provider]); + } + + /** + * @deprecated The method should not be used in new code + */ async rename( location: IPosition, documentInfo: IDocumentInfo, newName: string, emit = true - ): Promise { + ): Promise { if (!this.isReady || !this.isRenameSupported()) { return; } - const params: lsProtocol.RenameParams = { + const params: lsp.RenameParams = { textDocument: { uri: documentInfo.uri }, @@ -128,7 +387,7 @@ export class LSPConnection extends LspWsConnection { newName }; - const edit: lsProtocol.WorkspaceEdit = await this.connection.sendRequest( + const edit: lsp.WorkspaceEdit = await this.connection.sendRequest( 'textDocument/rename', params ); @@ -170,7 +429,7 @@ export class LSPConnection extends LspWsConnection { } private _sendChange( - changeEvents: lsProtocol.TextDocumentContentChangeEvent[], + changeEvents: lsp.TextDocumentContentChangeEvent[], documentInfo: IDocumentInfo ) { if (!this.isReady) { @@ -179,11 +438,11 @@ export class LSPConnection extends LspWsConnection { if (!this.openedUris.get(documentInfo.uri)) { this.sendOpen(documentInfo); } - const textDocumentChange: lsProtocol.DidChangeTextDocumentParams = { + const textDocumentChange: lsp.DidChangeTextDocumentParams = { textDocument: { uri: documentInfo.uri, version: documentInfo.version - } as lsProtocol.VersionedTextDocumentIdentifier, + } as lsp.VersionedTextDocumentIdentifier, contentChanges: changeEvents }; this.connection.sendNotification( @@ -193,18 +452,19 @@ export class LSPConnection extends LspWsConnection { documentInfo.version++; } - async getCompletionResolve(completionItem: lsProtocol.CompletionItem) { + async getCompletionResolve(completionItem: lsp.CompletionItem) { if (!this.isReady || !this.isCompletionResolveProvider()) { return; } - return this.connection.sendRequest( + return this.connection.sendRequest( 'completionItem/resolve', completionItem ); } /** - * Does support completionItem/resolve?. + * Does support completionItem/resolve? + * @deprecated The method should not be used in new code */ public isCompletionResolveProvider(): boolean { return this.serverCapabilities?.completionProvider?.resolveProvider;