@jupyterlab/notebook
Version:
JupyterLab - Notebook
539 lines (483 loc) • 13.3 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Dialog, showDialog } from '@jupyterlab/apputils';
import { ICellModel } from '@jupyterlab/cells';
import { IChangedArgs } from '@jupyterlab/coreutils';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import * as nbformat from '@jupyterlab/nbformat';
import { IObservableList } from '@jupyterlab/observables';
import {
IMapChange,
ISharedNotebook,
NotebookChange,
YNotebook
} from '@jupyter/ydoc';
import {
ITranslator,
nullTranslator,
TranslationBundle
} from '@jupyterlab/translation';
import { JSONExt } from '@lumino/coreutils';
import { ISignal, Signal } from '@lumino/signaling';
import { CellList } from './celllist';
/**
* The definition of a model object for a notebook widget.
*/
export interface INotebookModel extends DocumentRegistry.IModel {
/**
* The list of cells in the notebook.
*/
readonly cells: CellList;
/**
* The major version number of the nbformat.
*/
readonly nbformat: number;
/**
* The minor version number of the nbformat.
*/
readonly nbformatMinor: number;
/**
* The metadata associated with the notebook.
*
* ### Notes
* This is a copy of the metadata. Changing a part of it
* won't affect the model.
* As this returns a copy of all metadata, it is advised to
* use `getMetadata` to speed up the process of getting a single key.
*/
readonly metadata: nbformat.INotebookMetadata;
/**
* Signal emitted when notebook metadata changes.
*/
readonly metadataChanged: ISignal<INotebookModel, IMapChange>;
/**
* The array of deleted cells since the notebook was last run.
*/
readonly deletedCells: string[];
/**
* Shared model
*/
readonly sharedModel: ISharedNotebook;
/**
* Delete a metadata
*
* @param key Metadata key
*/
deleteMetadata(key: string): void;
/**
* Get a metadata
*
* ### Notes
* This returns a copy of the key value.
*
* @param key Metadata key
*/
getMetadata(key: string): any;
/**
* Set a metadata
*
* @param key Metadata key
* @param value Metadata value
*/
setMetadata(key: string, value: any): void;
}
/**
* An implementation of a notebook Model.
*/
export class NotebookModel implements INotebookModel {
/**
* Construct a new notebook model.
*/
constructor(options: NotebookModel.IOptions = {}) {
this.standaloneModel = typeof options.sharedModel === 'undefined';
if (options.sharedModel) {
this.sharedModel = options.sharedModel;
} else {
this.sharedModel = YNotebook.create({
disableDocumentWideUndoRedo:
options.disableDocumentWideUndoRedo ?? true,
data: {
nbformat: nbformat.MAJOR_VERSION,
nbformat_minor: nbformat.MINOR_VERSION,
metadata: {
kernelspec: { name: '', display_name: '' },
language_info: { name: options.languagePreference ?? '' }
}
}
});
}
this._cells = new CellList(this.sharedModel);
this._trans = (options.translator || nullTranslator).load('jupyterlab');
this._deletedCells = [];
this._collaborationEnabled = !!options?.collaborationEnabled;
this._cells.changed.connect(this._onCellsChanged, this);
this.sharedModel.changed.connect(this._onStateChanged, this);
this.sharedModel.metadataChanged.connect(this._onMetadataChanged, this);
}
/**
* A signal emitted when the document content changes.
*/
get contentChanged(): ISignal<this, void> {
return this._contentChanged;
}
/**
* Signal emitted when notebook metadata changes.
*/
get metadataChanged(): ISignal<INotebookModel, IMapChange<any>> {
return this._metadataChanged;
}
/**
* A signal emitted when the document state changes.
*/
get stateChanged(): ISignal<this, IChangedArgs<any>> {
return this._stateChanged;
}
/**
* Get the observable list of notebook cells.
*/
get cells(): CellList {
return this._cells;
}
/**
* The dirty state of the document.
*/
get dirty(): boolean {
return this._dirty;
}
set dirty(newValue: boolean) {
const oldValue = this._dirty;
if (newValue === oldValue) {
return;
}
this._dirty = newValue;
this.triggerStateChange({
name: 'dirty',
oldValue,
newValue
});
}
/**
* The read only state of the document.
*/
get readOnly(): boolean {
return this._readOnly;
}
set readOnly(newValue: boolean) {
if (newValue === this._readOnly) {
return;
}
const oldValue = this._readOnly;
this._readOnly = newValue;
this.triggerStateChange({ name: 'readOnly', oldValue, newValue });
}
/**
* The metadata associated with the notebook.
*
* ### Notes
* This is a copy of the metadata. Changing a part of it
* won't affect the model.
* As this returns a copy of all metadata, it is advised to
* use `getMetadata` to speed up the process of getting a single key.
*/
get metadata(): nbformat.INotebookMetadata {
return this.sharedModel.metadata;
}
/**
* The major version number of the nbformat.
*/
get nbformat(): number {
return this.sharedModel.nbformat;
}
/**
* The minor version number of the nbformat.
*/
get nbformatMinor(): number {
return this.sharedModel.nbformat_minor;
}
/**
* The default kernel name of the document.
*/
get defaultKernelName(): string {
const spec = this.getMetadata('kernelspec');
return spec?.name ?? '';
}
/**
* A list of deleted cells for the notebook..
*/
get deletedCells(): string[] {
return this._deletedCells;
}
/**
* The default kernel language of the document.
*/
get defaultKernelLanguage(): string {
const info = this.getMetadata('language_info');
return info?.name ?? '';
}
/**
* Whether the model is collaborative or not.
*/
get collaborative(): boolean {
return this._collaborationEnabled;
}
/**
* Dispose of the resources held by the model.
*/
dispose(): void {
// Do nothing if already disposed.
if (this.isDisposed) {
return;
}
this._isDisposed = true;
const cells = this.cells;
this._cells = null!;
cells.dispose();
if (this.standaloneModel) {
this.sharedModel.dispose();
}
Signal.clearData(this);
}
/**
* Delete a metadata
*
* @param key Metadata key
*/
deleteMetadata(key: string): void {
return this.sharedModel.deleteMetadata(key);
}
/**
* Get a metadata
*
* ### Notes
* This returns a copy of the key value.
*
* @param key Metadata key
*/
getMetadata(key: string): any {
return this.sharedModel.getMetadata(key);
}
/**
* Set a metadata
*
* @param key Metadata key
* @param value Metadata value
*/
setMetadata(key: string, value: any): void {
if (typeof value === 'undefined') {
this.sharedModel.deleteMetadata(key);
} else {
this.sharedModel.setMetadata(key, value);
}
}
/**
* Serialize the model to a string.
*/
toString(): string {
return JSON.stringify(this.toJSON());
}
/**
* Deserialize the model from a string.
*
* #### Notes
* Should emit a [contentChanged] signal.
*/
fromString(value: string): void {
this.fromJSON(JSON.parse(value));
}
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.INotebookContent {
this._ensureMetadata();
return this.sharedModel.toJSON();
}
/**
* Deserialize the model from JSON.
*
* #### Notes
* Should emit a [contentChanged] signal.
*/
fromJSON(value: nbformat.INotebookContent): void {
const copy = JSONExt.deepCopy(value);
const origNbformat = value.metadata.orig_nbformat;
// Alert the user if the format changes.
copy.nbformat = Math.max(value.nbformat, nbformat.MAJOR_VERSION);
if (
copy.nbformat !== value.nbformat ||
copy.nbformat_minor < nbformat.MINOR_VERSION
) {
copy.nbformat_minor = nbformat.MINOR_VERSION;
}
if (origNbformat !== undefined && copy.nbformat !== origNbformat) {
const newer = copy.nbformat > origNbformat;
let msg: string;
if (newer) {
msg = this._trans.__(
`This notebook has been converted from an older notebook format (v%1)
to the current notebook format (v%2).
The next time you save this notebook, the current notebook format (v%2) will be used.
'Older versions of Jupyter may not be able to read the new format.' To preserve the original format version,
close the notebook without saving it.`,
origNbformat,
copy.nbformat
);
} else {
msg = this._trans.__(
`This notebook has been converted from an newer notebook format (v%1)
to the current notebook format (v%2).
The next time you save this notebook, the current notebook format (v%2) will be used.
Some features of the original notebook may not be available.' To preserve the original format version,
close the notebook without saving it.`,
origNbformat,
copy.nbformat
);
}
void showDialog({
title: this._trans.__('Notebook converted'),
body: msg,
buttons: [Dialog.okButton({ label: this._trans.__('Ok') })]
});
}
// Ensure there is at least one cell
if ((copy.cells?.length ?? 0) === 0) {
copy['cells'] = [
{ cell_type: 'code', source: '', metadata: { trusted: true } }
];
}
this.sharedModel.fromJSON(copy);
this._ensureMetadata();
this.dirty = true;
}
/**
* Handle a change in the cells list.
*/
private _onCellsChanged(
list: CellList,
change: IObservableList.IChangedArgs<ICellModel>
): void {
switch (change.type) {
case 'add':
change.newValues.forEach(cell => {
cell.contentChanged.connect(this.triggerContentChange, this);
});
break;
case 'remove':
break;
case 'set':
change.newValues.forEach(cell => {
cell.contentChanged.connect(this.triggerContentChange, this);
});
break;
default:
break;
}
this.triggerContentChange();
}
private _onMetadataChanged(
sender: ISharedNotebook,
changes: IMapChange
): void {
this._metadataChanged.emit(changes);
this.triggerContentChange();
}
private _onStateChanged(
sender: ISharedNotebook,
changes: NotebookChange
): void {
if (changes.stateChange) {
changes.stateChange.forEach(value => {
if (value.name === 'dirty') {
// Setting `dirty` will trigger the state change.
// We always set `dirty` because the shared model state
// and the local attribute are synchronized one way shared model -> _dirty
this.dirty = value.newValue;
} else if (value.oldValue !== value.newValue) {
this.triggerStateChange({
newValue: undefined,
oldValue: undefined,
...value
});
}
});
}
}
/**
* Make sure we have the required metadata fields.
*/
private _ensureMetadata(languageName: string = ''): void {
if (!this.getMetadata('language_info')) {
this.sharedModel.setMetadata('language_info', { name: languageName });
}
if (!this.getMetadata('kernelspec')) {
this.sharedModel.setMetadata('kernelspec', {
name: '',
display_name: ''
});
}
}
/**
* Trigger a state change signal.
*/
protected triggerStateChange(args: IChangedArgs<any>): void {
this._stateChanged.emit(args);
}
/**
* Trigger a content changed signal.
*/
protected triggerContentChange(): void {
this._contentChanged.emit(void 0);
this.dirty = true;
}
/**
* Whether the model is disposed.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* The shared notebook model.
*/
readonly sharedModel: ISharedNotebook;
/**
* Whether the model should disposed the shared model on disposal or not.
*/
protected standaloneModel = false;
private _dirty = false;
private _readOnly = false;
private _contentChanged = new Signal<this, void>(this);
private _stateChanged = new Signal<this, IChangedArgs<any>>(this);
private _trans: TranslationBundle;
private _cells: CellList;
private _deletedCells: string[];
private _isDisposed = false;
private _metadataChanged = new Signal<NotebookModel, IMapChange>(this);
private _collaborationEnabled: boolean;
}
/**
* The namespace for the `NotebookModel` class statics.
*/
export namespace NotebookModel {
/**
* An options object for initializing a notebook model.
*/
export interface IOptions
extends DocumentRegistry.IModelOptions<ISharedNotebook> {
/**
* Default cell type.
*/
defaultCell?: 'code' | 'markdown' | 'raw';
/**
* Language translator.
*/
translator?: ITranslator;
/**
* Defines if the document can be undo/redo.
*
* Default: true
*
* @experimental
* @alpha
*/
disableDocumentWideUndoRedo?: boolean;
}
}