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