@jupyterlab/notebook
Version:
JupyterLab - Notebook
529 lines • 16.4 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { ObservableMap } from '@jupyterlab/observables';
import * as models from '@jupyterlab/shared-models';
import { ArrayExt, ArrayIterator, each, toArray } from '@lumino/algorithm';
import { Signal } from '@lumino/signaling';
/**
* A cell list object that supports undo/redo.
*/
export class CellList {
/**
* Construct the cell list.
*/
constructor(modelDB, factory, model) {
/**
* Prevents that the modeldb event handler is executed when the shared-model event handler is executed and vice-versa.
*/
this._mutex = models.createMutex();
this._isDisposed = false;
this._changed = new Signal(this);
this._factory = factory;
this._cellOrder = modelDB.createList('cellOrder');
this._cellMap = new ObservableMap();
this._cellOrder.changed.connect(this._onOrderChanged, this);
this.nbmodel = model;
this.nbmodel.changed.connect(this.onSharedModelChanged, this);
this.changed.connect(this.onModelDBChanged, this);
}
onModelDBChanged(self, change) {
this._mutex(() => {
const nbmodel = this.nbmodel;
nbmodel.transact(() => {
if (change.type === 'set' || change.type === 'remove') {
nbmodel.deleteCellRange(change.oldIndex, change.oldIndex + change.oldValues.length);
}
if (change.type === 'set' ||
change.type === 'add' ||
change.type === 'move') {
const cells = change.newValues.map(cell => {
return cell.sharedModel.clone();
});
let insertLocation = change.newIndex;
if (change.type === 'move' && insertLocation > change.oldIndex) {
insertLocation += change.oldValues.length;
}
nbmodel.insertCells(insertLocation, cells);
change.newValues.forEach((cell, index) => {
cell.switchSharedModel(cells[index], false);
});
}
if (change.type === 'move') {
let from = change.oldIndex;
if (from >= change.newIndex) {
from += change.oldValues.length;
}
nbmodel.deleteCellRange(from, from + change.oldValues.length);
}
});
});
}
onSharedModelChanged(self, change) {
this._mutex(() => {
var _a;
let currpos = 0;
(_a = change.cellsChange) === null || _a === void 0 ? void 0 : _a.forEach(delta => {
if (delta.insert != null) {
const cells = delta.insert.map(nbcell => {
const cell = this._factory.createCell(nbcell.cell_type, {});
cell.switchSharedModel(nbcell, true);
return cell;
});
this.insertAll(currpos, cells);
currpos += delta.insert.length;
}
else if (delta.delete != null) {
this.removeRange(currpos, currpos + delta.delete);
}
else if (delta.retain != null) {
currpos += delta.retain;
}
});
});
}
/**
* A signal emitted when the cell list has changed.
*/
get changed() {
return this._changed;
}
/**
* Test whether the cell list has been disposed.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Test whether the list is empty.
*
* @returns `true` if the cell list is empty, `false` otherwise.
*
* #### Notes
* This is a read-only property.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* No changes.
*/
get isEmpty() {
return this._cellOrder.length === 0;
}
/**
* Get the length of the cell list.
*
* @return The number of cells in the cell list.
*
* #### Notes
* This is a read-only property.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* No changes.
*/
get length() {
return this._cellOrder.length;
}
/**
* Create an iterator over the cells in the cell list.
*
* @returns A new iterator starting at the front of the cell list.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* No changes.
*/
iter() {
const arr = [];
for (const id of toArray(this._cellOrder)) {
arr.push(this._cellMap.get(id));
}
return new ArrayIterator(arr);
}
/**
* Dispose of the resources held by the cell list.
*/
dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
Signal.clearData(this);
// Clean up the cell map and cell order objects.
for (const cell of this._cellMap.values()) {
cell.dispose();
}
this._cellMap.dispose();
this._cellOrder.dispose();
}
/**
* Get the cell at the specified index.
*
* @param index - The positive integer index of interest.
*
* @returns The cell at the specified index.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* No changes.
*
* #### Undefined Behavior
* An `index` which is non-integral or out of range.
*/
get(index) {
return this._cellMap.get(this._cellOrder.get(index));
}
/**
* Set the cell at the specified index.
*
* @param index - The positive integer index of interest.
*
* @param cell - The cell to set at the specified index.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* No changes.
*
* #### Undefined Behavior
* An `index` which is non-integral or out of range.
*
* #### Notes
* This should be considered to transfer ownership of the
* cell to the `CellList`. As such, `cell.dispose()` should
* not be called by other actors.
*/
set(index, cell) {
// Set the internal data structures.
this._cellMap.set(cell.id, cell);
this._cellOrder.set(index, cell.id);
}
/**
* Add a cell to the back of the cell list.
*
* @param cell - The cell to add to the back of the cell list.
*
* @returns The new length of the cell list.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* No changes.
*
* #### Notes
* This should be considered to transfer ownership of the
* cell to the `CellList`. As such, `cell.dispose()` should
* not be called by other actors.
*/
push(cell) {
// Set the internal data structures.
this._cellMap.set(cell.id, cell);
const num = this._cellOrder.push(cell.id);
return num;
}
/**
* Insert a cell into the cell list at a specific index.
*
* @param index - The index at which to insert the cell.
*
* @param cell - The cell to set at the specified index.
*
* @returns The new length of the cell list.
*
* #### Complexity
* Linear.
*
* #### Iterator Validity
* No changes.
*
* #### Notes
* The `index` will be clamped to the bounds of the cell list.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*
* #### Notes
* This should be considered to transfer ownership of the
* cell to the `CellList`. As such, `cell.dispose()` should
* not be called by other actors.
*/
insert(index, cell) {
// Set the internal data structures.
this._cellMap.set(cell.id, cell);
this._cellOrder.insert(index, cell.id);
}
/**
* Remove the first occurrence of a cell from the cell list.
*
* @param cell - The cell of interest.
*
* @returns The index of the removed cell, or `-1` if the cell
* is not contained in the cell list.
*
* #### Complexity
* Linear.
*
* #### Iterator Validity
* Iterators pointing at the removed cell and beyond are invalidated.
*/
removeValue(cell) {
const index = ArrayExt.findFirstIndex(toArray(this._cellOrder), id => this._cellMap.get(id) === cell);
this.remove(index);
return index;
}
/**
* Remove and return the cell at a specific index.
*
* @param index - The index of the cell of interest.
*
* @returns The cell at the specified index, or `undefined` if the
* index is out of range.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* Iterators pointing at the removed cell and beyond are invalidated.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*/
remove(index) {
const id = this._cellOrder.get(index);
this._cellOrder.remove(index);
const cell = this._cellMap.get(id);
return cell;
}
/**
* Remove all cells from the cell list.
*
* #### Complexity
* Linear.
*
* #### Iterator Validity
* All current iterators are invalidated.
*/
clear() {
this._cellOrder.clear();
}
/**
* Move a cell from one index to another.
*
* @parm fromIndex - The index of the element to move.
*
* @param toIndex - The index to move the element to.
*
* #### Complexity
* Constant.
*
* #### Iterator Validity
* Iterators pointing at the lesser of the `fromIndex` and the `toIndex`
* and beyond are invalidated.
*
* #### Undefined Behavior
* A `fromIndex` or a `toIndex` which is non-integral.
*/
move(fromIndex, toIndex) {
this._cellOrder.move(fromIndex, toIndex);
}
/**
* Push a set of cells to the back of the cell list.
*
* @param cells - An iterable or array-like set of cells to add.
*
* @returns The new length of the cell list.
*
* #### Complexity
* Linear.
*
* #### Iterator Validity
* No changes.
*
* #### Notes
* This should be considered to transfer ownership of the
* cells to the `CellList`. As such, `cell.dispose()` should
* not be called by other actors.
*/
pushAll(cells) {
const newValues = toArray(cells);
each(newValues, cell => {
// Set the internal data structures.
this._cellMap.set(cell.id, cell);
this._cellOrder.push(cell.id);
});
return this.length;
}
/**
* Insert a set of items into the cell list at the specified index.
*
* @param index - The index at which to insert the cells.
*
* @param cells - The cells to insert at the specified index.
*
* @returns The new length of the cell list.
*
* #### Complexity.
* Linear.
*
* #### Iterator Validity
* No changes.
*
* #### Notes
* The `index` will be clamped to the bounds of the cell list.
*
* #### Undefined Behavior.
* An `index` which is non-integral.
*
* #### Notes
* This should be considered to transfer ownership of the
* cells to the `CellList`. As such, `cell.dispose()` should
* not be called by other actors.
*/
insertAll(index, cells) {
const newValues = toArray(cells);
each(newValues, cell => {
this._cellMap.set(cell.id, cell);
// @todo it looks like this compound operation shoult start before the `each` loop.
this._cellOrder.beginCompoundOperation();
this._cellOrder.insert(index++, cell.id);
this._cellOrder.endCompoundOperation();
});
return this.length;
}
/**
* Remove a range of items from the cell list.
*
* @param startIndex - The start index of the range to remove (inclusive).
*
* @param endIndex - The end index of the range to remove (exclusive).
*
* @returns The new length of the cell list.
*
* #### Complexity
* Linear.
*
* #### Iterator Validity
* Iterators pointing to the first removed cell and beyond are invalid.
*
* #### Undefined Behavior
* A `startIndex` or `endIndex` which is non-integral.
*/
removeRange(startIndex, endIndex) {
this._cellOrder.removeRange(startIndex, endIndex);
return this.length;
}
/**
* Whether the object can redo changes.
*/
get canRedo() {
return this.nbmodel.canRedo();
}
/**
* Whether the object can undo changes.
*/
get canUndo() {
return this.nbmodel.canUndo();
}
/**
* Begin a compound operation.
*
* @param isUndoAble - Whether the operation is undoable.
* The default is `true`.
*/
beginCompoundOperation(isUndoAble) {
this._cellOrder.beginCompoundOperation(isUndoAble);
}
/**
* End a compound operation.
*/
endCompoundOperation() {
this._cellOrder.endCompoundOperation();
}
/**
* Undo an operation.
*/
undo() {
this.nbmodel.undo();
}
/**
* Redo an operation.
*/
redo() {
this.nbmodel.redo();
}
/**
* Clear the change stack.
*/
clearUndo() {
this.nbmodel.clearUndoHistory();
}
_onOrderChanged(order, change) {
if (change.type === 'add' || change.type === 'set') {
each(change.newValues, id => {
const existingCell = this._cellMap.get(id);
if (existingCell == null) {
const cellDB = this._factory.modelDB;
const cellType = cellDB.createValue(id + '.type');
let cell;
switch (cellType.get()) {
case 'code':
cell = this._factory.createCodeCell({ id: id });
break;
case 'markdown':
cell = this._factory.createMarkdownCell({ id: id });
break;
default:
cell = this._factory.createRawCell({ id: id });
break;
}
this._cellMap.set(id, cell);
}
else if (!existingCell.sharedModel.isStandalone) {
this._mutex(() => {
// it does already exist, probably because it was deleted previously and we introduced it
// copy it to a fresh codecell instance
const cell = existingCell.toJSON();
let freshCell = null;
switch (cell.cell_type) {
case 'code':
freshCell = this._factory.createCodeCell({ cell });
break;
case 'markdown':
freshCell = this._factory.createMarkdownCell({ cell });
break;
default:
freshCell = this._factory.createRawCell({ cell });
break;
}
this._cellMap.set(id, freshCell);
});
}
});
}
const newValues = [];
const oldValues = [];
each(change.newValues, id => {
newValues.push(this._cellMap.get(id));
});
each(change.oldValues, id => {
oldValues.push(this._cellMap.get(id));
});
this._changed.emit({
type: change.type,
oldIndex: change.oldIndex,
newIndex: change.newIndex,
oldValues,
newValues
});
}
}
//# sourceMappingURL=celllist.js.map