diff --git a/packages/jupyterlab-lsp/src/command_manager.ts b/packages/jupyterlab-lsp/src/command_manager.ts index b17d5fc3d..bc8b1ef21 100644 --- a/packages/jupyterlab-lsp/src/command_manager.ts +++ b/packages/jupyterlab-lsp/src/command_manager.ts @@ -149,6 +149,11 @@ export class ContextAssembler { top: number, adapter: WidgetLSPAdapter ): IRootPosition | null { + // TODO: this relies on the editor under the cursor being the active editor; + // this may not be the case. Instead we should find editor from DOM and then + // have a magic way of transforming editor instances into CodeEditor and Document.Editor + // a naive way is to iterate all the editors in adapter (=cells) and see which one matches + // but the predicate is expensive and fallible in windowed notebook. const editorAccessor = adapter.activeEditor; if (!editorAccessor) { diff --git a/packages/jupyterlab-lsp/src/converter.ts b/packages/jupyterlab-lsp/src/converter.ts index 9a390ac34..7149114fd 100644 --- a/packages/jupyterlab-lsp/src/converter.ts +++ b/packages/jupyterlab-lsp/src/converter.ts @@ -34,7 +34,13 @@ export class PositionConverter { } } -/** TODO should it be wrapped into an object? */ +/** TODO should it be wrapped into an object? Where should these live? */ + +export interface IEditorRange { + start: IEditorPosition; + end: IEditorPosition; + editor: CodeEditor.IEditor; +} export function documentAtRootPosition( adapter: WidgetLSPAdapter, @@ -116,3 +122,34 @@ export function editorPositionToRootPosition( } return adapter.virtualDocument.transformFromEditorToRoot(editor, position); } + +export function rangeToEditorRange( + adapter: WidgetLSPAdapter, + range: lsProtocol.Range, + editor: CodeEditor.IEditor | null +): IEditorRange { + let start = PositionConverter.lsp_to_cm(range.start) as IVirtualPosition; + let end = PositionConverter.lsp_to_cm(range.end) as IVirtualPosition; + + let startInRoot = virtualPositionToRootPosition(adapter, start); + if (!startInRoot) { + throw Error('Could not determine position in root'); + } + + if (editor == null) { + let editorAccessor = editorAtRootPosition(adapter, startInRoot); + const candidate = editorAccessor.getEditor(); + if (!candidate) { + throw Error('Editor could not be accessed'); + } + editor = candidate; + } + + const document = documentAtRootPosition(adapter, startInRoot); + + return { + start: document.transformVirtualToEditor(start)!, + end: document.transformVirtualToEditor(end)!, + editor: editor + }; +} diff --git a/packages/jupyterlab-lsp/src/editor_integration/codemirror.ts b/packages/jupyterlab-lsp/src/editor_integration/codemirror.ts deleted file mode 100644 index fb2c50b53..000000000 --- a/packages/jupyterlab-lsp/src/editor_integration/codemirror.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CodeEditor } from '@jupyterlab/codeeditor'; -import { IEditorPosition } from '@jupyterlab/lsp'; - -export interface IEditorRange { - start: IEditorPosition; - end: IEditorPosition; - editor: CodeEditor.IEditor; -} diff --git a/packages/jupyterlab-lsp/src/features/highlights.ts b/packages/jupyterlab-lsp/src/features/highlights.ts index 6e0482fcf..1817590df 100644 --- a/packages/jupyterlab-lsp/src/features/highlights.ts +++ b/packages/jupyterlab-lsp/src/features/highlights.ts @@ -1,217 +1,303 @@ -/* +import { EditorView } from '@codemirror/view'; import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { CodeEditor } from '@jupyterlab/codeeditor'; +import { + CodeMirrorEditor, + IEditorExtensionRegistry, + EditorExtensionRegistry +} from '@jupyterlab/codemirror'; +import { + IVirtualPosition, + ILSPFeatureManager, + IEditorPosition, + ILSPDocumentConnectionManager, + WidgetLSPAdapter, + VirtualDocument +} from '@jupyterlab/lsp'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; 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 highlightSvg from '../../style/icons/highlight.svg'; import { CodeHighlights as LSPHighlightsSettings } from '../_highlights'; -import { CodeMirrorIntegration } from '../editor_integration/codemirror'; -import { FeatureSettings } from '../feature'; -import { DocumentHighlightKind } from '../lsp'; +import { ContextAssembler } from '../command_manager'; import { - IEditorPosition, - IRootPosition, - IVirtualPosition -} from '../positioning'; -import { ILSPFeatureManager, PLUGIN_ID } from '../tokens'; -import { VirtualDocument } from '../virtual/document'; + PositionConverter, + rootPositionToVirtualPosition, + editorPositionToRootPosition, + documentAtRootPosition, + rangeToEditorRange +} from '../converter'; +import { FeatureSettings, Feature } from '../feature'; +import { DocumentHighlightKind } from '../lsp'; +import { createMarkManager, ISimpleMarkManager } from '../marks'; +import { PLUGIN_ID } from '../tokens'; +import { BrowserConsole } from '../virtual/console'; export const highlightIcon = new LabIcon({ name: 'lsp:highlight', svgstr: highlightSvg }); -interface IHighlightDefinition { - options: CodeMirror.TextMarkerOptions; - start: IEditorPosition; - end: IEditorPosition; +interface IEditorHighlight { + kind: DocumentHighlightKind; + from: number; + to: number; } -export class HighlightsCM extends CodeMirrorIntegration { - protected highlightMarkers: Map = - new Map(); - 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; +export class HighlightsFeature extends Feature { + readonly capabilities: lsProtocol.ClientCapabilities = { + textDocument: { + documentHighlight: { + dynamicRegistration: true + } + } + }; + readonly id = HighlightsFeature.id; - get settings() { - return super.settings as FeatureSettings; - } + protected settings: FeatureSettings; + protected markManager: ISimpleMarkManager; + protected console = new BrowserConsole().scope('Highlights'); - register(): void { - this.debounced_get_highlight = this.create_debouncer(); + private _debouncedGetHighlight: Debouncer< + lsProtocol.DocumentHighlight[] | null, + void, + [VirtualDocument, IVirtualPosition] + >; + private _virtualPosition: IVirtualPosition; + private _versionSent: number; + private _lastToken: CodeEditor.IToken | null = null; + + constructor(options: HighlightsFeature.IOptions) { + super(options); + this.settings = options.settings; + const connectionManager = options.connectionManager; + 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.create_debouncer(); this.settings.changed.connect(() => { - this.debounced_get_highlight = this.create_debouncer(); + this._debouncedGetHighlight = 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(); + options.editorExtensionRegistry.addExtension({ + name: 'lsp:highlights', + factory: options => { + const updateListener = EditorView.updateListener.of(viewUpdate => { + if ( + viewUpdate.docChanged || + viewUpdate.selectionSet || + viewUpdate.focusChanged + ) { + // TODO how to get adapter here? + // this.onCursorActivity(); + const adapter = [...connectionManager.adapters.values()].find( + adapter => adapter.widget.node.contains(viewUpdate.view.dom) + ); + if (!adapter) { + this.console.warn('Adapter not found'); + return; + } + this.onCursorActivity(adapter); + } + }); + const eventListeners = EditorView.domEventHandlers({ + blur: (e, view) => { + this.onBlur(view); + }, + focus: event => { + const adapter = [...connectionManager.adapters.values()].find( + adapter => + adapter.widget.node.contains( + event.currentTarget! as HTMLElement + ) + ); + if (!adapter) { + this.console.warn('Adapter not found'); + return; + } + this.onCursorActivity(adapter); + }, + keydown: event => { + const adapter = [...connectionManager.adapters.values()].find( + adapter => + adapter.widget.node.contains( + event.currentTarget! as HTMLElement + ) + ); + if (!adapter) { + this.console.warn('Adapter not found'); + return; + } + this.onCursorActivity(adapter); + } + }); + return EditorExtensionRegistry.createImmutableExtension([ + updateListener, + eventListeners + ]); + } + }); } - protected clear_markers() { - for (const [cmEditor, markers] of this.highlightMarkers.entries()) { - cmEditor.operation(() => { - for (const marker of markers) { - marker.clear(); - } - }); - } - this.highlightMarkers = new Map(); - this.highlight_markers = []; + protected onBlur(view: EditorView) { + if (this.settings.composite.removeOnBlur) { + this.markManager.clearEditorMarks(view); + this._lastToken = null; + } // else { + // this.onCursorActivity().catch(console.warn); + //} } - protected handleHighlight = ( - items: lsProtocol.DocumentHighlight[] | undefined - ) => { - this.clear_markers(); + protected handleHighlight( + items: lsProtocol.DocumentHighlight[] | null, + adapter: WidgetLSPAdapter + ) { + this.markManager.clearAllMarks(); if (!items) { return; } - const highlightOptionsByEditor = new Map< - CodeMirror.Editor, - IHighlightDefinition[] + const highlightsByEditor = new Map< + CodeEditor.IEditor, + IEditorHighlight[] >(); 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 range = rangeToEditorRange(adapter, item.range, null); + const editor = range.editor; - let optionsList = highlightOptionsByEditor.get(range.editor); + let optionsList = highlightsByEditor.get(editor); if (!optionsList) { optionsList = []; - highlightOptionsByEditor.set(range.editor, optionsList); + highlightsByEditor.set(editor, optionsList); } + optionsList.push({ - options: { - className: 'cm-lsp-highlight ' + kind_class - }, - start: range.start, - end: range.end + 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 [ - cmEditor, - markerDefinitions - ] of highlightOptionsByEditor.entries()) { - // note: using `operation()` significantly improves performance. - // test cases: + 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`: - // - before: + // - step-by-step: // - highlight `math`: 13.1s // - then highlight `pi`: 16.6s - // - after: + // - operation(): // - highlight `math`: 160ms // - then highlight `pi`: 227ms // - 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): - // - before: + // - step-by-step: // - highlight `math`: 385ms // - then highlight `pi`: 683 ms - // - after: + // - operation(): // - highlight `math`: 390ms // - then highlight `pi`: 870ms - cmEditor.operation(() => { - const doc = cmEditor.getDoc(); - const markersList: CodeMirror.TextMarker[] = []; - for (const definition of markerDefinitions) { - let marker; - try { - marker = doc.markText( - definition.start, - definition.end, - definition.options - ); - } catch (e) { - this.console.warn('Marking highlight failed:', definition, e); - return; - } - markersList.push(marker); - this.highlight_markers.push(marker); - } - this.highlightMarkers.set(cmEditor, markersList); - }); + + const editorView = (editor as CodeMirrorEditor).editor; + this.markManager.putMarks(editorView, markerDefinitions); } - }; + } protected create_debouncer() { - return new Debouncer( - this.on_cursor_activity, - this.settings.composite.debouncerDelay - ); + return new Debouncer< + lsProtocol.DocumentHighlight[] | null, + void, + [VirtualDocument, IVirtualPosition] + >(this.on_cursor_activity, this.settings.composite.debouncerDelay); } - protected on_cursor_activity = async () => { - this.sent_version = this.virtualDocument.document_info.version; - return await this.connection.getDocumentHighlights( - this.virtual_position, - this.virtualDocument.document_info, - false - ); + protected on_cursor_activity = async ( + virtualDocument: VirtualDocument, + virtualPosition: IVirtualPosition + ) => { + const connection = this.connectionManager.connections.get( + virtualDocument.uri + )!; + if ( + !( + connection.isReady && + // @ts-ignore TODO remove once upstream fix released + 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 + } + }); }; - protected onCursorActivity = async () => { - if (!this.virtual_editor?.virtualDocument?.document_info) { + protected async onCursorActivity(adapter: WidgetLSPAdapter) { + // TODO: use setTimeout(() => resolve(), 0) to make sure document is updated? + if (!adapter.virtualDocument) { + this.console.log('virtualDocument not ready on adapter'); return; } - let root_position: IRootPosition; + await adapter.virtualDocument!.updateManager.updateDone; + + // TODO: this is the same problem as in signature + // TODO: the assumption that updated editor = active editor will fail on RTC. How to get `CodeEditor.IEditor` and `Document.IEditor` from `EditorView`? we got `CodeEditor.IModel` from `options.model` but may need more context here. + const editorAccessor = adapter.activeEditor; + const editor = editorAccessor!.getEditor()!; + const position = editor.getCursorPosition(); + const editorPosition = PositionConverter.ce_to_cm( + position + ) as IEditorPosition; + + const rootPosition = editorPositionToRootPosition( + adapter, + editorAccessor!, + editorPosition + ); - await this.virtual_editor.virtualDocument.update_manager.update_done; - try { - root_position = this.virtual_editor - .getDoc() - .getCursor('start') as IRootPosition; - } catch (err) { - this.console.warn('no root position available'); + if (!rootPosition) { + this.console.debug('Root position not available'); return; } - if (root_position == null) { - this.console.warn('no root position available'); + const document = documentAtRootPosition(adapter, rootPosition); + + if (!document.documentInfo) { + this.console.debug('Root document lacks document info'); return; } - const token = this.virtual_editor.get_token_at(root_position); + 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 if ( - this.last_token && - token.value === this.last_token.value && + this._lastToken && + token.value === this._lastToken.value && token.value !== '' ) { this.console.log( @@ -221,91 +307,92 @@ export class HighlightsCM extends CodeMirrorIntegration { 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 + const virtualPosition = rootPositionToVirtualPosition( + adapter, + rootPosition ); - return; - } - if (document !== this.virtualDocument) { - return; - } + this._virtualPosition = virtualPosition; - try { - let virtual_position = - this.virtual_editor.root_position_to_virtual_position(root_position); - - this.virtual_position = virtual_position; - - Promise.all([ + const [highlights] = await Promise.all([ // request the highlights as soon as possible - this.debounced_get_highlight.invoke(), + this._debouncedGetHighlight.invoke(document, virtualPosition), // and in the meantime remove the old markers async () => { - this.clear_markers(); - this.last_token = null; + this.markManager.clearAllMarks(); + this._lastToken = null; } - ]) - .then(([highlights]) => { - // in the time the response returned the document might have been closed - check that - if (this.virtualDocument.isDisposed) { - return; - } + ]); - let version_after = this.virtualDocument.document_info.version; + // in the time the response returned the document might have been closed - check that + if (document.isDisposed) { + return; + } - /// 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; - } + let version_after = document.documentInfo.version; - this.handleHighlight(highlights); - this.last_token = token; - }) - .catch(this.console.warn); + /// if document was updated since (e.g. user pressed delete - token change, but position did not) + if (version_after !== this._versionSent) { + this.console.log( + 'skipping highlights response delayed by ' + + (version_after - 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); + this._lastToken = token; } catch (e) { this.console.warn('Could not get highlights:', e); } - }; + } } -const FEATURE_ID = PLUGIN_ID + ':highlights'; +export namespace HighlightsFeature { + export interface IOptions extends Feature.IOptions { + settings: FeatureSettings; + editorExtensionRegistry: IEditorExtensionRegistry; + contextAssembler: ContextAssembler; + } + export const id = PLUGIN_ID + ':highlights'; +} export const HIGHLIGHTS_PLUGIN: JupyterFrontEndPlugin = { - id: FEATURE_ID, - requires: [ILSPFeatureManager, ISettingRegistry], + id: HighlightsFeature.id, + requires: [ + ILSPFeatureManager, + ISettingRegistry, + IEditorExtensionRegistry, + ILSPDocumentConnectionManager + ], autoStart: true, - activate: ( + activate: async ( app: JupyterFrontEnd, featureManager: ILSPFeatureManager, - settingRegistry: ISettingRegistry + settingRegistry: ISettingRegistry, + editorExtensionRegistry: IEditorExtensionRegistry, + connectionManager: ILSPDocumentConnectionManager ) => { - const settings = new FeatureSettings(settingRegistry, FEATURE_ID); - - featureManager.register({ - feature: { - editorIntegrationFactory: new Map([['CodeMirrorEditor', HighlightsCM]]), - id: FEATURE_ID, - name: 'LSP Highlights', - settings: settings - } + const contextAssembler = new ContextAssembler({ app, connectionManager }); + const settings = new FeatureSettings( + settingRegistry, + HighlightsFeature.id + ); + await settings.ready; + const feature = new HighlightsFeature({ + settings, + editorExtensionRegistry, + connectionManager, + contextAssembler }); + featureManager.register(feature); } }; -*/ diff --git a/packages/jupyterlab-lsp/src/features/hover.ts b/packages/jupyterlab-lsp/src/features/hover.ts index 043822506..405c7239a 100644 --- a/packages/jupyterlab-lsp/src/features/hover.ts +++ b/packages/jupyterlab-lsp/src/features/hover.ts @@ -1,5 +1,4 @@ -import { StateField, StateEffect } from '@codemirror/state'; -import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; +import { EditorView } from '@codemirror/view'; import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -13,6 +12,7 @@ import { import { IRootPosition, IVirtualPosition, + IEditorPosition, ProtocolCoordinates, ILSPFeatureManager, isEqual, @@ -38,10 +38,11 @@ import { rootPositionToEditorPosition, editorPositionToRootPosition, editorAtRootPosition, - virtualPositionToRootPosition + rangeToEditorRange, + IEditorRange } from '../converter'; -import { IEditorRange } from '../editor_integration/codemirror'; import { FeatureSettings, Feature } from '../feature'; +import { createMarkManager, ISimpleMarkManager } from '../marks'; import { PLUGIN_ID } from '../tokens'; import { getModifierState } from '../utils'; import { BrowserConsole } from '../virtual/console'; @@ -132,76 +133,11 @@ function to_markup( } } -/** - * Manage marks in multiple editor views (e.g. cells). - */ -interface ISimpleMarkManager { - putMark(view: EditorView, from: number, to: number): void; - /** - * Clear marks from all editor views. - */ - clearAllMarks(): void; -} - -type MarkDecorationSpec = Parameters[0] & { - class: string; -}; - -function createMarkManager(spec: MarkDecorationSpec): ISimpleMarkManager { - const hoverMark = Decoration.mark(spec); - - const addHoverMark = StateEffect.define<{ from: number; to: number }>({ - map: ({ from, to }, change) => ({ - from: change.mapPos(from), - to: change.mapPos(to) - }) - }); - - const removeHoverMark = StateEffect.define(); - - const hoverMarkField = StateField.define({ - create() { - return Decoration.none; - }, - update(marks, tr) { - marks = marks.map(tr.changes); - for (let e of tr.effects) { - if (e.is(addHoverMark)) { - marks = marks.update({ - add: [hoverMark.range(e.value.from, e.value.to)] - }); - } else if (e.is(removeHoverMark)) { - marks = marks.update({ - filter: (from, to, value) => { - return value.spec['class'] !== spec.class; - } - }); - } - } - return marks; - }, - provide: f => EditorView.decorations.from(f) - }); - const views = new Set(); - - return { - putMark(view: EditorView, from: number, to: number) { - const effects: StateEffect[] = [addHoverMark.of({ from, to })]; - - if (!view.state.field(hoverMarkField, false)) { - effects.push(StateEffect.appendConfig.of([hoverMarkField])); - } - view.dispatch({ effects }); - views.add(view); - }, - clearAllMarks() { - for (let view of views) { - const effects: StateEffect[] = [removeHoverMark.of(null)]; - view.dispatch({ effects }); - } - views.clear(); - } - }; +interface IContext { + adapter: WidgetLSPAdapter; + token: CodeEditor.IToken; + editor: CodeEditor.IEditor; + editorAccessor: Document.IEditor; } export class HoverFeature extends Feature { @@ -221,12 +157,16 @@ export class HoverFeature extends Feature { protected lastHoverCharacter: IRootPosition | null = null; private last_hover_response: lsProtocol.Hover | null; protected hasMarker: boolean = false; - protected markManager: ISimpleMarkManager; + protected markManager: ISimpleMarkManager<'hover'>; private virtualPosition: IVirtualPosition; protected cache: ResponseCache; protected contextAssembler: ContextAssembler; - private debounced_get_hover: Throttler>; + private debouncedGetHover: Throttler< + Promise, + void, + [VirtualDocument, IVirtualPosition, IContext] + >; private tooltip: FreeTooltip; private _previousHoverRequest: Promise< Promise @@ -241,7 +181,9 @@ export class HoverFeature extends Feature { this.cache = new ResponseCache(10); const connectionManager = options.connectionManager; - this.markManager = createMarkManager({ class: 'cm-lsp-hover-available' }); + this.markManager = createMarkManager({ + hover: { class: 'cm-lsp-hover-available' } + }); options.editorExtensionRegistry.addExtension({ name: 'lsp:hover', @@ -297,11 +239,22 @@ export class HoverFeature extends Feature { } }); - this.debounced_get_hover = this.create_throttler(); + this.debouncedGetHover = this.create_throttler(); this.settings.changed.connect(() => { this.cache.maxSize = this.settings.composite.cacheSize; - this.debounced_get_hover = this.create_throttler(); + this.debouncedGetHover = this.create_throttler(); + }); + } + + protected create_throttler() { + return new Throttler< + Promise, + void, + [VirtualDocument, IVirtualPosition, IContext] + >(this.getHover, { + limit: this.settings.composite.throttlerDelay || 0, + edge: 'trailing' }); } @@ -374,13 +327,6 @@ export class HoverFeature extends Feature { } } - protected create_throttler() { - return new Throttler>(this.onHover, { - limit: this.settings.composite.throttlerDelay || 0, - edge: 'trailing' - }); - } - afterChange() { // reset cache on any change in the document this.cache.clean(); @@ -388,10 +334,10 @@ export class HoverFeature extends Feature { this.removeRangeHighlight(); } - protected onHover = async ( + protected getHover = async ( virtualDocument: VirtualDocument, virtualPosition: IVirtualPosition, - add_range_fn: (hover: lsProtocol.Hover) => lsProtocol.Hover + context: IContext ): Promise => { const connection = this.connectionManager.connections.get( virtualDocument.uri @@ -405,7 +351,9 @@ export class HoverFeature extends Feature { ) { return null; } - let hover = await connection.clientRequests['textDocument/hover'].request({ + let response = await connection.clientRequests[ + 'textDocument/hover' + ].request({ textDocument: { uri: virtualDocument.documentInfo.uri }, @@ -415,11 +363,26 @@ export class HoverFeature extends Feature { } }); - if (hover == null) { + if (response == null) { return null; } - return add_range_fn(hover); + if (typeof response.range !== 'undefined') { + return response; + } + // Harmonise response by adding range + const editorRange = this._getEditorRange( + context.adapter, + response, + context.token, + context.editor + ); + return this._addRange( + context.adapter, + response, + editorRange, + context.editorAccessor + ); }; protected static get_markup_for_hover( @@ -469,11 +432,13 @@ export class HoverFeature extends Feature { const range = responseData.editor_range; const editorView = (range.editor as CodeMirrorEditor).editor; - this.markManager.putMark( - editorView, - range.editor.getOffsetAt(PositionConverter.cm_to_ce(range.start)), - range.editor.getOffsetAt(PositionConverter.cm_to_ce(range.end)) + const from = range.editor.getOffsetAt( + PositionConverter.cm_to_ce(range.start) + ); + const to = range.editor.getOffsetAt( + PositionConverter.cm_to_ce(range.end) ); + this.markManager.putMarks(editorView, [{ from, to, kind: 'hover' }]); this.hasMarker = true; } @@ -614,26 +579,15 @@ export class HoverFeature extends Feature { if (responseData == null) { //const ceEditor = // editorAtRootPosition(adapter, rootPosition).getEditor()!; - const add_range_fn = (hover: lsProtocol.Hover): lsProtocol.Hover => { - const editor_range = this.get_editor_range( + const promise = this.debouncedGetHover.invoke( + document, + virtualPosition, + { adapter, - hover, - rootPosition, token, - editor - ); - return this.add_range_if_needed( - adapter, - hover, - editor_range, + editor, editorAccessor - ); - }; - - const promise = this.debounced_get_hover.invoke( - document, - virtualPosition, - add_range_fn + } ); this._previousHoverRequest = promise; let response = await promise; @@ -649,17 +603,17 @@ export class HoverFeature extends Feature { ) && this.is_useful_response(response) ) { - const editor_range = this.get_editor_range( + // TODO: I am reconstructing the range anyways - do I really want to ensure it in getHover? + const editorRange = this._getEditorRange( adapter, response, - rootPosition, token, editor ); responseData = { response: response, document: document, - editor_range: editor_range, + editor_range: editorRange, ceEditor: editor }; @@ -714,77 +668,44 @@ export class HoverFeature extends Feature { remove(): void { this.cache.clean(); this.removeRangeHighlight(); - this.debounced_get_hover.dispose(); + this.debouncedGetHover.dispose(); } - range_to_editor_range( - adapter: WidgetLSPAdapter, - range: lsProtocol.Range, - editor: CodeEditor.IEditor - ): IEditorRange { - let start = PositionConverter.lsp_to_cm(range.start) as IVirtualPosition; - let end = PositionConverter.lsp_to_cm(range.end) as IVirtualPosition; - - let startInRoot = virtualPositionToRootPosition(adapter, start); - if (!startInRoot) { - throw Error('Could not determine position in root'); - } - - if (editor == null) { - let editorAccessor = editorAtRootPosition(adapter, startInRoot); - const candidate = editorAccessor.getEditor(); - if (!candidate) { - throw Error('Editor could not be accessed'); - } - editor = candidate; - } - - const document = documentAtRootPosition(adapter, startInRoot); - - return { - start: document.transformVirtualToEditor(start)!, - end: document.transformVirtualToEditor(end)!, - editor: editor - }; - } - - private get_editor_range( + /** + * Construct the range to underline manually using the token information. + */ + private _getEditorRange( adapter: WidgetLSPAdapter, response: lsProtocol.Hover, - position: IRootPosition, token: CodeEditor.IToken, - cm_editor: CodeEditor.IEditor + editor: CodeEditor.IEditor ): IEditorRange { if (typeof response.range !== 'undefined') { - return this.range_to_editor_range(adapter, response.range, cm_editor); + return rangeToEditorRange(adapter, response.range, editor); } - // construct the range manually using the token information - let startInRoot = { - line: position.line, - ch: token.offset - } as IRootPosition; - let endInRoot = { - line: position.line, - ch: token.offset + token.value.length - } as IRootPosition; + const startInEditor = editor.getPositionAt(token.offset); + const endInEditor = editor.getPositionAt(token.offset + token.value.length); + + if (!startInEditor || !endInEditor) { + throw Error( + 'Could not reconstruct editor range: start or end of token in editor do not resolve to a position' + ); + } return { - start: rootPositionToEditorPosition(adapter, startInRoot), - end: rootPositionToEditorPosition(adapter, endInRoot), - editor: cm_editor + start: PositionConverter.ce_to_cm(startInEditor) as IEditorPosition, + end: PositionConverter.ce_to_cm(endInEditor) as IEditorPosition, + editor }; } - private add_range_if_needed( + private _addRange( adapter: WidgetLSPAdapter, response: lsProtocol.Hover, - editor_range: IEditorRange, + editorEange: IEditorRange, editorAccessor: Document.IEditor ): lsProtocol.Hover { - if (typeof response.range !== 'undefined') { - return response; - } return { ...response, range: { @@ -794,7 +715,7 @@ export class HoverFeature extends Feature { editorPositionToRootPosition( adapter, editorAccessor, - editor_range.start + editorEange.start )! ) ), @@ -804,7 +725,7 @@ export class HoverFeature extends Feature { editorPositionToRootPosition( adapter, editorAccessor, - editor_range.end + editorEange.end )! ) ) diff --git a/packages/jupyterlab-lsp/src/features/signature.ts b/packages/jupyterlab-lsp/src/features/signature.ts index dcf915f6a..9b3fb8d0e 100644 --- a/packages/jupyterlab-lsp/src/features/signature.ts +++ b/packages/jupyterlab-lsp/src/features/signature.ts @@ -221,6 +221,7 @@ export class SignatureFeature extends Feature { throw Error('[signature] no adapter for model aborting'); } + // TODO: the assumption that updated editor = active editor will fail on RTC. How to get `CodeEditor.IEditor` and `Document.IEditor` from `EditorView`? we got `CodeEditor.IModel` from `options.model` but may need more context here. const editorAccessor = adapter.activeEditor; const editor = editorAccessor!.getEditor()!; @@ -228,10 +229,9 @@ export class SignatureFeature extends Feature { // especially on copy paste this can be problematic. const position = editor.getCursorPosition(); - const editorPosition = { - line: position.line, - ch: position.column - } as IEditorPosition; + const editorPosition = PositionConverter.ce_to_cm( + position + ) as IEditorPosition; // Delay handling by moving on top of the stack // so that virtual document is updated. @@ -436,15 +436,11 @@ export class SignatureFeature extends Feature { return; } - // get_cursor_position - // TODO: helper? this is wrong - it is editor position, not root position + // TODO: helper? const editorAccessor = adapter.activeEditor!; const editor = editorAccessor.getEditor()!; const pos = editor.getCursorPosition(); - const editorPosition = { - ch: pos.column, - line: pos.line - } as IEditorPosition; + const editorPosition = PositionConverter.ce_to_cm(pos) as IEditorPosition; // TODO should I just shove it into Feature class and have an adapter getter in there? const rootPosition = editorPositionToRootPosition( @@ -578,11 +574,15 @@ export class SignatureFeature extends Feature { const connection = this.connectionManager.connections.get( virtualDocument.uri )!; + if (!connection.isReady) { + return; + } // @ts-ignore TODO remove after upstream fixes are released const signatureCharacters = // @ts-ignore - connection.serverCapabilities?.signatureHelpProvider?.triggerCharacters; + connection.serverCapabilities?.signatureHelpProvider?.triggerCharacters ?? + []; // only proceed if: trigger character was used or the signature is/was visible immediately before if (!(signatureCharacters.includes(lastCharacter) || isSignatureShown)) { diff --git a/packages/jupyterlab-lsp/src/features/syntax_highlighting.ts b/packages/jupyterlab-lsp/src/features/syntax_highlighting.ts index 0ffa5c8f4..fcfacce7e 100644 --- a/packages/jupyterlab-lsp/src/features/syntax_highlighting.ts +++ b/packages/jupyterlab-lsp/src/features/syntax_highlighting.ts @@ -1,4 +1,4 @@ -/* +import { EditorView } from '@codemirror/view'; import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -7,48 +7,69 @@ import { IEditorMimeTypeService, IEditorServices } from '@jupyterlab/codeeditor'; -import { CodeMirrorEditor, ICodeMirror } from '@jupyterlab/codemirror'; +import { CodeMirrorEditor , + IEditorExtensionRegistry, + IEditorLanguageRegistry, + EditorExtensionRegistry +} from '@jupyterlab/codemirror'; +import { + ILSPFeatureManager, + ILSPDocumentConnectionManager, + WidgetLSPAdapter +} from '@jupyterlab/lsp'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import { ITranslator } from '@jupyterlab/translation'; import { LabIcon } from '@jupyterlab/ui-components'; import syntaxSvg from '../../style/icons/syntax-highlight.svg'; import { CodeSyntax as LSPSyntaxHighlightingSettings } from '../_syntax_highlighting'; -import { CodeMirrorIntegration } from '../editor_integration/codemirror'; -import { - FeatureSettings, - IEditorIntegrationOptions, - IFeatureLabIntegration, - IFeatureSettings -} from '../feature'; -import { ILSPFeatureManager, PLUGIN_ID } from '../tokens'; +import { FeatureSettings, Feature } from '../feature'; +import { PLUGIN_ID } from '../tokens'; +import { VirtualDocument } from '../virtual/document'; export const syntaxHighlightingIcon = new LabIcon({ name: 'lsp:syntax-highlighting', svgstr: syntaxSvg }); -const FEATURE_ID = PLUGIN_ID + ':syntax_highlighting'; +export class SyntaxHighlightingFeature extends Feature { + readonly id = SyntaxHighlightingFeature.id; + // note: semantic highlighting could be implemented here + readonly capabilities = {}; + protected originalModes = new Map(); -export class CMSyntaxHighlighting extends CodeMirrorIntegration { - editors_with_active_highlight: Set; - - constructor(options: IEditorIntegrationOptions) { + constructor(protected options: SyntaxHighlightingFeature.IOptions) { super(options); - this.virtualDocument.changed.connect(this.update_mode.bind(this), this); - this.editors_with_active_highlight = new Set(); - } + const connectionManager = options.connectionManager; + + options.editorExtensionRegistry.addExtension({ + name: 'lsp:codeSignature', + factory: options => { + const updateListener = EditorView.updateListener.of(viewUpdate => { + if (!viewUpdate.docChanged) { + return; + } - get lab_integration() { - return super.lab_integration as SyntaxLabIntegration; - } + const adapter = [...connectionManager.adapters.values()].find( + adapter => adapter.widget.node.contains(viewUpdate.view.contentDOM) + ); + + // TODO https://github.com/jupyterlab/jupyterlab/issues/14711#issuecomment-1624442627 + // const editor = adapter.editors.find(e => e.model === options.model); + + if (adapter) { + this.update_mode(adapter, viewUpdate.view); + } + }); - get settings() { - return super.settings as IFeatureSettings; + return EditorExtensionRegistry.createImmutableExtension([ + updateListener + ]); + } + }); } - private get_mode(language: string) { - let mimetype = this.lab_integration.mimeTypeService.getMimeTypeByLanguage({ + private getMode(language: string): string | undefined { + let mimetype = this.options.mimeTypeService.getMimeTypeByLanguage({ name: language }); @@ -59,28 +80,45 @@ export class CMSyntaxHighlighting extends CodeMirrorIntegration { return; } - return this.lab_integration.codeMirror.CodeMirror.findModeByMIME(mimetype); + const editorLanguage = this.options.languageRegistry.findByMIME(mimetype); + + if (!editorLanguage) { + return; + } + + return mimetype; } - update_mode() { - let root = this.virtualDocument; - let editors_with_current_highlight = new Set(); + update_mode(adapter: WidgetLSPAdapter, view: EditorView) { + let topDocument = adapter.virtualDocument as VirtualDocument; + const totalArea = view.state.doc.length; + + // TODO no way to map from EditorView to Document.IEditor is blocking here. + // TODO: active editor is not necessairly the editor that triggered the update + const editorAccessor = adapter.activeEditor; + const editor = editorAccessor?.getEditor()!; + if ( + !editorAccessor || + !editor || + (editor as CodeMirrorEditor).editor !== view + ) { + // TODO: ideally we would not have to do this (we would have view -> editor map) + return; + } + if (!topDocument) { + return; + } - for (let map of root.foreignDocument_maps) { + const overrides = new Map(); + for (let map of topDocument.getForeignDocuments(editorAccessor)) { for (let [range, block] of map.entries()) { - let ceEditor = block.editor as CodeMirrorEditor; - let editor = ceEditor.editor; - let lines = editor.getValue('\n'); - let total_area = lines.concat('').length; - + let editor = block.editor.getEditor()! as CodeMirrorEditor; let covered_area = - ceEditor.getOffsetAt(range.end) - ceEditor.getOffsetAt(range.start); - - let coverage = covered_area / total_area; + editor.getOffsetAt(range.end) - editor.getOffsetAt(range.start); + let coverage = covered_area / totalArea; let language = block.virtualDocument.language; - - let mode = this.get_mode(language); + let mode = this.getMode(language); // if not highlighting mode available, skip this editor if (typeof mode === 'undefined') { @@ -88,75 +126,80 @@ export class CMSyntaxHighlighting extends CodeMirrorIntegration { } // change the mode if the majority of the code is the foreign code - if (coverage > this.settings.composite.foreignCodeThreshold) { - editors_with_current_highlight.add(ceEditor); - let old_mode = editor.getOption('mode'); - if (old_mode != mode.mime) { - editor.setOption('mode', mode.mime); - } + if (coverage > this.options.settings.composite.foreignCodeThreshold) { + // this will trigger a side effect of switching language by updating + // private language compartment (implementation detail). + editor.model.mimeType = mode; + overrides.set(editor, editor.model.mimeType); } } } - - if (editors_with_current_highlight != this.editors_with_active_highlight) { - for (let ceEditor of this.editors_with_active_highlight) { - if (!editors_with_current_highlight.has(ceEditor)) { - // ceEditor.injectExtension ? - ceEditor.editor.setOption('mode', ceEditor.model.mimeType); - } + const relevantEditors = new Set( + adapter.editors.map(e => e.ceEditor.getEditor()) + ); + // restore modes on editors which are no longer over the threshold + // (but only those which belong to this adapter). + for (const [editor, originalMode] of this.originalModes) { + if (!relevantEditors.has(editor)) { + continue; + } + if (overrides.has(editor)) { + continue; + } else { + editor.model.mimeType = originalMode; + } + } + // add new ovverrides to remember the original mode + for (const [editor, mode] of overrides) { + if (!this.originalModes.has(editor)) { + this.originalModes.set(editor, mode); } } - - this.editors_with_active_highlight = editors_with_current_highlight; } } -class SyntaxLabIntegration implements IFeatureLabIntegration { - // TODO: we could accept custom mimetype mapping from settings - settings: IFeatureSettings; - - constructor( - public mimeTypeService: IEditorMimeTypeService, - public codeMirror: ICodeMirror - ) {} +export namespace SyntaxHighlightingFeature { + export interface IOptions extends Feature.IOptions { + settings: FeatureSettings; + mimeTypeService: IEditorMimeTypeService; + editorExtensionRegistry: IEditorExtensionRegistry; + languageRegistry: IEditorLanguageRegistry; + } + export const id = PLUGIN_ID + ':syntax_highlighting'; } export const SYNTAX_HIGHLIGHTING_PLUGIN: JupyterFrontEndPlugin = { - id: FEATURE_ID, + id: SyntaxHighlightingFeature.id, requires: [ ILSPFeatureManager, IEditorServices, ISettingRegistry, - ICodeMirror, - ITranslator + IEditorExtensionRegistry, + IEditorLanguageRegistry ], autoStart: true, - activate: ( + activate: async ( app: JupyterFrontEnd, featureManager: ILSPFeatureManager, editorServices: IEditorServices, settingRegistry: ISettingRegistry, - codeMirror: ICodeMirror, - translator: ITranslator + editorExtensionRegistry: IEditorExtensionRegistry, + languageRegistry: IEditorLanguageRegistry, + connectionManager: ILSPDocumentConnectionManager ) => { - const settings = new FeatureSettings(settingRegistry, FEATURE_ID); - const trans = translator.load('jupyterlab_lsp'); - - featureManager.register({ - feature: { - editorIntegrationFactory: new Map([ - ['CodeMirrorEditor', CMSyntaxHighlighting] - ]), - commands: [], - id: FEATURE_ID, - name: trans.__('Syntax highlighting'), - labIntegration: new SyntaxLabIntegration( - editorServices.mimeTypeService, - codeMirror - ), - settings: settings - } + const settings = new FeatureSettings( + settingRegistry, + SyntaxHighlightingFeature.id + ); + await settings.ready; + const feature = new SyntaxHighlightingFeature({ + settings, + connectionManager, + editorExtensionRegistry, + mimeTypeService: editorServices.mimeTypeService, + languageRegistry }); + featureManager.register(feature); + // return feature; } }; -*/ diff --git a/packages/jupyterlab-lsp/src/index.ts b/packages/jupyterlab-lsp/src/index.ts index adb3776c5..cab871fb5 100644 --- a/packages/jupyterlab-lsp/src/index.ts +++ b/packages/jupyterlab-lsp/src/index.ts @@ -35,7 +35,7 @@ import { NOTEBOOK_ADAPTER_PLUGIN } from './adapters/notebook'; import { StatusButtonExtension } from './components/statusbar'; import { COMPLETION_PLUGIN } from './features/completion'; import { DIAGNOSTICS_PLUGIN } from './features/diagnostics'; -//import { HIGHLIGHTS_PLUGIN } from './features/highlights'; +import { HIGHLIGHTS_PLUGIN } from './features/highlights'; import { HOVER_PLUGIN } from './features/hover'; import { JUMP_PLUGIN } from './features/jump_to'; import { RENAME_PLUGIN } from './features/rename'; @@ -206,7 +206,7 @@ const default_features: JupyterFrontEndPlugin[] = [ SIGNATURE_PLUGIN, HOVER_PLUGIN, RENAME_PLUGIN, - //HIGHLIGHTS_PLUGIN, + HIGHLIGHTS_PLUGIN, DIAGNOSTICS_PLUGIN //SYNTAX_HIGHLIGHTING_PLUGIN ]; diff --git a/packages/jupyterlab-lsp/src/marks.ts b/packages/jupyterlab-lsp/src/marks.ts new file mode 100644 index 000000000..3b7d7b51c --- /dev/null +++ b/packages/jupyterlab-lsp/src/marks.ts @@ -0,0 +1,102 @@ +import { StateField, StateEffect } from '@codemirror/state'; +import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; + +export interface IMark { + from: number; + to: number; + kind: Kinds; +} + +/** + * Manage marks in multiple editor views (e.g. cells). + */ +export interface ISimpleMarkManager { + putMarks(view: EditorView, positions: IMark[]): void; + /** + * Clear marks from all editor views. + */ + clearAllMarks(): void; + clearEditorMarks(view: EditorView): void; +} + +export type MarkDecorationSpec = Parameters[0] & { + class: string; +}; + +namespace Private { + export let specCounter = 0; +} + +export function createMarkManager( + specs: Record +): ISimpleMarkManager { + const specId = ++Private.specCounter; + const kindToMark = Object.fromEntries( + Object.entries(specs).map(([k, spec]) => [ + k as Kinds, + Decoration.mark({ + ...(spec as MarkDecorationSpec), + _id: Private.specCounter + }) + ]) + ) as Record; + + const addMark = StateEffect.define>({ + map: ({ from, to, kind }, change) => ({ + from: change.mapPos(from), + to: change.mapPos(to), + kind + }) + }); + + const removeMark = StateEffect.define(); + + const markField = StateField.define({ + create() { + return Decoration.none; + }, + update(marks, tr) { + marks = marks.map(tr.changes); + for (let e of tr.effects) { + if (e.is(addMark)) { + marks = marks.update({ + add: [kindToMark[e.value.kind].range(e.value.from, e.value.to)] + }); + } else if (e.is(removeMark)) { + marks = marks.update({ + filter: (from, to, value) => { + return value.spec['_id'] !== specId; + } + }); + } + } + return marks; + }, + provide: f => EditorView.decorations.from(f) + }); + const views = new Set(); + + return { + putMarks(view: EditorView, positions: IMark[]) { + const effects: StateEffect[] = positions.map(position => + addMark.of(position) + ); + + if (!view.state.field(markField, false)) { + effects.push(StateEffect.appendConfig.of([markField])); + } + view.dispatch({ effects }); + views.add(view); + }, + clearAllMarks() { + for (let view of views) { + this.clearEditorMarks(view); + } + views.clear(); + }, + clearEditorMarks(view: EditorView) { + const effects: StateEffect[] = [removeMark.of(null)]; + view.dispatch({ effects }); + } + }; +} diff --git a/packages/jupyterlab-lsp/src/virtual/document.ts b/packages/jupyterlab-lsp/src/virtual/document.ts index 819183a38..0a008326b 100644 --- a/packages/jupyterlab-lsp/src/virtual/document.ts +++ b/packages/jupyterlab-lsp/src/virtual/document.ts @@ -3,7 +3,8 @@ import { CodeEditor } from '@jupyterlab/codeeditor'; import type { IVirtualPosition, IRootPosition, - Document + Document, + ForeignDocumentsMap } from '@jupyterlab/lsp'; import { VirtualDocument as VirtualDocumentBase } from '@jupyterlab/lsp'; @@ -49,7 +50,9 @@ export class VirtualDocument extends VirtualDocumentBase { } } - // Override parent method to hook cell magics overrides + /** + * Extends parent method to hook cell magics overrides. + */ prepareCodeBlock( block: Document.ICodeBlockOptions, editorShift: CodeEditor.IPosition = { line: 0, column: 0 } @@ -78,11 +81,27 @@ export class VirtualDocument extends VirtualDocumentBase { return { lines, foreignDocumentsMap, skipInspect }; } - // add method which was previously implemented in CodeMirrorIntegration - // but probably should have been in VirtualDocument all along + /** + * @experimental + */ transformVirtualToRoot(position: IVirtualPosition): IRootPosition | null { + // a method which was previously implemented in CodeMirrorIntegration + // but probably should have been in VirtualDocument all along let editor = this.virtualLines.get(position.line)!.editor; let editorPosition = this.transformVirtualToEditor(position); return this.transformFromEditorToRoot(editor, editorPosition!); } + + /** + * @experimental + */ + getForeignDocuments(editorAccessor: Document.IEditor): ForeignDocumentsMap[] { + let maps = new Set(); + for (let line of this.sourceLines.values()) { + if (line.editor === editorAccessor) { + maps.add(line.foreignDocumentsMap); + } + } + return [...maps.values()]; + } } diff --git a/packages/jupyterlab-lsp/style/highlight.css b/packages/jupyterlab-lsp/style/highlight.css index 40b26cdb4..4010aece5 100644 --- a/packages/jupyterlab-lsp/style/highlight.css +++ b/packages/jupyterlab-lsp/style/highlight.css @@ -77,6 +77,19 @@ } /* highlight */ -.cm-lsp-highlight { +.cm-lsp-highlight-Read { + /* highlight on places where variable is accessed, e.g. `print(var)` */ + background-color: var(--jp-editor-mirror-lsp-highlight-background-color); + outline: 1px dashed var(--jp-editor-mirror-lsp-highlight-border-color); +} + +.cm-lsp-highlight-Write { + /* highlight on places where variable is defined, e.g. `var = 1` */ + background-color: var(--jp-editor-mirror-lsp-highlight-background-color); + outline: 1px solid var(--jp-editor-mirror-lsp-highlight-border-color); +} + +.cm-lsp-highlight-Text { + /* highlight on places where variable was matched based on text rather than semantics */ background-color: var(--jp-editor-mirror-lsp-highlight-background-color); } diff --git a/packages/jupyterlab-lsp/style/variables/base.css b/packages/jupyterlab-lsp/style/variables/base.css index 94c0fe787..7b0ceb37a 100644 --- a/packages/jupyterlab-lsp/style/variables/base.css +++ b/packages/jupyterlab-lsp/style/variables/base.css @@ -1,6 +1,7 @@ :root { /* highlight */ --jp-editor-mirror-lsp-highlight-background-color: var(--jp-layout-color2); + --jp-editor-mirror-lsp-highlight-border-color: var(--jp-layout-color3); /* diagnostics */ --jp-editor-mirror-lsp-diagnostic-decoration-style: dashed; --jp-editor-mirror-lsp-diagnostic-error-decoration-color: var( diff --git a/packages/tsconfigbase.json b/packages/tsconfigbase.json index 3595fb478..49680d157 100644 --- a/packages/tsconfigbase.json +++ b/packages/tsconfigbase.json @@ -14,7 +14,7 @@ "preserveWatchOutput": true, "resolveJsonModule": true, "sourceMap": true, - "target": "es2018", + "target": "es2019", "skipLibCheck": true, "strictNullChecks": true, "noImplicitThis": true,