@jupyterlab/notebook
Version:
JupyterLab - Notebook
379 lines • 14.9 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { IEditorMimeTypeService } from '@jupyterlab/codeeditor';
import { untilReady, VirtualDocument, WidgetLSPAdapter } from '@jupyterlab/lsp';
import { PromiseDelegate } from '@lumino/coreutils';
import { Signal } from '@lumino/signaling';
export class NotebookAdapter extends WidgetLSPAdapter {
constructor(editorWidget, options) {
super(editorWidget, options);
this.editorWidget = editorWidget;
this.options = options;
this._type = 'code';
this._readyDelegate = new PromiseDelegate();
this._editorToCell = new Map();
this.editor = editorWidget.content;
this._cellToEditor = new WeakMap();
this.isReady = this.isReady.bind(this);
Promise.all([
this.widget.context.sessionContext.ready,
this.connectionManager.ready
])
.then(async () => {
await this.initOnceReady();
this._readyDelegate.resolve();
})
.catch(console.error);
}
/**
* Get current path of the document.
*/
get documentPath() {
return this.widget.context.path;
}
/**
* Get the mime type of the document.
*/
get mimeType() {
var _a;
let mimeType;
let languageMetadata = this.language_info();
if (!languageMetadata || !languageMetadata.mimetype) {
// fallback to the code cell mime type if no kernel in use
mimeType = this.widget.content.codeMimetype;
}
else {
mimeType = languageMetadata.mimetype;
}
return Array.isArray(mimeType)
? (_a = mimeType[0]) !== null && _a !== void 0 ? _a : IEditorMimeTypeService.defaultMimeType
: mimeType;
}
/**
* Get the file extension of the document.
*/
get languageFileExtension() {
let languageMetadata = this.language_info();
if (!languageMetadata || !languageMetadata.file_extension) {
return;
}
return languageMetadata.file_extension.replace('.', '');
}
/**
* Get the inner HTMLElement of the document widget.
*/
get wrapperElement() {
return this.widget.node;
}
/**
* Get the list of CM editor with its type in the document,
*/
get editors() {
if (this.isDisposed) {
return [];
}
let notebook = this.widget.content;
this._editorToCell.clear();
if (notebook.isDisposed) {
return [];
}
return notebook.widgets.map(cell => {
return {
ceEditor: this._getCellEditor(cell),
type: cell.model.type,
value: cell.model.sharedModel.getSource()
};
});
}
/**
* Get the activated CM editor.
*/
get activeEditor() {
return this.editor.activeCell
? this._getCellEditor(this.editor.activeCell)
: undefined;
}
/**
* Promise that resolves once the adapter is initialized
*/
get ready() {
return this._readyDelegate.promise;
}
/**
* Get the index of editor from the cursor position in the virtual
* document.
* @deprecated This is error-prone and will be removed in JupyterLab 5.0, use `getEditorIndex()` with `virtualDocument.getEditorAtVirtualLine(position)` instead.
*
* @param position - the position of cursor in the virtual document.
*/
getEditorIndexAt(position) {
let cell = this._getCellAt(position);
let notebook = this.widget.content;
return notebook.widgets.findIndex(otherCell => {
return cell === otherCell;
});
}
/**
* Get the index of input editor
*
* @param ceEditor - instance of the code editor
*/
getEditorIndex(ceEditor) {
let cell = this._editorToCell.get(ceEditor);
return this.editor.widgets.findIndex(otherCell => {
return cell === otherCell;
});
}
/**
* Get the wrapper of input editor.
*
* @param ceEditor - instance of the code editor
*/
getEditorWrapper(ceEditor) {
let cell = this._editorToCell.get(ceEditor);
return cell.node;
}
/**
* Callback on kernel changed event, it will disconnect the
* document with the language server and then reconnect.
*
* @param _session - Session context of changed kernel
* @param change - Changed data
*/
async onKernelChanged(_session, change) {
if (!change.newValue) {
return;
}
try {
// note: we need to wait until ready before updating language info
const oldLanguageInfo = this._languageInfo;
await untilReady(this.isReady, -1);
await this._updateLanguageInfo();
const newLanguageInfo = this._languageInfo;
if ((oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.name) != newLanguageInfo.name ||
(oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.mimetype) != (newLanguageInfo === null || newLanguageInfo === void 0 ? void 0 : newLanguageInfo.mimetype) ||
(oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.file_extension) != (newLanguageInfo === null || newLanguageInfo === void 0 ? void 0 : newLanguageInfo.file_extension)) {
console.log(`Changed to ${this._languageInfo.name} kernel, reconnecting`);
this.reloadConnection();
}
else {
console.log('Keeping old LSP connection as the new kernel uses the same language');
}
}
catch (err) {
console.warn(err);
// try to reconnect anyway
this.reloadConnection();
}
}
/**
* Dispose the widget.
*/
dispose() {
if (this.isDisposed) {
return;
}
this.widget.context.sessionContext.kernelChanged.disconnect(this.onKernelChanged, 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._editorToCell.clear();
Signal.clearData(this);
}
/**
* Method to check if the notebook context is ready.
*/
isReady() {
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);
}
/**
* Update the virtual document on cell changing event.
*
* @param cells - Observable list of changed cells
* @param change - Changed data
*/
async handleCellChange(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 === 'set' ||
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 can 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.updateDocuments();
}
for (let cellModel of cellsAdded) {
let cellWidget = this.widget.content.widgets.find(cell => cell.model.id === cellModel.id);
if (!cellWidget) {
console.warn(`Widget for added cell with ID: ${cellModel.id} not found!`);
continue;
}
// Add editor to the mapping if needed
this._getCellEditor(cellWidget);
}
}
/**
* Generate the virtual document associated with the document.
*/
createVirtualDocument() {
return new VirtualDocument({
language: this.language,
foreignCodeExtractors: this.options.foreignCodeExtractorsManager,
path: this.documentPath,
fileExtension: this.languageFileExtension,
// notebooks are continuous, each cell is dependent on the previous one
standalone: false,
// notebooks are not supported by LSP servers
hasLspSupportedFile: false
});
}
/**
* Get the metadata of notebook.
*/
language_info() {
return this._languageInfo;
}
/**
* Initialization function called once the editor and the LSP connection
* manager is ready. This function will create the virtual document and
* connect various signals.
*/
async initOnceReady() {
await untilReady(this.isReady.bind(this), -1);
await this._updateLanguageInfo();
this.initVirtual();
// connect the document, but do not open it as the adapter will handle this
// after registering all features
this.connectDocument(this.virtualDocument, false).catch(console.warn);
this.widget.context.sessionContext.kernelChanged.connect(this.onKernelChanged, 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
console.warn('Model changed, connecting cell change handler; this is not something we were expecting');
this._connectModelSignals(notebook);
});
}
/**
* Connect the cell changed event to its handler
*
* @param notebook - The notebook that emitted event.
*/
_connectModelSignals(notebook) {
if (notebook.model === null) {
console.warn(`Model is missing for notebook ${notebook}, cannot connect cell changed signal!`);
}
else {
notebook.model.cells.changed.connect(this.handleCellChange, this);
}
}
/**
* Update the stored language info with the one from the notebook.
*/
async _updateLanguageInfo() {
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._languageInfo = language_info;
}
else {
throw new Error('Language info update failed (no session, kernel, or info available)');
}
}
/**
* Handle the cell changed event
* @param notebook - The notebook that emitted event
* @param cell - Changed cell.
*/
_activeCellChanged(notebook, cell) {
if (!cell || cell.model.type !== this._type) {
return;
}
this._activeEditorChanged.emit({
editor: this._getCellEditor(cell)
});
}
/**
* Get the cell at the cursor position of the virtual document.
* @param pos - Position in the virtual document.
*/
_getCellAt(pos) {
let editor = this.virtualDocument.getEditorAtVirtualLine(pos);
return this._editorToCell.get(editor);
}
/**
* Get the cell editor and add new ones to the mappings.
*
* @param cell Cell widget
* @returns Cell editor accessor
*/
_getCellEditor(cell) {
if (!this._cellToEditor.has(cell)) {
const editor = Object.freeze({
getEditor: () => cell.editor,
ready: async () => {
await cell.ready;
return cell.editor;
},
reveal: async () => {
await this.editor.scrollToCell(cell);
return cell.editor;
}
});
this._cellToEditor.set(cell, editor);
this._editorToCell.set(editor, cell);
cell.disposed.connect(() => {
this._cellToEditor.delete(cell);
this._editorToCell.delete(editor);
this._editorRemoved.emit({
editor
});
});
this._editorAdded.emit({
editor
});
}
return this._cellToEditor.get(cell);
}
}
//# sourceMappingURL=notebooklspadapter.js.map