@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
265 lines (233 loc) • 8.13 kB
text/typescript
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { CodeEditor } from '@jupyterlab/codeeditor';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
import { LabIcon } from '@jupyterlab/ui-components';
import { Debouncer } from '@lumino/polling';
import type * as CodeMirror from 'codemirror';
import type * as lsProtocol from 'vscode-languageserver-protocol';
import highlightTypeSvg from '../../style/icons/highlight-type.svg';
import highlightSvg from '../../style/icons/highlight.svg';
import { CodeHighlights as LSPHighlightsSettings } from '../_highlights';
import { CodeMirrorIntegration } from '../editor_integration/codemirror';
import { FeatureSettings, IFeatureCommand } from '../feature';
import { DocumentHighlightKind } from '../lsp';
import { IRootPosition, IVirtualPosition } from '../positioning';
import { ILSPFeatureManager, PLUGIN_ID } from '../tokens';
import { VirtualDocument } from '../virtual/document';
export const highlightIcon = new LabIcon({
name: 'lsp:highlight',
svgstr: highlightSvg
});
export const highlightTypeIcon = new LabIcon({
name: 'lsp:highlight-type',
svgstr: highlightTypeSvg
});
const COMMANDS = (trans: TranslationBundle): IFeatureCommand[] => [
{
id: 'highlight-references',
execute: ({ connection, virtual_position, document }) =>
connection?.getReferences(virtual_position, document.document_info),
is_enabled: ({ connection }) =>
connection ? connection.isReferencesSupported() : false,
label: trans.__('Highlight references'),
icon: highlightIcon
},
{
id: 'highlight-type-definition',
execute: ({ connection, virtual_position, document }) =>
connection?.getTypeDefinition(virtual_position, document.document_info),
is_enabled: ({ connection }) =>
connection ? connection.isTypeDefinitionSupported() : false,
label: trans.__('Highlight type definition'),
icon: highlightTypeIcon
}
];
export class HighlightsCM extends CodeMirrorIntegration {
protected highlight_markers: CodeMirror.TextMarker[] = [];
private debounced_get_highlight: Debouncer<
lsProtocol.DocumentHighlight[] | undefined
>;
private virtual_position: IVirtualPosition;
private sent_version: number;
private last_token: CodeEditor.IToken | null = null;
get settings() {
return super.settings as FeatureSettings<LSPHighlightsSettings>;
}
register(): void {
this.debounced_get_highlight = this.create_debouncer();
this.settings.changed.connect(() => {
this.debounced_get_highlight = this.create_debouncer();
});
this.editor_handlers.set('cursorActivity', this.onCursorActivity);
this.editor_handlers.set('blur', this.onBlur);
this.editor_handlers.set('focus', this.onCursorActivity);
super.register();
}
protected onBlur = () => {
if (this.settings.composite.removeOnBlur) {
this.clear_markers();
this.last_token = null;
} else {
this.onCursorActivity().catch(console.warn);
}
};
remove(): void {
this.clear_markers();
super.remove();
}
protected clear_markers() {
for (let marker of this.highlight_markers) {
marker.clear();
}
this.highlight_markers = [];
}
protected handleHighlight = (
items: lsProtocol.DocumentHighlight[] | undefined
) => {
this.clear_markers();
if (!items) {
return;
}
for (let item of items) {
let range = this.range_to_editor_range(item.range);
let kind_class = item.kind
? 'cm-lsp-highlight-' + DocumentHighlightKind[item.kind]
: '';
let marker = this.highlight_range(
range,
'cm-lsp-highlight ' + kind_class
);
this.highlight_markers.push(marker);
}
};
protected create_debouncer() {
return new Debouncer<lsProtocol.DocumentHighlight[] | undefined>(
this.on_cursor_activity,
this.settings.composite.debouncerDelay
);
}
protected on_cursor_activity = async () => {
this.sent_version = this.virtual_document.document_info.version;
return await this.connection.getDocumentHighlights(
this.virtual_position,
this.virtual_document.document_info,
false
);
};
protected onCursorActivity = async () => {
if (!this.virtual_editor?.virtual_document?.document_info) {
return;
}
let root_position: IRootPosition;
await this.virtual_editor.virtual_document.update_manager.update_done;
try {
root_position = this.virtual_editor
.getDoc()
.getCursor('start') as IRootPosition;
} catch (err) {
this.console.warn('no root position available');
return;
}
if (root_position == null) {
this.console.warn('no root position available');
return;
}
const token = this.virtual_editor.get_token_at(root_position);
// if token has not changed, no need to update highlight, unless it is an empty token
// which would indicate that the cursor is at the first character
if (
this.last_token &&
token.value === this.last_token.value &&
token.value !== ''
) {
this.console.log(
'not requesting highlights (token did not change)',
token
);
return;
}
let document: VirtualDocument;
try {
document = this.virtual_editor.document_at_root_position(root_position);
} catch (e) {
this.console.warn(
'Could not obtain virtual document from position',
root_position
);
return;
}
if (document !== this.virtual_document) {
return;
}
try {
let virtual_position =
this.virtual_editor.root_position_to_virtual_position(root_position);
this.virtual_position = virtual_position;
Promise.all([
// request the highlights as soon as possible
this.debounced_get_highlight.invoke(),
// and in the meantime remove the old markers
async () => {
this.clear_markers();
this.last_token = null;
}
])
.then(([highlights]) => {
// in the time the response returned the document might have been closed - check that
if (this.virtual_document.isDisposed) {
return;
}
let version_after = this.virtual_document.document_info.version;
/// if document was updated since (e.g. user pressed delete - token change, but position did not)
if (version_after !== this.sent_version) {
this.console.log(
'skipping highlights response delayed by ' +
(version_after - this.sent_version) +
' document versions'
);
return;
}
// if cursor position changed (e.g. user moved cursor up - position has changed, but document version did not)
if (virtual_position !== this.virtual_position) {
this.console.log(
'skipping highlights response: cursor moved since it was requested'
);
return;
}
this.handleHighlight(highlights);
this.last_token = token;
})
.catch(this.console.warn);
} catch (e) {
this.console.warn('Could not get highlights:', e);
}
};
}
const FEATURE_ID = PLUGIN_ID + ':highlights';
export const HIGHLIGHTS_PLUGIN: JupyterFrontEndPlugin<void> = {
id: FEATURE_ID,
requires: [ILSPFeatureManager, ISettingRegistry, ITranslator],
autoStart: true,
activate: (
app: JupyterFrontEnd,
featureManager: ILSPFeatureManager,
settingRegistry: ISettingRegistry,
translator: ITranslator
) => {
const settings = new FeatureSettings(settingRegistry, FEATURE_ID);
const trans = translator.load('jupyterlab_lsp');
featureManager.register({
feature: {
editorIntegrationFactory: new Map([['CodeMirrorEditor', HighlightsCM]]),
id: FEATURE_ID,
name: 'LSP Highlights',
settings: settings,
commands: COMMANDS(trans)
}
});
}
};