UNPKG

@krassowski/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

408 lines 15.7 kB
import { CodeMirrorEditor } from '@jupyterlab/codemirror'; import { Signal } from '@lumino/signaling'; import { ILSPVirtualEditorManager, PLUGIN_ID } from '../tokens'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore class DocDispatcher { constructor(virtual_editor, adapter) { this.virtual_editor = virtual_editor; this.adapter = adapter; } markText(from, to, options) { // TODO: edgecase: from and to in different cells let ce_editor = this.virtual_editor.virtual_document.get_editor_at_source_line(from); let cm_editor = this.virtual_editor.ce_editor_to_cm_editor.get(ce_editor); let notebook_map = this.virtual_editor; return cm_editor .getDoc() .markText(notebook_map.transform_from_root_to_editor(from), notebook_map.transform_from_root_to_editor(to), options); } getCursor(start) { let active_editor = this.adapter.activeEditor; if (active_editor == null) { // TODO return undefined; } let cursor = active_editor.editor .getDoc() .getCursor(start); return this.virtual_editor.transform_from_editor_to_root(active_editor, cursor); } } /** * VirtualEditor extends the CodeMirror.Editor interface; its subclasses may either * fast-forward any requests to an existing instance of the CodeMirror.Editor * (using ES6 Proxy), or implement custom behaviour, allowing for the use of * virtual documents representing code in complex entities such as notebooks. */ export class CodeMirrorVirtualEditor { constructor(options) { // TODO: getValue could be made private in the virtual editor and the virtual editor // could stop exposing the full implementation of CodeMirror but rather hide it inside. this.editor_name = 'CodeMirrorEditor'; this.isDisposed = false; this._event_wrappers = new Map(); this.adapter = options.adapter; this.virtual_document = options.virtual_document; this.console = this.adapter.console; this.change = new Signal(this); this.block_added_handlers = []; this.block_removed_handlers = []; this.editor_to_source_line = new Map(); this.cm_editor_to_ce_editor = new Map(); this.ce_editor_to_cm_editor = new Map(); this._proxy = new Proxy(this, { get: function (target, prop, receiver) { if (!(prop in target)) { console.warn(`Unimplemented method ${prop.toString()} for CodeMirrorVirtualEditor`); return; } else { return Reflect.get(target, prop, receiver); } } }); // this is not the most efficient, but probably the most reliable way this.virtual_document.update_manager.update_began.connect(this.onEditorsUpdated, this); this.virtual_document.update_manager.block_added.connect(this.save_block_position, this); this.virtual_document.update_manager.update_finished.connect(() => { this.editor_to_source_line = this.editor_to_source_line_new; }, this); this.set_event_handlers(); return this._proxy; } dispose() { if (this.isDisposed) { return; } this.editor_to_source_line.clear(); this.cm_editor_to_ce_editor.clear(); this.ce_editor_to_cm_editor.clear(); this.off('change', this.emit_change); for (let [[eventName], wrapped_handler] of this._event_wrappers.entries()) { this.forEveryBlockEditor(cm_editor => { cm_editor.off(eventName, wrapped_handler); }, false); } this._event_wrappers.clear(); this.disconnectBlockMonitoring(); this.virtual_document.dispose(); // just to be sure this.virtual_document = null; this.code_extractors = null; this.isDisposed = true; // just to be sure this.forEveryBlockEditor = null; this._proxy = null; } get_cursor_position() { return this.getDoc().getCursor('end'); } onEditorsUpdated(update_manager, blocks) { this.cm_editor_to_ce_editor.clear(); this.ce_editor_to_cm_editor.clear(); this.editor_to_source_line_new = new Map(); for (let block of blocks) { let ce_editor = block.ce_editor; let cm_editor = ce_editor.editor; this.cm_editor_to_ce_editor.set(cm_editor, ce_editor); this.ce_editor_to_cm_editor.set(ce_editor, cm_editor); } } save_block_position(update_manager, block_data) { this.editor_to_source_line_new.set(block_data.block.ce_editor, block_data.virtual_document.last_source_line); } set_event_handlers() { this.on('change', this.emit_change.bind(this)); } emit_change(instance, change) { this.change.emit(change); } window_coords_to_root_position(coordinates) { const position = this.coordsChar(coordinates, 'window'); if (position.line === -1 && position.ch === -1) { return null; } return position; } get_token_at(position) { let token = this.getTokenAt(position); return { value: token.string, offset: token.start, type: token.type || '' }; } get_cm_editor(position) { return this.get_editor_at_root_line(position); } transform_virtual_to_editor(position) { return this.virtual_document.transform_virtual_to_editor(position); } // TODO .root is not really needed as we are in editor now... document_at_root_position(position) { let root_as_source = position; return this.virtual_document.root.document_at_source_position(root_as_source); } root_position_to_virtual_position(position) { let root_as_source = position; return this.virtual_document.root.virtual_position_at_document(root_as_source); } get_editor_at_root_position(root_position) { return this.virtual_document.root.get_editor_at_source_line(root_position); } root_position_to_editor(root_position) { return this.virtual_document.root.transform_source_to_editor(root_position); } /** * Proxy the event handler binding to the CodeMirror editors, * allowing for multiple actual editors per a virtual editor. * * Only handlers accepting CodeMirror.Editor are supported for simplicity. */ on(eventName, handler, ...args) { let wrapped_handler = (instance, ...args) => { try { return handler(this, ...args); } catch (error) { this.console.warn('CodeMirrorVirtualEditor handler (which should accept a CodeMirror Editor instance) failed', { error, instance, args, this: this }); } }; this._event_wrappers.set([eventName, handler], wrapped_handler); this.forEveryBlockEditor(cm_editor => { cm_editor.on(eventName, wrapped_handler); }, true, cm_editor => { cm_editor.off(eventName, wrapped_handler); }); } off(eventName, handler, ...args) { let wrapped_handler = this._event_wrappers.get([eventName, handler]); this.forEveryBlockEditor(cm_editor => { cm_editor.off(eventName, wrapped_handler); }); } find_ce_editor(cm_editor) { return this.cm_editor_to_ce_editor.get(cm_editor); } transform_from_editor_to_root(editor, position) { if (!this.editor_to_source_line.has(editor)) { this.console.warn('Editor not found in editor_to_source_line map'); return null; } let shift = this.editor_to_source_line.get(editor); return Object.assign(Object.assign({}, position), { line: position.line + shift }); } transform_from_root_to_editor(pos) { // from notebook to editor space return this.virtual_document.transform_source_to_editor(pos); } addKeyMap(map, bottom) { return; } addLineClass(line, where, _class) { return undefined; } addLineWidget(line, node, options) { return undefined; } addOverlay(mode, options) { for (let editor of this.adapter.editors) { // TODO: use some more intelligent strategy to determine editors to test let cm_editor = editor; cm_editor.editor.addOverlay(mode, options); } } addPanel(node, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore options // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ) { return undefined; } charCoords(pos, mode) { try { let editor = this.get_editor_at_root_line(pos); return editor.charCoords(pos, mode); } catch (e) { console.log(e); return { bottom: 0, left: 0, right: 0, top: 0 }; } } /** * @note returns {line: -1, ch: -1} if position is outside of all editors */ coordsChar(object, mode) { let bestGuess = null; for (let editor of this.adapter.editors) { // TODO: use some more intelligent strategy to determine editors to test let cm_editor = editor; let pos = cm_editor.editor.coordsChar(object, mode); if (pos.outside === 1) { continue; } bestGuess = this.transform_from_editor_to_root(editor, pos); break; } if (bestGuess == null) { return { line: -1, ch: -1 }; } return bestGuess; } cursorCoords(where, mode) { if (typeof where !== 'boolean' && where != null) { let editor = this.get_editor_at_root_line(where); return editor.cursorCoords(this.transform_from_root_to_editor(where)); } return { bottom: 0, left: 0, top: 0 }; } get any_editor() { return this.adapter.editors[0].editor; } defaultCharWidth() { return this.any_editor.defaultCharWidth(); } defaultTextHeight() { return this.any_editor.defaultTextHeight(); } endOperation() { for (let editor of this.adapter.editors) { let cm_editor = editor; cm_editor.editor.endOperation(); } } execCommand(name) { for (let editor of this.adapter.editors) { let cm_editor = editor; cm_editor.editor.execCommand(name); } } getDoc() { let dummy_doc = new DocDispatcher(this, this.adapter); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return dummy_doc; } get_editor_at_root_line(pos) { let ce_editor = this.virtual_document.root.get_editor_at_source_line(pos); return this.ce_editor_to_cm_editor.get(ce_editor); } getTokenAt(pos, precise) { let editor = this.get_editor_at_root_line(pos); return editor.getTokenAt(this.transform_from_root_to_editor(pos)); } getTokenTypeAt(pos) { let ce_editor = this.virtual_document.get_editor_at_source_line(pos); let cm_editor = this.ce_editor_to_cm_editor.get(ce_editor); return cm_editor.getTokenTypeAt(this.transform_from_root_to_editor(pos)); } get_editor_value(editor) { let codemirror_editor = editor; return codemirror_editor.model.value.text; // A previous implementation was using the underlying // CodeMirror editor instance (as one could expect), i.e: // let cm_editor = codemirror_editor.editor; // return cm_editor.getValue(); // however, because we are listening to: // this.widget.context.model.contentChanged // it turns out that the model can not be as fast to propagate // the changes to the underlying CodeMirror editor instance yet // so it seems reasonable to use the newer value from the model // to build the next VirtualDocument; it turned out to solve some // of the failures to resolve position within the virtual document // which were due to race conditions. } getWrapperElement() { return this.adapter.wrapper_element; } heightAtLine(line, mode, includeWidgets) { return 0; } isReadOnly() { return false; } lineAtHeight(height, mode) { return 0; } disconnectBlockMonitoring() { for (let handler of this.block_added_handlers) { this.adapter.editorAdded.disconnect(handler); } this.block_added_handlers = []; for (let handler of this.block_removed_handlers) { this.adapter.editorRemoved.disconnect(handler); } this.block_removed_handlers = []; } forEveryBlockEditor(callback, monitor_for_new_blocks = true, on_editor_removed_callback = null) { const editors_with_handlers = new Set(); // TODO... the need of iterating over all editors is universal - so this could be // generalised to the VirtualEditor rather than live in CodeMirrorVirtualEditor; // How would the VirtualEditor get knowledge of the editor instances? // From the adapter (obviously). for (let editor of this.adapter.editors) { let cm_editor = editor.editor; editors_with_handlers.add(cm_editor); callback(cm_editor); } if (monitor_for_new_blocks) { const on_block_added = (adapter, data) => { let { editor } = data; if (editor == null) { return; } let cm_editor = editor.editor; if (!editors_with_handlers.has(cm_editor)) { callback(cm_editor); } }; const on_block_removed = (adapter, data) => { if (on_editor_removed_callback == null) { return; } let { editor } = data; if (editor == null) { return; } let cm_editor = editor.editor; on_editor_removed_callback(cm_editor); }; this.block_added_handlers.push(on_block_added); this.block_added_handlers.push(on_block_removed); this.adapter.editorAdded.connect(on_block_added); this.adapter.editorRemoved.connect(on_block_removed); } } /** * Find a cell in notebook which uses given CodeMirror editor. * This function is O(n) - when looking up many cells * using a hashmap based approach may be more efficient. * @param cm_editor */ find_editor(cm_editor) { let ce_editor = this.cm_editor_to_ce_editor.get(cm_editor); return { index: this.adapter.get_editor_index(ce_editor), node: this.adapter.get_editor_wrapper(ce_editor) }; } } export const CODEMIRROR_VIRTUAL_EDITOR = { id: PLUGIN_ID + ':CodeMirrorVirtualEditor', requires: [ILSPVirtualEditorManager], activate: (app, editorManager) => { return editorManager.registerEditorType({ implementation: CodeMirrorVirtualEditor, name: 'CodeMirrorEditor', supports: CodeMirrorEditor }); }, autoStart: true }; //# sourceMappingURL=codemirror_editor.js.map