diff --git a/atest/05_Features/Completion.robot b/atest/05_Features/Completion.robot index 29974166d..93eaec4da 100644 --- a/atest/05_Features/Completion.robot +++ b/atest/05_Features/Completion.robot @@ -36,7 +36,6 @@ Filters Completions In Case Sensitive Mode Completer Should Suggest test Completer Should Not Suggest TabError - Can Prioritize Kernel Completions # note: disabling pre-filtering to get ranking without match scoring Configure JupyterLab Plugin {"kernelCompletionsFirst": true, "kernelResponseTimeout": -1, "preFilterMatches": false} plugin id=${COMPLETION PLUGIN ID} diff --git a/atest/05_Features/Diagnostics.robot b/atest/05_Features/Diagnostics.robot index 987d8ad5a..8a90dbb76 100644 --- a/atest/05_Features/Diagnostics.robot +++ b/atest/05_Features/Diagnostics.robot @@ -4,7 +4,6 @@ Force Tags feature:diagnostics Test Setup Setup Notebook Python Diagnostic.ipynb Test Teardown Clean Up After Working With File Diagnostic.ipynb Resource ../Keywords.robot - # note: diagnostics are also tested in 01_Editor and 04_Interface/DiagnosticsPanel.robot *** Test Cases *** diff --git a/atest/05_Features/Signature.robot b/atest/05_Features/Signature.robot index d441000f9..2dd717f29 100644 --- a/atest/05_Features/Signature.robot +++ b/atest/05_Features/Signature.robot @@ -62,18 +62,16 @@ Details Should Expand On Click Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX} Wait Until Keyword Succeeds 10x 0.5s Element Should Contain ${SIGNATURE_BOX} Short description. Page Should Contain Element ${SIGNATURE_DETAILS} - Details Should Be Collapsed ${SIGNATURE_DETAILS_CSS} + Details Should Be Collapsed ${SIGNATURE_DETAILS_CSS} Click Element ${SIGNATURE_DETAILS} Details Should Be Expanded ${SIGNATURE_DETAILS_CSS} *** Keywords *** - Details Should Be Expanded [Arguments] ${css_locator} ${is_open} Execute JavaScript return document.querySelector('${css_locator}').open Should Be True ${is_open} == True - Details Should Be Collapsed [Arguments] ${css_locator} ${is_open} Execute JavaScript return document.querySelector('${css_locator}').open diff --git a/docs/Extending.ipynb b/docs/Extending.ipynb index ce6f9a826..63f0bd806 100644 --- a/docs/Extending.ipynb +++ b/docs/Extending.ipynb @@ -34,6 +34,8 @@ " `CodeMirrorIntegration` class.\n", "- `labIntegration`: an optional object integrating feature with the JupyterLab\n", " interface\n", + "- `capabilities`: an optional object defining the [client\n", + " capabilities][clientcapabilities] implemented by your feature,\n", "- optional fields for easy integration of some of the common JupyterLab systems,\n", " such as:\n", " - settings system\n", @@ -46,7 +48,10 @@ "#### How to override the default implementation of a feature?\n", "\n", "You can specify a list of extensions to be disabled the the feature manager\n", - "passing their plugin identifiers in `supersedes` field of `IFeatureOptions`." + "passing their plugin identifiers in `supersedes` field of `IFeatureOptions`.\n", + "\n", + "[clientCapabilities]:\n", + "https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#clientCapabilities]" ] }, { diff --git a/packages/jupyterlab-lsp/src/adapters/adapter.ts b/packages/jupyterlab-lsp/src/adapters/adapter.ts index 68f39c610..221885ca4 100644 --- a/packages/jupyterlab-lsp/src/adapters/adapter.ts +++ b/packages/jupyterlab-lsp/src/adapters/adapter.ts @@ -6,6 +6,7 @@ import { ILogPayload } from '@jupyterlab/logconsole'; import { nullTranslator, TranslationBundle } from '@jupyterlab/translation'; import { JSONObject } from '@lumino/coreutils'; import { Signal } from '@lumino/signaling'; +import mergeWith from 'lodash.mergewith'; import { ICommandContext } from '../command_manager'; import { LSPConnection } from '../connection'; @@ -17,7 +18,7 @@ import { import { EditorAdapter } from '../editor_integration/editor_adapter'; import { IFeature, IFeatureEditorIntegration } from '../feature'; import { ILSPExtension, ILSPLogConsole } from '../index'; -import { LanguageIdentifier } from '../lsp'; +import { ClientCapabilities, LanguageIdentifier } from '../lsp'; import { IRootPosition, IVirtualPosition } from '../positioning'; import { IForeignContext, VirtualDocument } from '../virtual/document'; import { IVirtualEditor } from '../virtual/editor'; @@ -632,7 +633,31 @@ export abstract class WidgetAdapter { this.console.log(`will connect using language: ${language}`); + let capabilities: ClientCapabilities = { + textDocument: { + synchronization: { + dynamicRegistration: true, + willSave: false, + didSave: true, + willSaveWaitUntil: false + } + }, + workspace: { + didChangeConfiguration: { + dynamicRegistration: true + } + } + }; + + for (const feature of this.extension.feature_manager.features) { + if (!feature.capabilities) { + continue; + } + capabilities = mergeWith(capabilities, feature.capabilities); + } + let options: ISocketConnectionOptions = { + capabilities, virtual_document, language, document_path: this.document_path diff --git a/packages/jupyterlab-lsp/src/connection.ts b/packages/jupyterlab-lsp/src/connection.ts index 5d9589cdf..7e7609284 100644 --- a/packages/jupyterlab-lsp/src/connection.ts +++ b/packages/jupyterlab-lsp/src/connection.ts @@ -19,11 +19,12 @@ import type * as rpc from 'vscode-jsonrpc'; import type * as lsp from 'vscode-languageserver-protocol'; import type { MessageConnection } from 'vscode-ws-jsonrpc'; -import { CompletionItemTag, DiagnosticTag } from './lsp'; +import { ClientCapabilities } from './lsp'; import { ILSPLogConsole } from './tokens'; import { until_ready } from './utils'; interface ILSPOptions extends ILspOptions { + capabilities: ClientCapabilities; serverIdentifier?: string; console: ILSPLogConsole; } @@ -336,6 +337,7 @@ export class LSPConnection extends LspWsConnection { public clientRequests: ClientRequests; public serverRequests: ServerRequests; protected console: ILSPLogConsole; + private _options: ILSPOptions; public logAllCommunication: boolean; public log(kind: MessageKind, message: IMessageLog) { @@ -379,6 +381,7 @@ export class LSPConnection extends LspWsConnection { constructor(options: ILSPOptions) { super(options); + this._options = options; this.logAllCommunication = false; this.serverIdentifier = options.serverIdentifier; this.console = options.console.scope(this.serverIdentifier + ' connection'); @@ -400,66 +403,10 @@ export class LSPConnection extends LspWsConnection { protected initializeParams(): lsp.InitializeParams { return { ...super.initializeParams(), - capabilities: { - textDocument: { - hover: { - dynamicRegistration: true, - contentFormat: ['markdown', 'plaintext'] - }, - synchronization: { - dynamicRegistration: true, - willSave: false, - didSave: true, - willSaveWaitUntil: false - }, - completion: { - dynamicRegistration: true, - completionItem: { - snippetSupport: false, - commitCharactersSupport: true, - documentationFormat: ['markdown', 'plaintext'], - deprecatedSupport: true, - preselectSupport: false, - tagSupport: { - valueSet: [CompletionItemTag.Deprecated] - } - }, - contextSupport: false - }, - publishDiagnostics: { - tagSupport: { - valueSet: [DiagnosticTag.Deprecated, DiagnosticTag.Unnecessary] - } - }, - signatureHelp: { - dynamicRegistration: true, - signatureInformation: { - documentationFormat: ['markdown', 'plaintext'] - } - }, - declaration: { - dynamicRegistration: true, - linkSupport: true - }, - definition: { - dynamicRegistration: true, - linkSupport: true - }, - typeDefinition: { - dynamicRegistration: true, - linkSupport: true - }, - implementation: { - dynamicRegistration: true, - linkSupport: true - } - } as lsp.TextDocumentClientCapabilities, - workspace: { - didChangeConfiguration: { - dynamicRegistration: true - } - } as lsp.WorkspaceClientCapabilities - } as lsp.ClientCapabilities, + // TODO: remove as `lsp.ClientCapabilities` after upgrading to 3.17 + // which should finally include a fix for moniker issue: + // https://github.com/microsoft/vscode-languageserver-node/pull/720 + capabilities: this._options.capabilities as lsp.ClientCapabilities, initializationOptions: null, processId: null, workspaceFolders: null diff --git a/packages/jupyterlab-lsp/src/connection_manager.ts b/packages/jupyterlab-lsp/src/connection_manager.ts index 1511dfe33..e0b3907ee 100644 --- a/packages/jupyterlab-lsp/src/connection_manager.ts +++ b/packages/jupyterlab-lsp/src/connection_manager.ts @@ -4,6 +4,7 @@ import type * as protocol from 'vscode-languageserver-protocol'; import { AskServersToSendTraceNotifications } from './_plugin'; import type * as ConnectionModuleType from './connection'; +import { ClientCapabilities } from './lsp'; import { ILSPLogConsole, ILanguageServerManager, @@ -29,6 +30,10 @@ export interface ISocketConnectionOptions { * Path to the document in the JupyterLab space */ document_path: string; + /** + * LSP capabilities describing currently supported features + */ + capabilities: ClientCapabilities; } /** @@ -127,7 +132,7 @@ export class DocumentConnectionManager { options: ISocketConnectionOptions ): Promise { this.console.log('Connection Socket', options); - let { virtual_document, language } = options; + let { virtual_document, language, capabilities } = options; this.connect_document_signals(virtual_document); @@ -153,7 +158,8 @@ export class DocumentConnectionManager { language_server_id!, uris, this.on_new_connection, - this.console + this.console, + capabilities ); // if connecting for the first time, all documents subsequent documents will @@ -469,7 +475,8 @@ namespace Private { language_server_id: TLanguageServerId, uris: DocumentConnectionManager.IURIs, onCreate: (connection: ConnectionModuleType.LSPConnection) => void, - console: ILSPLogConsole + console: ILSPLogConsole, + capabilities: ClientCapabilities ): Promise { if (_promise == null) { // TODO: consider lazy-loading _only_ the modules that _must_ be webpacked @@ -489,7 +496,8 @@ namespace Private { serverUri: uris.server, rootUri: uris.base, serverIdentifier: language_server_id, - console: console + console: console, + capabilities: capabilities }); // TODO: remove remaining unbounded users of connection.on connection.setMaxListeners(999); diff --git a/packages/jupyterlab-lsp/src/editor_integration/testutils.ts b/packages/jupyterlab-lsp/src/editor_integration/testutils.ts index c22a54ace..041ce8580 100644 --- a/packages/jupyterlab-lsp/src/editor_integration/testutils.ts +++ b/packages/jupyterlab-lsp/src/editor_integration/testutils.ts @@ -258,7 +258,8 @@ function FeatureSupport(Base: TBase) { serverUri: 'ws://localhost:8080', rootUri: 'file:///unit-test', serverIdentifier: DEFAULT_SERVER_ID, - console: new BrowserConsole() + console: new BrowserConsole(), + capabilities: {} }); } diff --git a/packages/jupyterlab-lsp/src/feature.ts b/packages/jupyterlab-lsp/src/feature.ts index c5540ee52..3dfbf8983 100644 --- a/packages/jupyterlab-lsp/src/feature.ts +++ b/packages/jupyterlab-lsp/src/feature.ts @@ -8,6 +8,7 @@ import { Signal } from '@lumino/signaling'; import { StatusMessage, WidgetAdapter } from './adapters/adapter'; import { CommandEntryPoint, ICommandContext } from './command_manager'; import { LSPConnection } from './connection'; +import { ClientCapabilities } from './lsp'; import { IRootPosition } from './positioning'; import { VirtualDocument } from './virtual/document'; import { IEditorChange, IVirtualEditor } from './virtual/editor'; @@ -123,6 +124,10 @@ export interface IFeature { IEditorName, IFeatureEditorIntegrationConstructor> >; + /** + * LSP capabilities implemented by the feature. + */ + capabilities?: ClientCapabilities; /** * Command specification, including context menu placement options. */ diff --git a/packages/jupyterlab-lsp/src/features/completion/index.ts b/packages/jupyterlab-lsp/src/features/completion/index.ts index 8d9941033..ffc41bf19 100644 --- a/packages/jupyterlab-lsp/src/features/completion/index.ts +++ b/packages/jupyterlab-lsp/src/features/completion/index.ts @@ -11,6 +11,7 @@ import { ILSPCompletionThemeManager } from '@krassowski/completion-theme/lib/typ import completionSvg from '../../../style/icons/completion.svg'; import { CodeCompletion as LSPCompletionSettings } from '../../_completion'; import { FeatureSettings } from '../../feature'; +import { CompletionItemTag } from '../../lsp'; import { ILSPAdapterManager, ILSPFeatureManager, @@ -66,7 +67,25 @@ export const COMPLETION_PLUGIN: JupyterFrontEndPlugin = { id: FEATURE_ID, name: 'LSP Completion', labIntegration: labIntegration, - settings: settings + settings: settings, + capabilities: { + textDocument: { + completion: { + dynamicRegistration: true, + completionItem: { + snippetSupport: false, + commitCharactersSupport: true, + documentationFormat: ['markdown', 'plaintext'], + deprecatedSupport: true, + preselectSupport: false, + tagSupport: { + valueSet: [CompletionItemTag.Deprecated] + } + }, + contextSupport: false + } + } + } } }); } diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/index.ts b/packages/jupyterlab-lsp/src/features/diagnostics/index.ts index 98c87caa6..17b6218b6 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/index.ts +++ b/packages/jupyterlab-lsp/src/features/diagnostics/index.ts @@ -6,6 +6,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator, TranslationBundle } from '@jupyterlab/translation'; import { FeatureSettings, IFeatureCommand } from '../../feature'; +import { DiagnosticTag } from '../../lsp'; import { ILSPFeatureManager, PLUGIN_ID } from '../../tokens'; import { @@ -65,6 +66,15 @@ export const DIAGNOSTICS_PLUGIN: JupyterFrontEndPlugin = { ['CodeMirrorEditor', DiagnosticsCM] ]), id: FEATURE_ID, + capabilities: { + textDocument: { + publishDiagnostics: { + tagSupport: { + valueSet: [DiagnosticTag.Deprecated, DiagnosticTag.Unnecessary] + } + } + } + }, name: 'LSP Diagnostics', settings: settings, commands: COMMANDS(trans) diff --git a/packages/jupyterlab-lsp/src/features/hover.ts b/packages/jupyterlab-lsp/src/features/hover.ts index 1e7e31862..769303fcb 100644 --- a/packages/jupyterlab-lsp/src/features/hover.ts +++ b/packages/jupyterlab-lsp/src/features/hover.ts @@ -611,7 +611,15 @@ export const HOVER_PLUGIN: JupyterFrontEndPlugin = { id: FEATURE_ID, name: 'LSP Hover tooltip', labIntegration: labIntegration, - settings: settings + settings: settings, + capabilities: { + textDocument: { + hover: { + dynamicRegistration: true, + contentFormat: ['markdown', 'plaintext'] + } + } + } } }); } diff --git a/packages/jupyterlab-lsp/src/features/jump_to.ts b/packages/jupyterlab-lsp/src/features/jump_to.ts index e061947ab..47d3799ba 100644 --- a/packages/jupyterlab-lsp/src/features/jump_to.ts +++ b/packages/jupyterlab-lsp/src/features/jump_to.ts @@ -340,7 +340,27 @@ export const JUMP_PLUGIN: JupyterFrontEndPlugin = { id: FEATURE_ID, name: 'Jump to definition', labIntegration: labIntegration, - settings: settings + settings: settings, + capabilities: { + textDocument: { + declaration: { + dynamicRegistration: true, + linkSupport: true + }, + definition: { + dynamicRegistration: true, + linkSupport: true + }, + typeDefinition: { + dynamicRegistration: true, + linkSupport: true + }, + implementation: { + dynamicRegistration: true, + linkSupport: true + } + } + } } }); } diff --git a/packages/jupyterlab-lsp/src/features/signature.ts b/packages/jupyterlab-lsp/src/features/signature.ts index f35c9da0f..a6a391819 100644 --- a/packages/jupyterlab-lsp/src/features/signature.ts +++ b/packages/jupyterlab-lsp/src/features/signature.ts @@ -488,7 +488,17 @@ export const SIGNATURE_PLUGIN: JupyterFrontEndPlugin = { id: FEATURE_ID, name: 'LSP Function signature', labIntegration: labIntegration, - settings: settings + settings: settings, + capabilities: { + textDocument: { + signatureHelp: { + dynamicRegistration: true, + signatureInformation: { + documentationFormat: ['markdown', 'plaintext'] + } + } + } + } } }); } diff --git a/packages/jupyterlab-lsp/src/lsp.ts b/packages/jupyterlab-lsp/src/lsp.ts index dfaf05709..e461e1d4d 100644 --- a/packages/jupyterlab-lsp/src/lsp.ts +++ b/packages/jupyterlab-lsp/src/lsp.ts @@ -1,3 +1,11 @@ +import type * as lsp from 'vscode-languageserver-protocol'; + +/** Workaround to silence a bug in https://github.com/microsoft/vscode-languageserver-node/pull/720 */ +export type ClientCapabilities = Omit< + lsp.ClientCapabilities, + 'textDocument' +> & { textDocument?: Omit }; + export enum DiagnosticSeverity { Error = 1, Warning = 2,