From d5dea60f23ea968b8272682283782e2561a2ecb7 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 24 Aug 2022 00:28:54 +0100 Subject: [PATCH] Explore how to implement `isIncomplete` support --- packages/jupyterlab-lsp/src/connection.ts | 4 +- .../src/features/completion/completion.ts | 36 ++++++++++- .../features/completion/completion_handler.ts | 62 +++++++++++++++---- .../src/features/completion/model.ts | 23 ++++++- packages/jupyterlab-lsp/src/lsp.ts | 12 ++++ packages/lsp-ws-connection/src/types.ts | 9 +++ 6 files changed, 128 insertions(+), 18 deletions(-) diff --git a/packages/jupyterlab-lsp/src/connection.ts b/packages/jupyterlab-lsp/src/connection.ts index ef17d576e..083910ddd 100644 --- a/packages/jupyterlab-lsp/src/connection.ts +++ b/packages/jupyterlab-lsp/src/connection.ts @@ -4,8 +4,6 @@ // Introduced modifications are BSD licenced, copyright JupyterLab development team. import { ISignal, Signal } from '@lumino/signaling'; import { - AnyCompletion, - AnyLocation, IDocumentInfo, ILspOptions, IPosition, @@ -19,7 +17,7 @@ import type * as rpc from 'vscode-jsonrpc'; import type * as lsp from 'vscode-languageserver-protocol'; import type { MessageConnection } from 'vscode-ws-jsonrpc'; -import { ClientCapabilities } from './lsp'; +import { AnyLocation, AnyCompletion, ClientCapabilities } from './lsp'; import { ILSPLogConsole } from './tokens'; import { until_ready } from './utils'; diff --git a/packages/jupyterlab-lsp/src/features/completion/completion.ts b/packages/jupyterlab-lsp/src/features/completion/completion.ts index 1ba99b0b3..0c7fdad52 100644 --- a/packages/jupyterlab-lsp/src/features/completion/completion.ts +++ b/packages/jupyterlab-lsp/src/features/completion/completion.ts @@ -312,11 +312,45 @@ export class CompletionLabIntegration implements IFeatureLabIntegration { completer.model = new CompleterModel(); } else { completer.addClass(LSP_COMPLETER_CLASS); - completer.model = new LSPCompleterModel({ + const model = new LSPCompleterModel({ caseSensitive: this.settings.composite.caseSensitive, includePerfectMatches: this.settings.composite.includePerfectMatches, preFilterMatches: this.settings.composite.preFilterMatches }); + completer.model = model; + model.queryChanged.connect(this._handleQuery.bind(this)); + // TODO: disconnect! + } + } + + /** + * User typed a character while the completer is shown changing the query. + */ + private async _handleQuery(model: LSPCompleterModel, query: string) { + // it is important not to fail here: otherwise we break native completer too. + try { + if (!this.current_completion_connector.isIncomplete) { + // do nothing if the result was complete + return; + } else if(!this.current_adapter) { + return; + } else { + // TODO: can we only fetch LSP items, keep kernel items as-is? + await this.current_adapter.update_documents(); + const reply = await this.current_completion_connector.fetch(); + if (reply) { + model.query = '' + // ref `CompletionHandler._updateModel` + model.cursor = { + start: reply.start, // should be wrapped in `Text.charIndexToJsIndex(start, text)`, + end: reply.end + } + model.original = model.current; + model.setCompletionItems(reply.items as LazyCompletionItem[]); + } + } + } catch(e) { + this.console.error('handling query change failed', e); } } diff --git a/packages/jupyterlab-lsp/src/features/completion/completion_handler.ts b/packages/jupyterlab-lsp/src/features/completion/completion_handler.ts index 1693590ec..67bdd9dee 100644 --- a/packages/jupyterlab-lsp/src/features/completion/completion_handler.ts +++ b/packages/jupyterlab-lsp/src/features/completion/completion_handler.ts @@ -19,6 +19,7 @@ import { LSPConnection } from '../../connection'; import { PositionConverter } from '../../converter'; import { FeatureSettings } from '../../feature'; import { + AnyCompletion, AdditionalCompletionTriggerKinds, CompletionItemKind, CompletionTriggerKind, @@ -60,6 +61,7 @@ export function transformLSPCompletions( createCompletionItem: (kind: string, match: lsProtocol.CompletionItem) => T, console: ILSPLogConsole ) { + console.debug('Transforming'); let prefix = token.value.slice(0, position_in_token + 1); let all_non_prefixed = true; let items: T[] = []; @@ -146,6 +148,22 @@ export function transformLSPCompletions( return response; } +function toCompletionList(reply: AnyCompletion | null): lsProtocol.CompletionList { + if (!reply) { + return { + items: [], + isIncomplete: false + }; + } + if (Array.isArray(reply)) { + return { + items: reply as lsProtocol.CompletionItem[], + isIncomplete: false + }; + } + return reply as lsProtocol.CompletionList; +} + /** * A LSP connector for completion handlers. */ @@ -158,6 +176,7 @@ export class LSPConnector private _context_connector: ContextConnector; private _kernel_connector: KernelConnector; private _kernel_and_context_connector: CompletionConnector; + private _isIncomplete: boolean; private console: ILSPLogConsole; // signal that this is the new type connector (providing completion items) @@ -272,13 +291,21 @@ export class LSPConnector )!; } + /** + * @deprecated; instead of using a global state like this, + * the latest reply should be cached by completer and this info extracted? + */ + get isIncomplete() { + return this._isIncomplete; + } + /** * Fetch completion requests. * * @param request - The completion request text and details. */ async fetch( - request: CompletionHandler.IRequest + requestIn?: CompletionHandler.IRequest ): Promise { let editor = this._editor; @@ -311,6 +338,12 @@ export class LSPConnector let position_in_token = cursor.column - start.column - 1; const typed_character = token.value[cursor.column - start.column - 1]; + const request: CompletionHandler.IRequest + = requestIn ? requestIn : { + offset: token.offset + position_in_token + 1, + text: editor.model.value.text + }; + let start_in_root = this.transform_from_editor_to_root(start); let end_in_root = this.transform_from_editor_to_root(end); let cursor_in_root = this.transform_from_editor_to_root(cursor); @@ -455,20 +488,23 @@ export class LSPConnector ? CompletionTriggerKind.Invoked : this.trigger_kind; - let lspCompletionItems = ((await connection.getCompletion( - cursor, - { - start, - end, - text: token.value + let completionReply = toCompletionList(await connection.clientRequests['textDocument/completion'].request( + { + textDocument: { + uri: document.document_info.uri }, - document.document_info, - false, - typed_character, - trigger_kind - )) || []) as lsProtocol.CompletionItem[]; + position: { + line: cursor.line, + character: cursor.ch + }, + context: { + triggerKind: trigger_kind || CompletionTriggerKind.Invoked, + triggerCharacter: typed_character + } + })); + this._isIncomplete = completionReply.isIncomplete; - this.console.debug('Transforming'); + let lspCompletionItems: lsProtocol.CompletionItem[] = completionReply.items || []; return transformLSPCompletions( token, diff --git a/packages/jupyterlab-lsp/src/features/completion/model.ts b/packages/jupyterlab-lsp/src/features/completion/model.ts index 0ce6e8664..d1ef773d5 100644 --- a/packages/jupyterlab-lsp/src/features/completion/model.ts +++ b/packages/jupyterlab-lsp/src/features/completion/model.ts @@ -3,6 +3,7 @@ import { CompleterModel, CompletionHandler } from '@jupyterlab/completer'; import { StringExt } from '@lumino/algorithm'; +import { Signal } from '@lumino/signaling'; import { LazyCompletionItem } from './item'; @@ -41,7 +42,6 @@ export class GenericCompleterModel< this.query = ''; let unfilteredItems = super.completionItems!() as T[]; this.query = query; - // always want to sort // TODO does this behave strangely with %% if always sorting? return this._sortAndFilter(query, unfilteredItems); @@ -208,6 +208,27 @@ export namespace GenericCompleterModel { } export class LSPCompleterModel extends GenericCompleterModel { + queryChanged: Signal; + private _lastQuery: string; + + constructor(settings: GenericCompleterModel.IOptions = {}) { + super(settings); + this.queryChanged = new Signal(this); + this.stateChanged.connect(this.onStateChanged.bind(this)) + } + + onStateChanged() { + // TODO: this does not get called for the second time. + // It seems that there is a condition in completer usptream which is not met. + + // we will bail when query gets set to empty strings as these + // are to invoke setter side-effects. + if (this.query && this.query != this._lastQuery) { + this.queryChanged.emit(this.query); + this._lastQuery = this.query; + } + } + protected getFilterText(item: LazyCompletionItem): string { if (item.filterText) { return item.filterText; diff --git a/packages/jupyterlab-lsp/src/lsp.ts b/packages/jupyterlab-lsp/src/lsp.ts index e461e1d4d..2f6260b2e 100644 --- a/packages/jupyterlab-lsp/src/lsp.ts +++ b/packages/jupyterlab-lsp/src/lsp.ts @@ -13,6 +13,18 @@ export enum DiagnosticSeverity { Hint = 4 } + +export type AnyLocation = + | lsp.Location + | lsp.Location[] + | lsp.LocationLink[] + | undefined + | null; + +export type AnyCompletion = + | lsp.CompletionList + | lsp.CompletionItem[]; + export enum DiagnosticTag { Unnecessary = 1, Deprecated = 2 diff --git a/packages/lsp-ws-connection/src/types.ts b/packages/lsp-ws-connection/src/types.ts index 6bb0ff1ac..01b7d065f 100644 --- a/packages/lsp-ws-connection/src/types.ts +++ b/packages/lsp-ws-connection/src/types.ts @@ -18,6 +18,10 @@ export interface IDocumentInfo { languageId: string; } +/** + * @deprecated, moved to `@jupyter-lsp/jupyterlab-lsp/lsp.ts` + * (will become `@jupyterlab/lsp/lsp.ts` in near futurue) + */ export type AnyLocation = | lsProtocol.Location | lsProtocol.Location[] @@ -25,10 +29,15 @@ export type AnyLocation = | undefined | null; +/** + * @deprecated, moved to `@jupyter-lsp/jupyterlab-lsp/lsp.ts` + * (will become `@jupyterlab/lsp/lsp.ts` in near futurue) + */ export type AnyCompletion = | lsProtocol.CompletionList | lsProtocol.CompletionItem[]; + export enum CompletionTriggerKind { Invoked = 1, TriggerCharacter = 2,