UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

243 lines 10.8 kB
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