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; } 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( 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 = { 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) } }); } };