@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
243 lines • 10.8 kB
JavaScript
import { EditorView } from '@codemirror/view';
import { EditorExtensionRegistry } from '@jupyterlab/codemirror';
import { ILSPFeatureManager, ILSPDocumentConnectionManager } from '@jupyterlab/lsp';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { LabIcon } from '@jupyterlab/ui-components';
import { Debouncer } from '@lumino/polling';
import highlightSvg from '../../style/icons/highlight.svg';
import { PositionConverter, rootPositionToVirtualPosition, editorPositionToRootPosition, documentAtRootPosition, rangeToEditorRange } from '../converter';
import { FeatureSettings, Feature } from '../feature';
import { DocumentHighlightKind } from '../lsp';
import { createMarkManager } from '../marks';
import { PLUGIN_ID } from '../tokens';
import { BrowserConsole } from '../virtual/console';
export const highlightIcon = new LabIcon({
name: 'lsp:highlight',
svgstr: highlightSvg
});
export class HighlightsFeature extends Feature {
constructor(options) {
super(options);
this.capabilities = {
textDocument: {
documentHighlight: {
dynamicRegistration: true
}
}
};
this.id = HighlightsFeature.id;
this.console = new BrowserConsole().scope('Highlights');
this._lastToken = null;
this.requestHighlights = async (virtualDocument, virtualPosition) => {
const connection = this.connectionManager.connections.get(virtualDocument.uri);
if (!(connection.isReady &&
connection.serverCapabilities.documentHighlightProvider)) {
return null;
}
this._versionSent = virtualDocument.documentInfo.version;
return await connection.clientRequests['textDocument/documentHighlight'].request({
textDocument: {
uri: virtualDocument.documentInfo.uri
},
position: {
line: virtualPosition.line,
character: virtualPosition.ch
}
});
};
this.settings = options.settings;
this.markManager = createMarkManager({
[DocumentHighlightKind.Text]: { class: 'cm-lsp-highlight-Text' },
[DocumentHighlightKind.Read]: { class: 'cm-lsp-highlight-Read' },
[DocumentHighlightKind.Write]: { class: 'cm-lsp-highlight-Write' }
});
this._debouncedGetHighlight = this.createDebouncer();
this.settings.changed.connect(() => {
this._debouncedGetHighlight = this.createDebouncer();
});
this.extensionFactory = {
name: 'lsp:highlights',
factory: factoryOptions => {
const { editor: editorAccessor, widgetAdapter: adapter } = factoryOptions;
const updateListener = EditorView.updateListener.of(viewUpdate => {
if (viewUpdate.docChanged || viewUpdate.selectionSet) {
this.onCursorActivity(editorAccessor, adapter).catch(this.console.warn);
}
});
const eventListeners = EditorView.domEventHandlers({
blur: (_, view) => {
this.onBlur(view);
},
focus: () => {
this.onCursorActivity(editorAccessor, adapter).catch(this.console.warn);
},
keydown: () => {
this.onCursorActivity(editorAccessor, adapter).catch(this.console.warn);
}
});
return EditorExtensionRegistry.createImmutableExtension([
updateListener,
eventListeners
]);
}
};
}
onBlur(view) {
if (this.settings.composite.removeOnBlur) {
// Delayed evaluation to avoid error:
// `Error: Calls to EditorView.update are not allowed while an update is in progress`
setTimeout(() => {
this.markManager.clearEditorMarks(view);
this._lastToken = null;
}, 0);
}
}
handleHighlight(items, adapter, document) {
this.markManager.clearAllMarks();
if (!items) {
return;
}
const highlightsByEditor = new Map();
for (let item of items) {
let range = rangeToEditorRange(adapter, item.range, null, document);
const editor = range.editor;
let optionsList = highlightsByEditor.get(editor);
if (!optionsList) {
optionsList = [];
highlightsByEditor.set(editor, optionsList);
}
optionsList.push({
kind: item.kind || DocumentHighlightKind.Text,
from: editor.getOffsetAt(PositionConverter.cm_to_ce(range.start)),
to: editor.getOffsetAt(PositionConverter.cm_to_ce(range.end))
});
}
for (const [editor, markerDefinitions] of highlightsByEditor.entries()) {
// CodeMirror5 performance test cases:
// - one cell with 1000 `math.pi` and `import math`; move cursor to `math`,
// wait for 1000 highlights, then move to `pi`:
// - step-by-step:
// - highlight `math`: 13.1s
// - then highlight `pi`: 16.6s
// - operation():
// - highlight `math`: 160ms
// - then highlight `pi`: 227ms
// - CodeMirror6, measuring `markManager.putMarks`:
// - highlight `math`: 181ms
// - then highlight `pi`: 334ms
// - 100 cells with `math.pi` and one with `import math`; move cursor to `math`,
// wait for 1000 highlights, then move to `pi` (this is overhead control,
// no gains expected):
// - step-by-step:
// - highlight `math`: 385ms
// - then highlight `pi`: 683 ms
// - operation():
// - highlight `math`: 390ms
// - then highlight `pi`: 870ms
const editorView = editor.editor;
this.markManager.putMarks(editorView, markerDefinitions);
}
}
createDebouncer() {
return new Debouncer(this.requestHighlights, this.settings.composite.debouncerDelay);
}
async onCursorActivity(editorAccessor, adapter) {
if (!adapter.virtualDocument) {
this.console.log('virtualDocument not ready on adapter');
return;
}
await adapter.virtualDocument.updateManager.updateDone;
const editor = editorAccessor.getEditor();
if (!editor) {
this.console.log('editor not found ready');
return;
}
const position = editor.getCursorPosition();
const editorPosition = PositionConverter.ce_to_cm(position);
const rootPosition = editorPositionToRootPosition(adapter, editorAccessor, editorPosition);
if (!rootPosition) {
this.console.debug('Root position not available');
return;
}
const document = documentAtRootPosition(adapter, rootPosition);
if (!document.documentInfo) {
this.console.debug('Root document lacks document info');
return;
}
const offset = editor.getOffsetAt(PositionConverter.cm_to_ce(editorPosition));
const token = editor.getTokenAt(offset);
// 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; we also need to check
// adapter in case if user switched between documents/notebooks.
if (this._lastToken &&
token.value === this._lastToken.token.value &&
adapter === this._lastToken.adapter &&
token.value !== '') {
this.console.log('not requesting highlights (token did not change)', token);
return;
}
try {
const virtualPosition = rootPositionToVirtualPosition(adapter, rootPosition);
this._virtualPosition = virtualPosition;
const [highlights] = await Promise.all([
// request the highlights as soon as possible
this._debouncedGetHighlight.invoke(document, virtualPosition),
// and in the meantime remove the old markers
async () => {
this.markManager.clearAllMarks();
this._lastToken = null;
}
]);
// in the time the response returned the document might have been closed - check that
if (document.isDisposed) {
return;
}
let versionAfter = document.documentInfo.version;
/// if document was updated since (e.g. user pressed delete - token change, but position did not)
if (versionAfter !== this._versionSent) {
this.console.log('skipping highlights response delayed by ' +
(versionAfter - this._versionSent) +
' document versions');
return;
}
// if cursor position changed (e.g. user moved cursor up - position has changed, but document version did not)
if (virtualPosition !== this._virtualPosition) {
this.console.log('skipping highlights response: cursor moved since it was requested');
return;
}
this.handleHighlight(highlights, adapter, document);
this._lastToken = {
token,
adapter
};
}
catch (e) {
this.console.warn('Could not get highlights:', e);
}
}
}
(function (HighlightsFeature) {
HighlightsFeature.id = PLUGIN_ID + ':highlights';
})(HighlightsFeature || (HighlightsFeature = {}));
export const HIGHLIGHTS_PLUGIN = {
id: HighlightsFeature.id,
requires: [
ILSPFeatureManager,
ISettingRegistry,
ILSPDocumentConnectionManager
],
autoStart: true,
activate: async (app, featureManager, settingRegistry, connectionManager) => {
const settings = new FeatureSettings(settingRegistry, HighlightsFeature.id);
await settings.ready;
if (settings.composite.disable) {
return;
}
const feature = new HighlightsFeature({
settings,
connectionManager
});
featureManager.register(feature);
}
};
//# sourceMappingURL=highlights.js.map