@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
345 lines • 14.9 kB
JavaScript
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