@jupyterlab/cells
Version:
JupyterLab - Notebook Cells
960 lines (855 loc) • 22.9 kB
text/typescript
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { ISignal, Signal } from '@lumino/signaling';
import { AttachmentsModel, IAttachmentsModel } from '@jupyterlab/attachments';
import { CodeEditor } from '@jupyterlab/codeeditor';
import { IChangedArgs } from '@jupyterlab/coreutils';
import * as nbformat from '@jupyterlab/nbformat';
import { IObservableString, ObservableValue } from '@jupyterlab/observables';
import { IOutputAreaModel, OutputAreaModel } from '@jupyterlab/outputarea';
import {
CellChange,
createMutex,
createStandaloneCell,
IExecutionState,
IMapChange,
ISharedAttachmentsCell,
ISharedCell,
ISharedCodeCell,
ISharedMarkdownCell,
ISharedRawCell,
YCodeCell
} from '@jupyter/ydoc';
const globalModelDBMutex = createMutex();
/**
* The definition of a model object for a cell.
*/
export interface ICellModel extends CodeEditor.IModel {
/**
* The type of the cell.
*/
readonly type: nbformat.CellType;
/**
* A unique identifier for the cell.
*/
readonly id: string;
/**
* A signal emitted when the content of the model changes.
*/
readonly contentChanged: ISignal<ICellModel, void>;
/**
* A signal emitted when a model state changes.
*/
readonly stateChanged: ISignal<
ICellModel,
IChangedArgs<boolean, boolean, any>
>;
/**
* Whether the cell is trusted.
*/
trusted: boolean;
/**
* The metadata associated with the cell.
*
* ### 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: Omit<nbformat.IBaseCellMetadata, 'trusted'>;
/**
* Signal emitted when cell metadata changes.
*/
readonly metadataChanged: ISignal<ICellModel, IMapChange>;
/**
* The cell shared model.
*/
readonly sharedModel: ISharedCell;
/**
* 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;
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.ICell;
}
/**
* The definition of a model cell object for a cell with attachments.
*/
export interface IAttachmentsCellModel extends ICellModel {
/**
* The cell attachments
*/
readonly attachments: IAttachmentsModel;
}
/**
* The definition of a code cell.
*/
export interface ICodeCellModel extends ICellModel {
/**
* The type of the cell.
*
* #### Notes
* This is a read-only property.
*/
readonly type: 'code';
/**
* Whether the code cell has been edited since the last run.
*/
readonly isDirty: boolean;
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.ICodeCell;
/**
* The code cell's prompt number. Will be null if the cell has not been run.
*/
executionCount: nbformat.ExecutionCount;
/**
* The code cell's state.
*/
executionState: IExecutionState;
/**
* The cell outputs.
*/
readonly outputs: IOutputAreaModel;
/**
* Clear execution, outputs, and related metadata
*/
clearExecution(): void;
/**
* The code cell shared model
*/
readonly sharedModel: ISharedCodeCell;
}
/**
* The definition of a markdown cell.
*/
export interface IMarkdownCellModel extends IAttachmentsCellModel {
/**
* The type of the cell.
*/
readonly type: 'markdown';
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.IMarkdownCell;
}
/**
* The definition of a raw cell.
*/
export interface IRawCellModel extends IAttachmentsCellModel {
/**
* The type of the cell.
*/
readonly type: 'raw';
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.IRawCell;
}
export function isCodeCellModel(model: ICellModel): model is ICodeCellModel {
return model.type === 'code';
}
export function isMarkdownCellModel(
model: ICellModel
): model is IMarkdownCellModel {
return model.type === 'markdown';
}
export function isRawCellModel(model: ICellModel): model is IRawCellModel {
return model.type === 'raw';
}
/**
* An implementation of the cell model.
*/
export abstract class CellModel extends CodeEditor.Model implements ICellModel {
constructor(options: CellModel.IOptions<ISharedCell> = {}) {
const { cell_type, sharedModel, ...others } = options;
super({
sharedModel:
sharedModel ??
createStandaloneCell({
cell_type: cell_type ?? 'raw',
id: options.id
}),
...others
});
this.standaloneModel = typeof options.sharedModel === 'undefined';
this.trusted = !!this.getMetadata('trusted') || !!options.trusted;
this.sharedModel.changed.connect(this.onGenericChange, this);
this.sharedModel.metadataChanged.connect(this._onMetadataChanged, this);
}
readonly sharedModel: ISharedCell;
/**
* The type of cell.
*/
abstract get type(): nbformat.CellType;
/**
* A signal emitted when the state of the model changes.
*/
readonly contentChanged = new Signal<this, void>(this);
/**
* Signal emitted when cell metadata changes.
*/
get metadataChanged(): ISignal<ICellModel, IMapChange> {
return this._metadataChanged;
}
/**
* A signal emitted when a model state changes.
*/
readonly stateChanged = new Signal<
this,
IChangedArgs<
any,
any,
'isDirty' | 'trusted' | 'executionCount' | 'executionState'
>
>(this);
/**
* The id for the cell.
*/
get id(): string {
return this.sharedModel.getId();
}
/**
* The metadata associated with the cell.
*/
get metadata(): Omit<nbformat.IBaseCellMetadata, 'trusted'> {
return this.sharedModel.metadata;
}
/**
* The trusted state of the model.
*/
get trusted(): boolean {
return this._trusted;
}
set trusted(newValue: boolean) {
const oldValue = this.trusted;
if (oldValue !== newValue) {
this._trusted = newValue;
this.onTrustedChanged(this, { newValue, oldValue });
}
}
/**
* Dispose of the resources held by the model.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this.sharedModel.changed.disconnect(this.onGenericChange, this);
this.sharedModel.metadataChanged.disconnect(this._onMetadataChanged, this);
super.dispose();
}
/**
* Handle a change to the trusted state.
*
* The default implementation is a no-op.
*/
onTrustedChanged(
trusted: CellModel,
args: ObservableValue.IChangedArgs
): void {
/* no-op */
}
/**
* Delete a metadata
*
* @param key Metadata key
*/
deleteMetadata(key: string): any {
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 JSON.
*/
toJSON(): nbformat.ICell {
return this.sharedModel.toJSON();
}
/**
* Handle a change to the observable value.
*/
protected onGenericChange(): void {
this.contentChanged.emit(void 0);
}
private _onMetadataChanged(sender: ISharedCell, change: IMapChange) {
this._metadataChanged.emit(change);
}
private _metadataChanged = new Signal<this, IMapChange>(this);
private _trusted = false;
}
/**
* The namespace for `CellModel` statics.
*/
export namespace CellModel {
/**
* The options used to initialize a `CellModel`.
*/
export interface IOptions<T extends ISharedCell> {
/**
* A unique identifier for the model.
*/
id?: string;
/**
* The cell shared model.
*/
sharedModel?: T;
/**
* The cell type
*/
cell_type?: string;
/**
* Whether the cell is trusted or not.
*/
trusted?: boolean;
}
}
/**
* A base implementation for cell models with attachments.
*/
export abstract class AttachmentsCellModel extends CellModel {
/**
* Construct a new cell with optional attachments.
*/
constructor(options: AttachmentsCellModel.IOptions<ISharedCell>) {
super(options);
const factory =
options.contentFactory ?? AttachmentsCellModel.defaultContentFactory;
const values = (
this.sharedModel as ISharedAttachmentsCell
).getAttachments();
this._attachments = factory.createAttachmentsModel({ values });
this._attachments.stateChanged.connect(this.onGenericChange, this);
this._attachments.changed.connect(this._onAttachmentsChange, this);
this.sharedModel.changed.connect(this._onSharedModelChanged, this);
}
/**
* Get the attachments of the model.
*/
get attachments(): IAttachmentsModel {
return this._attachments;
}
/**
* Dispose of the resources held by the model.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._attachments.stateChanged.disconnect(this.onGenericChange, this);
this._attachments.changed.disconnect(this._onAttachmentsChange, this);
this._attachments.dispose();
this.sharedModel.changed.disconnect(this._onSharedModelChanged, this);
super.dispose();
}
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.IRawCell | nbformat.IMarkdownCell {
return super.toJSON() as nbformat.IRawCell | nbformat.IMarkdownCell;
}
/**
* Handle a change to the cell outputs modelDB and reflect it in the shared model.
*/
private _onAttachmentsChange(
sender: IAttachmentsModel,
event: IAttachmentsModel.ChangedArgs
): void {
const cell = this.sharedModel as ISharedAttachmentsCell;
globalModelDBMutex(() => cell.setAttachments(sender.toJSON()));
}
/**
* Handle a change to the code cell value.
*/
private _onSharedModelChanged(
slot: ISharedAttachmentsCell,
change: CellChange
): void {
if (change.attachmentsChange) {
const cell = this.sharedModel as ISharedAttachmentsCell;
globalModelDBMutex(() =>
this._attachments.fromJSON(cell.getAttachments() ?? {})
);
}
}
private _attachments: IAttachmentsModel;
}
/**
* The namespace for `AttachmentsCellModel` statics.
*/
export namespace AttachmentsCellModel {
/**
* The options used to initialize a `AttachmentsCellModel`.
*/
export interface IOptions<T extends ISharedCell>
extends CellModel.IOptions<T> {
/**
* The factory for attachment model creation.
*/
contentFactory?: IContentFactory;
}
/**
* A factory for creating code cell model content.
*/
export interface IContentFactory {
/**
* Create an output area.
*/
createAttachmentsModel(
options: IAttachmentsModel.IOptions
): IAttachmentsModel;
}
/**
* The default implementation of an `IContentFactory`.
*/
export class ContentFactory implements IContentFactory {
/**
* Create an attachments model.
*/
createAttachmentsModel(
options: IAttachmentsModel.IOptions
): IAttachmentsModel {
return new AttachmentsModel(options);
}
}
/**
* The shared `ContentFactory` instance.
*/
export const defaultContentFactory = new ContentFactory();
}
/**
* An implementation of a raw cell model.
*/
export class RawCellModel extends AttachmentsCellModel {
/**
* Construct a raw cell model from optional shared model.
*/
constructor(
options: Omit<
AttachmentsCellModel.IOptions<ISharedRawCell>,
'cell_type'
> = {}
) {
super({
cell_type: 'raw',
...options
});
}
/**
* The type of the cell.
*/
get type(): 'raw' {
return 'raw';
}
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.IRawCell {
return super.toJSON() as nbformat.IRawCell;
}
}
/**
* An implementation of a markdown cell model.
*/
export class MarkdownCellModel extends AttachmentsCellModel {
/**
* Construct a markdown cell model from optional shared model.
*/
constructor(
options: Omit<
AttachmentsCellModel.IOptions<ISharedMarkdownCell>,
'cell_type'
> = {}
) {
super({
cell_type: 'markdown',
...options
});
// Use the Github-flavored markdown mode.
this.mimeType = 'text/x-ipythongfm';
}
/**
* The type of the cell.
*/
get type(): 'markdown' {
return 'markdown';
}
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.IMarkdownCell {
return super.toJSON() as nbformat.IMarkdownCell;
}
}
/**
* An implementation of a code cell Model.
*/
export class CodeCellModel extends CellModel implements ICodeCellModel {
/**
* Construct a new code cell with optional original cell content.
*/
constructor(options: CodeCellModel.IOptions = {}) {
super({
cell_type: 'code',
...options
});
const factory =
options?.contentFactory ?? CodeCellModel.defaultContentFactory;
const trusted = this.trusted;
const outputs = this.sharedModel.getOutputs();
this._outputs = factory.createOutputArea({ trusted, values: outputs });
this.sharedModel.changed.connect(this._onSharedModelChanged, this);
this._outputs.changed.connect(this.onGenericChange, this);
this._outputs.changed.connect(this.onOutputsChange, this);
}
/**
* The type of the cell.
*/
get type(): 'code' {
return 'code';
}
/**
* The execution count of the cell.
*/
get executionCount(): nbformat.ExecutionCount {
return this.sharedModel.execution_count || null;
}
set executionCount(newValue: nbformat.ExecutionCount) {
this.sharedModel.execution_count = newValue || null;
}
/**
* The execution state of the cell.
*/
get executionState(): IExecutionState {
return this.sharedModel.executionState;
}
set executionState(newValue: IExecutionState) {
this.sharedModel.executionState = newValue;
}
/**
* Whether the cell is dirty or not.
*
* A cell is dirty if it is output is not empty and does not
* result of the input code execution.
*/
get isDirty(): boolean {
// Test could be done dynamically with this._executedCode
// but for performance reason, the diff status is stored in a boolean.
return this._isDirty;
}
/**
* Public Set whether the cell is dirty or not.
*/
set isDirty(dirty: boolean) {
this._setDirty(dirty);
}
/**
* The cell outputs.
*/
get outputs(): IOutputAreaModel {
return this._outputs;
}
readonly sharedModel: ISharedCodeCell;
clearExecution(): void {
this.outputs.clear();
this.executionCount = null;
this.executionState = 'idle';
this._setDirty(false);
this.sharedModel.deleteMetadata('execution');
// We trust this cell as it no longer has any outputs.
this.trusted = true;
}
/**
* Dispose of the resources held by the model.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this.sharedModel.changed.disconnect(this._onSharedModelChanged, this);
this._outputs.changed.disconnect(this.onGenericChange, this);
this._outputs.changed.disconnect(this.onOutputsChange, this);
this._outputs.dispose();
this._outputs = null!;
super.dispose();
}
/**
* Handle a change to the trusted state.
*/
onTrustedChanged(
trusted: CellModel,
args: ObservableValue.IChangedArgs
): void {
const newTrusted = args.newValue as boolean;
if (this._outputs) {
this._outputs.trusted = newTrusted;
}
if (newTrusted) {
const codeCell = this.sharedModel as YCodeCell;
const metadata = codeCell.getMetadata();
metadata.trusted = true;
codeCell.setMetadata(metadata);
}
this.stateChanged.emit({
name: 'trusted',
oldValue: args.oldValue as boolean,
newValue: newTrusted
});
}
/**
* Serialize the model to JSON.
*/
toJSON(): nbformat.ICodeCell {
return super.toJSON() as nbformat.ICodeCell;
}
/**
* Handle a change to the cell outputs modelDB and reflect it in the shared model.
*/
protected onOutputsChange(
sender: IOutputAreaModel,
event: IOutputAreaModel.ChangedArgs
): void {
const codeCell = this.sharedModel as YCodeCell;
globalModelDBMutex(() => {
switch (event.type) {
case 'add': {
for (const output of event.newValues) {
if (output.type === 'stream') {
output.streamText!.changed.connect(
(
sender: IObservableString,
textEvent: IObservableString.IChangedArgs
) => {
if (
textEvent.options !== undefined &&
(textEvent.options as { [key: string]: any })['silent']
) {
return;
}
const codeCell = this.sharedModel as YCodeCell;
if (textEvent.type === 'remove') {
codeCell.removeStreamOutput(
event.newIndex,
textEvent.start,
'silent-change'
);
} else {
codeCell.appendStreamOutput(
event.newIndex,
textEvent.value,
'silent-change'
);
}
},
this
);
}
}
const outputs = event.newValues.map(output => output.toJSON());
codeCell.updateOutputs(
event.newIndex,
event.newIndex,
outputs,
'silent-change'
);
break;
}
case 'set': {
const newValues = event.newValues.map(output => output.toJSON());
codeCell.updateOutputs(
event.oldIndex,
event.oldIndex + newValues.length,
newValues,
'silent-change'
);
break;
}
case 'remove':
codeCell.updateOutputs(
event.oldIndex,
event.oldValues.length,
[],
'silent-change'
);
break;
default:
throw new Error(`Invalid event type: ${event.type}`);
}
});
}
/**
* Handle a change to the code cell value.
*/
private _onSharedModelChanged(
slot: ISharedCodeCell,
change: CellChange
): void {
if (change.streamOutputChange) {
globalModelDBMutex(() => {
for (const streamOutputChange of change.streamOutputChange!) {
if ('delete' in streamOutputChange) {
this._outputs.removeStreamOutput(streamOutputChange.delete!);
}
if ('insert' in streamOutputChange) {
this._outputs.appendStreamOutput(
streamOutputChange.insert!.toString()
);
}
}
});
}
if (change.outputsChange) {
globalModelDBMutex(() => {
let retain = 0;
for (const outputsChange of change.outputsChange!) {
if ('retain' in outputsChange) {
retain += outputsChange.retain!;
}
if ('delete' in outputsChange) {
for (let i = 0; i < outputsChange.delete!; i++) {
this._outputs.remove(retain);
}
}
if ('insert' in outputsChange) {
// Inserting an output always results in appending it.
for (const output of outputsChange.insert!) {
// For compatibility with older ydoc where a plain object,
// (rather than a Map instance) could be provided.
// In a future major release the use of Map will be required.
this._outputs.add('toJSON' in output ? output.toJSON() : output);
}
}
}
});
}
if (change.executionCountChange) {
if (
change.executionCountChange.newValue &&
(this.isDirty || !change.executionCountChange.oldValue)
) {
this._setDirty(false);
}
this.stateChanged.emit({
name: 'executionCount',
oldValue: change.executionCountChange.oldValue,
newValue: change.executionCountChange.newValue
});
}
if (change.executionStateChange) {
this.stateChanged.emit({
name: 'executionState',
oldValue: change.executionStateChange.oldValue,
newValue: change.executionStateChange.newValue
});
}
if (change.sourceChange && this.executionCount !== null) {
this._setDirty(
this._executedCode !== this.sharedModel.getSource().trim()
);
}
}
/**
* Set whether the cell is dirty or not.
*/
private _setDirty(v: boolean) {
if (!v) {
this._executedCode = this.sharedModel.getSource().trim();
}
if (v !== this._isDirty) {
this._isDirty = v;
this.stateChanged.emit({
name: 'isDirty',
oldValue: !v,
newValue: v
});
}
}
private _executedCode = '';
private _isDirty = false;
private _outputs: IOutputAreaModel;
}
/**
* The namespace for `CodeCellModel` statics.
*/
export namespace CodeCellModel {
/**
* The options used to initialize a `CodeCellModel`.
*/
export interface IOptions
extends Omit<CellModel.IOptions<ISharedCodeCell>, 'cell_type'> {
/**
* The factory for output area model creation.
*/
contentFactory?: IContentFactory;
}
/**
* A factory for creating code cell model content.
*/
export interface IContentFactory {
/**
* Create an output area.
*/
createOutputArea(options: IOutputAreaModel.IOptions): IOutputAreaModel;
}
/**
* The default implementation of an `IContentFactory`.
*/
export class ContentFactory implements IContentFactory {
/**
* Create an output area.
*/
createOutputArea(options: IOutputAreaModel.IOptions): IOutputAreaModel {
return new OutputAreaModel(options);
}
}
/**
* The shared `ContentFactory` instance.
*/
export const defaultContentFactory = new ContentFactory();
}