UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

195 lines 7.87 kB
import { PositionConverter, documentAtRootPosition, rootPositionToVirtualPosition, editorPositionToRootPosition, editorAtRootPosition, rootPositionToEditorPosition } from './converter'; import { BrowserConsole } from './virtual/console'; // TODO: reconsider naming export class ContextAssembler { constructor(options) { this.options = options; this.console = new BrowserConsole().scope('ContexAssembler'); this._wasOverToken = false; // no-op } /** * Get context from current context menu (or fallback to document context). */ getContext() { let context = null; try { context = this.getContextFromContextMenu(); } catch (e) { this.console.warn('contextMenu is attached, but could not get the context', e); context = null; } if (context == null) { try { context = this._contextFromActiveDocument(); } catch (e) { if (e instanceof Error && e.message === 'Source line not mapped to virtual position') { this.console.log('Could not get context from active document: it is expected when restoring workspace with open files'); } else { throw e; } } } return context; } isContextMenuOverToken() { const context = this.getContextFromContextMenu(); if (!context) { // annoyingly `isEnabled()` gets called again when mouse is over the menu // which means we are no longer able to retrieve context; therefore we cache // last value and return it if the mouse is over menu. return this._wasOverToken; } const { rootPosition, adapter } = context; const editorAccessor = editorAtRootPosition(adapter, rootPosition); const editor = editorAccessor.getEditor(); if (!editor) { return; } const editorPosition = rootPositionToEditorPosition(adapter, rootPosition); const offset = editor.getOffsetAt(PositionConverter.cm_to_ce(editorPosition)); const token = editor.getTokenAt(offset); const isOverToken = token.value !== ''; this._wasOverToken = isOverToken; return isOverToken; } _contextFromActiveDocument() { const adapter = [...this.options.connectionManager.adapters.values()].find(adapter => adapter.widget == this.options.app.shell.currentWidget); if (!adapter) { this.console.debug('No adapter'); return null; } const editor = adapter.activeEditor.getEditor(); if (editor === null) { return null; } let ceCursor = editor.getCursorPosition(); let cmCursor = PositionConverter.ce_to_cm(ceCursor); const virtualDocument = adapter.virtualDocument; if (!virtualDocument) { console.warn('Could not retrieve current context', virtualDocument); return null; } const rootPosition = virtualDocument.transformFromEditorToRoot(adapter.activeEditor, cmCursor); if (rootPosition == null) { console.warn('Could not retrieve current context', virtualDocument); return null; } return this._contextFromRoot(adapter, rootPosition); } _contextFromRoot(adapter, rootPosition) { const document = documentAtRootPosition(adapter, rootPosition); const connection = this.options.connectionManager.connections.get(document.uri); const virtualPosition = rootPositionToVirtualPosition(adapter, rootPosition); return { document: document, connection, virtualPosition, rootPosition, adapter: adapter }; } adapterFromNode(leafNode) { return [...this.options.connectionManager.adapters.values()].find(adapter => adapter.widget.node.contains(leafNode)); } positionFromCoordinates(left, top, adapter, editorAccessor) { if (!editorAccessor) { return null; } const editorPosition = this.editorPositionFromCoordinates(left, top, editorAccessor); if (!editorPosition) { return null; } return editorPositionToRootPosition(adapter, editorAccessor, editorPosition); } editorPositionFromCoordinates(left, top, editorAccessor) { const editor = editorAccessor.getEditor(); if (!editor) { return null; } const position = editor.getPositionForCoordinate({ left: left, right: left, bottom: top, top: top, y: top, x: left, width: 0, height: 0 }); if (!position) { return null; } const editorPosition = { ch: position.column, line: position.line }; return editorPosition; } /* * Attempt to find editor from DOM and then (naively) find `Document.Editor` * from `CodeEditor` isntances. The naive approach iterates all the editors * (=cells) in the adapter, which is expensive and prone to fail in windowed notebooks. * * This may not be needed once https://github.com/jupyterlab/jupyterlab/pull/14920 * is finalised an released. */ editorFromNode(adapter, node) { var _a; const cmContent = node.closest('.cm-content'); if (!cmContent) { return; } const cmView = (_a = cmContent === null || cmContent === void 0 ? void 0 : cmContent.cmView) === null || _a === void 0 ? void 0 : _a.view; if (!cmView) { return; } const editorAccessor = adapter.editors .map(e => e.ceEditor) .find(e => { var _a; return ((_a = e.getEditor()) === null || _a === void 0 ? void 0 : _a.editor) === cmView; }); return editorAccessor; } getContextFromContextMenu() { // Note: could also try using this.app.contextMenu.menu.contentNode position. // Note: could add a guard on this.app.contextMenu.menu.isAttached // get the first node as it gives the most accurate approximation const leafNode = this.options.app.contextMenuHitTest(() => true); if (!leafNode) { return null; } let { left, top } = leafNode.getBoundingClientRect(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore let event = this.options.app._contextMenuEvent; // if possible, use more accurate position from the actual event // (but this relies on an undocumented and unstable feature) if (event !== undefined) { left = event.clientX; top = event.clientY; event.stopPropagation(); } const adapter = this.adapterFromNode(leafNode); if (!adapter) { return null; } const accessorFromNode = this.editorFromNode(adapter, leafNode); if (!accessorFromNode) { // Using `activeEditor` can lead to suprising results in notebook // as a cell can be opened over a cell diffferent than the active one. this.console.warn('Editor accessor not found from node, falling back to activeEditor'); } const editorAccessor = accessorFromNode ? accessorFromNode : adapter.activeEditor; const rootPosition = this.positionFromCoordinates(left, top, adapter, editorAccessor); if (!rootPosition) { return null; } return this._contextFromRoot(adapter, rootPosition); } } //# sourceMappingURL=context.js.map