UNPKG

@krassowski/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

295 lines 13.5 kB
import { PositionConverter } from '../../converter'; import { until_ready } from '../../utils'; import { VirtualDocument } from '../../virtual/document'; import { WidgetAdapter } from '../adapter'; export class NotebookAdapter extends WidgetAdapter { constructor(extension, editor_widget) { super(extension, editor_widget); this.type = 'code'; this.is_ready = () => { var _a; return (!this.widget.isDisposed && this.widget.context.isReady && this.widget.content.isVisible && this.widget.content.widgets.length > 0 && ((_a = this.widget.context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel) != null); }; this.ce_editor_to_cell = new Map(); this.editor = editor_widget.content; this.known_editors_ids = new Set(); this.initialized = new Promise((resolve, reject) => { this.init_once_ready().then(resolve).catch(reject); }); } async update_language_info() { var _a, _b, _c, _d; const language_info = (_d = (await ((_c = (_b = (_a = this.widget.context.sessionContext) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.info))) === null || _d === void 0 ? void 0 : _d.language_info; if (language_info) { this._language_info = language_info; } else { throw new Error('Language info update failed (no session, kernel, or info available)'); } } async on_kernel_changed(_session, change) { if (!change.newValue) { this.console.log('Kernel was shut down'); return; } try { // note: we need to wait until ready before updating language info this.console.log('Changed kernel, will try to reconnect'); const old_language_info = this._language_info; await until_ready(this.is_ready, -1); await this.update_language_info(); const new_language_info = this._language_info; if ((old_language_info === null || old_language_info === void 0 ? void 0 : old_language_info.name) != new_language_info.name || (old_language_info === null || old_language_info === void 0 ? void 0 : old_language_info.mimetype) != (new_language_info === null || new_language_info === void 0 ? void 0 : new_language_info.mimetype) || (old_language_info === null || old_language_info === void 0 ? void 0 : old_language_info.file_extension) != (new_language_info === null || new_language_info === void 0 ? void 0 : new_language_info.file_extension)) { this.console.log(`Changed to ${this._language_info.name} kernel, reconnecting`); this.reload_connection(); } else { this.console.log('Keeping old LSP connection as the new kernel uses the same langauge'); } } catch (err) { this.console.warn(err); // try to reconnect anyway this.reload_connection(); } } dispose() { if (this.isDisposed) { return; } this.widget.context.sessionContext.kernelChanged.disconnect(this.on_kernel_changed, this); this.widget.content.activeCellChanged.disconnect(this.activeCellChanged, this); super.dispose(); // editors are needed for the parent dispose() to unbind signals, so they are the last to go this.ce_editor_to_cell.clear(); } get document_path() { return this.widget.context.path; } language_info() { return this._language_info; } get mime_type() { let language_metadata = this.language_info(); if (!language_metadata || !language_metadata.mimetype) { // fallback to the code cell mime type if no kernel in use return this.widget.content.codeMimetype; } return language_metadata.mimetype; } get language_file_extension() { let language_metadata = this.language_info(); if (!language_metadata || !language_metadata.file_extension) { return; } return language_metadata.file_extension.replace('.', ''); } get wrapper_element() { return this.widget.node; } async init_once_ready() { this.console.log('waiting for', this.document_path, 'to fully load'); await this.widget.context.sessionContext.ready; await until_ready(this.is_ready, -1); await this.update_language_info(); this.console.log(this.document_path, 'ready for connection'); this.init_virtual(); // connect the document, but do not open it as the adapter will handle this // after registering all features this.connect_document(this.virtual_editor.virtual_document, false).catch(this.console.warn); this.widget.context.sessionContext.kernelChanged.connect(this.on_kernel_changed, this); this.widget.content.activeCellChanged.connect(this.activeCellChanged, this); this._connectModelSignals(this.widget); this.editor.modelChanged.connect(notebook => { // note: this should not usually happen; // there is no default action that would trigger this, // its just a failsafe in case if another extension decides // to swap the notebook model this.console.warn('Model changed, connecting cell change handler; this is not something we were expecting'); this._connectModelSignals(notebook); }); } _connectModelSignals(notebook) { if (notebook.model === null) { this.console.warn(`Model is missing for notebook ${notebook}, cannot connet cell changed signal!`); } else { notebook.model.cells.changed.connect(this.handle_cell_change, this); } } async handle_cell_change(cells, change) { let cellsAdded = []; let cellsRemoved = []; const type = this.type; if (change.type === 'set') { // handling of conversions is important, because the editors get re-used and their handlers inherited, // so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells. let convertedToMarkdownOrRaw = []; let convertedToCode = []; if (change.newValues.length === change.oldValues.length) { // during conversion the cells should not get deleted nor added for (let i = 0; i < change.newValues.length; i++) { if (change.oldValues[i].type === type && change.newValues[i].type !== type) { convertedToMarkdownOrRaw.push(change.newValues[i]); } else if (change.oldValues[i].type !== type && change.newValues[i].type === type) { convertedToCode.push(change.newValues[i]); } } cellsAdded = convertedToCode; cellsRemoved = convertedToMarkdownOrRaw; } } else if (change.type == 'add') { cellsAdded = change.newValues.filter(cellModel => cellModel.type === type); } // note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion) // because there is no easy way to get the widget associated with the removed cell(s) - because it is no // longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary // as (except for a tiny memory leak) it should not impact the functionality in any way if (cellsRemoved.length || cellsAdded.length || change.type === 'move' || change.type === 'remove') { // in contrast to the file editor document which can be only changed by the modification of the editor content, // the notebook document cna also get modified by a change in the number or arrangement of editors themselves; // for this reason each change has to trigger documents update (so that LSP mirror is in sync). await this.update_documents(); } for (let cellModel of cellsRemoved) { let cellWidget = this.widget.content.widgets.find(cell => cell.model.id === cellModel.id); if (!cellWidget) { this.console.warn(`Widget for removed cell with ID: ${cellModel.id} not found!`); continue; } this.known_editors_ids.delete(cellWidget.editor.uuid); // for practical purposes this editor got removed from our consideration; // it might seem that we should instead look for the editor indicated by // the oldValues[i] cellModel, but this one got already transferred to the // markdown cell in newValues[i] this.editorRemoved.emit({ editor: cellWidget.editor }); } for (let cellModel of cellsAdded) { let cellWidget = this.widget.content.widgets.find(cell => cell.model.id === cellModel.id); if (!cellWidget) { this.console.warn(`Widget for added cell with ID: ${cellModel.id} not found!`); continue; } this.known_editors_ids.add(cellWidget.editor.uuid); this.editorAdded.emit({ editor: cellWidget.editor }); } } get editors() { if (this.isDisposed) { return []; } let notebook = this.widget.content; this.ce_editor_to_cell.clear(); if (notebook.isDisposed) { return []; } return notebook.widgets .filter(cell => cell.model.type === 'code') .map(cell => { this.ce_editor_to_cell.set(cell.editor, cell); return cell.editor; }); } create_virtual_document() { return new VirtualDocument({ language: this.language, path: this.document_path, overrides_registry: this.code_overrides, foreign_code_extractors: this.foreign_code_extractors, file_extension: this.language_file_extension, // notebooks are continuous, each cell is dependent on the previous one standalone: false, // notebooks are not supported by LSP servers has_lsp_supported_file: false, console: this.console }); } get activeEditor() { var _a; return (_a = this.widget.content.activeCell) === null || _a === void 0 ? void 0 : _a.editor; } activeCellChanged(notebook, cell) { if (cell.model.type !== this.type) { return; } if (!this.known_editors_ids.has(cell.editor.uuid)) { this.known_editors_ids.add(cell.editor.uuid); this.editorAdded.emit({ editor: cell.editor }); } this.activeEditorChanged.emit({ editor: cell.editor }); } context_from_active_document() { let cell = this.widget.content.activeCell; if (cell === null) { return null; } // short circuit if disposed if (!this.virtual_editor || !this.get_context) { return null; } if (cell.model.type !== this.type) { // context will be sought on all cells to verify if the context menu should be visible, // thus it is ok to just return null; it seems to stem from the implementation detail // upstream, i.e. the markdown cells appear to be created by transforming the code cells // but do not quote me on that. return null; } let editor = cell.editor; let ce_cursor = editor.getCursorPosition(); let cm_cursor = PositionConverter.ce_to_cm(ce_cursor); let virtual_editor = this.virtual_editor; if (virtual_editor == null) { return null; } let root_position = virtual_editor.transform_from_editor_to_root(editor, cm_cursor); if (root_position == null) { this.console.warn('Could not retrieve current context', virtual_editor); return null; } return this.get_context(root_position); } get_editor_index_at(position) { let cell = this.get_cell_at(position); let notebook = this.widget.content; return notebook.widgets.findIndex(other_cell => { return cell === other_cell; }); } get_editor_index(ce_editor) { let cell = this.ce_editor_to_cell.get(ce_editor); let notebook = this.widget.content; return notebook.widgets.findIndex(other_cell => { return cell === other_cell; }); } get_editor_wrapper(ce_editor) { let cell = this.ce_editor_to_cell.get(ce_editor); return cell.node; } get_cell_at(pos) { let ce_editor = this.virtual_editor.virtual_document.get_editor_at_virtual_line(pos); return this.ce_editor_to_cell.get(ce_editor); } } //# sourceMappingURL=notebook.js.map