UNPKG

@krassowski/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

345 lines 14.9 kB
import { CompleterModel } from '@jupyterlab/completer'; import { NotebookAdapter } from '../../adapters/notebook/notebook'; import { CodeMirrorIntegration } from '../../editor_integration/codemirror'; import { AdditionalCompletionTriggerKinds, CompletionTriggerKind } from '../../lsp'; import { LSPConnector } from './completion_handler'; import { LSPCompleterModel } from './model'; import { LSPCompletionRenderer } from './renderer'; const DOC_PANEL_SELECTOR = '.jp-Completer-docpanel'; const DOC_PANEL_PLACEHOLDER_CLASS = 'lsp-completer-placeholder'; const LSP_COMPLETER_CLASS = 'lsp-completer'; export class CompletionCM extends CodeMirrorIntegration { get settings() { return super.settings; } get completionCharacters() { if (this._completionCharacters == null || !this._completionCharacters.length) { this._completionCharacters = this.connection.getLanguageCompletionCharacters(); } return this._completionCharacters; } // public handleCompletion(completions: lsProtocol.CompletionItem[]) { // TODO: populate the (already displayed) completions list if the completions timed out initially? // } afterChange(change) { // TODO: maybe the completer could be kicked off in the handleChange() method directly; signature help still // requires an up-to-date virtual document on the LSP side, so we need to wait for sync. // note: trigger character completion need to be have a higher priority than auto-invoked completion // because the latter does not work for on-dot completion due to suppression of trivial suggestions // see gh430 let last_character = this.extract_last_character(change); if (this.completionCharacters.indexOf(last_character) > -1) { this.virtual_editor.console.log('Will invoke completer after', last_character); this.feature.labIntegration .invoke_completer(CompletionTriggerKind.TriggerCharacter) .catch(this.console.warn); return; } if (change.text && change.text[0].length == 1 && this.settings.composite.continuousHinting) { this.feature.labIntegration .invoke_completer(AdditionalCompletionTriggerKinds.AutoInvoked) .catch(this.console.warn); } } } export class CompletionLabIntegration { constructor(app, completionManager, settings, adapterManager, completionThemeManager, console, renderMimeRegistry) { this.app = app; this.completionManager = completionManager; this.settings = settings; this.adapterManager = adapterManager; this.completionThemeManager = completionThemeManager; this.console = console; this.renderMimeRegistry = renderMimeRegistry; this.current_adapter = null; this._latestActiveItem = null; this._signalConnected = false; const markdown_renderer = this.renderMimeRegistry.createRenderer('text/markdown'); this.renderer = new LSPCompletionRenderer({ integrator: this, markdownRenderer: markdown_renderer, latexTypesetter: this.renderMimeRegistry.latexTypesetter, console: console.scope('renderer') }); this.renderer.activeChanged.connect(this.active_completion_changed, this); this.renderer.itemShown.connect(this.resolve_and_update, this); this._signalConnected = false; this._disabled = false; // TODO: figure out a better way to disable lab integration elements (postpone initialization?) settings.ready .then(() => { this._disabled = settings.composite.disable; if (!settings.composite.disable) { adapterManager.adapterChanged.connect(this.swap_adapter, this); this._signalConnected = true; } }) .catch(console.warn); settings.changed.connect(() => { this._disabled = settings.composite.disable; if (!settings.composite.disable) { document.body.dataset.lspCompleterLayout = this.settings.composite.layout; completionThemeManager.set_theme(this.settings.composite.theme); completionThemeManager.set_icons_overrides(this.settings.composite.typesMap); if (!this._signalConnected) { adapterManager.adapterChanged.connect(this.swap_adapter, this); this._signalConnected = true; } } else { completionThemeManager.set_theme(null); delete document.body.dataset.lspCompleterLayout; } if (this.current_completion_handler && this.model instanceof LSPCompleterModel) { this.model.settings.caseSensitive = this.settings.composite.caseSensitive; this.model.settings.includePerfectMatches = this.settings.composite.includePerfectMatches; this.model.settings.preFilterMatches = this.settings.composite.preFilterMatches; } }); } fetchDocumentation(item) { if (!item) { return; } item .resolve() .then(resolvedCompletionItem => { if (item.self !== this._latestActiveItem.self) { return; } this.set_doc_panel_placeholder(false); if (resolvedCompletionItem === null) { return; } this.refresh_doc_panel(item); }) .catch(e => { // disabling placeholder can remove currently displayed documentation, // so only do that if this is really the active item! if (item.self === this._latestActiveItem.self) { this.set_doc_panel_placeholder(false); } this.console.warn(e); }); } active_completion_changed(renderer, active_completion) { let { item } = active_completion; this._latestActiveItem = item; if (!item.supportsResolution()) { if (item.isDocumentationMarkdown) { // TODO: remove once https://github.com/jupyterlab/jupyterlab/pull/9663 is merged and released this.refresh_doc_panel(item); } return; } if (item.needsResolution()) { this.set_doc_panel_placeholder(true); this.fetchDocumentation(item); } else if (item.isResolved()) { this.refresh_doc_panel(item); } else { // resolution has already started, but the re-render update could have been invalidated // by user action, so let's ensure the documentation will get shown this time. this.fetchDocumentation(item); } // also fetch completion for the previous and the next item to prevent jitter const index = this.current_index; const items = this.current_items; if (index - 1 >= 0) { const previous = items[index - 1]; this.resolve_and_update_from_item(previous === null || previous === void 0 ? void 0 : previous.self); } if (index + 1 < items.length) { const next = items[index + 1]; this.resolve_and_update_from_item(next === null || next === void 0 ? void 0 : next.self); } } resolve_and_update_from_item(item) { if (!item) { return; } this.resolve_and_update(this.renderer, { item: item, element: item.element }); } resolve_and_update(renderer, active_completion) { let { item, element } = active_completion; if (!item.supportsResolution()) { this.renderer.updateExtraInfo(item, element); return; } if (item.isResolved()) { this.renderer.updateExtraInfo(item, element); } else { // supportsResolution as otherwise would short-circuit above item .resolve() .then(resolvedCompletionItem => { this.renderer.updateExtraInfo(item, element); }) .catch(e => { this.console.warn(e); }); } } swap_adapter(manager, adapter) { if (this.current_adapter) { // disconnect signals from the old adapter this.current_adapter.activeEditorChanged.disconnect(this.set_connector, this); this.current_adapter.adapterConnected.disconnect(this.connect_completion, this); } this.current_adapter = adapter; // connect the new adapter if (this.current_adapter.isConnected) { this.connect_completion(this.current_adapter); // TODO: what to do if adapter.activeEditor was just deleted/there is none because focus shifted? this.set_connector(adapter, { editor: adapter.activeEditor }); } // connect signals to the new adapter this.current_adapter.activeEditorChanged.connect(this.set_connector, this); this.current_adapter.adapterConnected.connect(this.connect_completion, this); } connect_completion(adapter, data) { let editor = adapter.activeEditor; if (editor == null) { return; } this.set_completion_connector(adapter, editor); this.current_completion_handler = this.completionManager.register({ connector: this.current_completion_connector, editor: editor, parent: adapter.widget }, this.renderer); let completer = this.completer; if (this._disabled) { completer.removeClass(LSP_COMPLETER_CLASS); completer.model = new CompleterModel(); } else { completer.addClass(LSP_COMPLETER_CLASS); completer.model = new LSPCompleterModel({ caseSensitive: this.settings.composite.caseSensitive, includePerfectMatches: this.settings.composite.includePerfectMatches, preFilterMatches: this.settings.composite.preFilterMatches }); } } get completer() { // TODO upstream: make completer public? return this.current_completion_handler.completer; } get model() { return this.completer.model; } invoke_completer(kind) { // TODO: ideally this would not re-trigger if list of items not isIncomplete let command; this.current_completion_connector.trigger_kind = kind; if (this.adapterManager.currentAdapter instanceof NotebookAdapter) { command = 'completer:invoke-notebook'; } else { command = 'completer:invoke-file'; } return this.app.commands.execute(command).catch(() => { this.current_completion_connector.trigger_kind = CompletionTriggerKind.Invoked; }); } set_connector(adapter, editor_changed) { if (!this.current_completion_handler) { // workaround for current_completion_handler not being there yet this.connect_completion(adapter); } this.set_completion_connector(adapter, editor_changed.editor); this.current_completion_handler.editor = editor_changed.editor; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.current_completion_handler.connector = this.current_completion_connector; } get current_items() { // TODO upstream: allow to get completionItems() without markup // (note: not trivial as _markup() does filtering too) if (!(this.model instanceof LSPCompleterModel)) { return []; } return this.model.completionItems(); } get current_index() { let completer = this.current_completion_handler.completer; // TODO: use public activeIndex available since 3.1 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return completer._activeIndex; } refresh_doc_panel(item) { let completer = this.current_completion_handler.completer; const active = this.current_items[this.current_index]; if (!item || !active || active.insertText != item.insertText) { return; } const docPanel = completer.node.querySelector(DOC_PANEL_SELECTOR); if (!docPanel) { this.console.warn('Could not find completer panel to refresh'); return; } docPanel.classList.remove(DOC_PANEL_PLACEHOLDER_CLASS); if (item.documentation) { // remove all children docPanel.textContent = ''; // TODO upstream: renderer should take care of the documentation rendering // sent PR: https://github.com/jupyterlab/jupyterlab/pull/9663 const node = this.renderer.createDocumentationNode(item); docPanel.appendChild(node); docPanel.setAttribute('style', ''); } else { docPanel.setAttribute('style', 'display: none'); } } set_doc_panel_placeholder(enable) { let completer = this.current_completion_handler.completer; const docPanel = completer.node.querySelector(DOC_PANEL_SELECTOR); if (!docPanel) { this.console.warn('Could not find completer panel for placeholder'); return; } if (enable) { docPanel.setAttribute('style', ''); docPanel.classList.add(DOC_PANEL_PLACEHOLDER_CLASS); } else if (docPanel.classList.contains(DOC_PANEL_PLACEHOLDER_CLASS)) { docPanel.setAttribute('style', 'display: none'); docPanel.classList.remove(DOC_PANEL_PLACEHOLDER_CLASS); } } set_completion_connector(adapter, editor) { var _a, _b; this.current_completion_connector = new LSPConnector({ editor: editor, themeManager: this.completionThemeManager, connections: this.current_adapter.connection_manager.connections, virtual_editor: this.current_adapter.virtual_editor, settings: this.settings, labIntegration: this, // it might or might not be a notebook panel (if it is not, the sessionContext and session will just be undefined) session: (_b = (_a = this.current_adapter.widget) === null || _a === void 0 ? void 0 : _a.sessionContext) === null || _b === void 0 ? void 0 : _b.session, console: this.console }); } } //# sourceMappingURL=completion.js.map