@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
408 lines • 15.7 kB
JavaScript
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