@jupyterlab/notebook
Version:
JupyterLab - Notebook
1,380 lines • 76.5 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Clipboard, Dialog, sessionContextDialogs, showDialog } from '@jupyterlab/apputils';
import { CodeCell, isCodeCellModel, isMarkdownCellModel, isRawCellModel, MarkdownCell } from '@jupyterlab/cells';
import { nullTranslator } from '@jupyterlab/translation';
import { ArrayExt, each, findIndex, toArray } from '@lumino/algorithm';
import { JSONExt } from '@lumino/coreutils';
import { ElementExt } from '@lumino/domutils';
import { Signal } from '@lumino/signaling';
import * as React from 'react';
/**
* The mimetype used for Jupyter cell data.
*/
const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
export class KernelError extends Error {
/**
* Construct the kernel error.
*/
constructor(content) {
const errorContent = content;
const errorName = errorContent.ename;
const errorValue = errorContent.evalue;
super(`KernelReplyNotOK: ${errorName} ${errorValue}`);
this.errorName = errorName;
this.errorValue = errorValue;
this.traceback = errorContent.traceback;
Object.setPrototypeOf(this, KernelError.prototype);
}
}
/**
* A collection of actions that run against notebooks.
*
* #### Notes
* All of the actions are a no-op if there is no model on the notebook.
* The actions set the widget `mode` to `'command'` unless otherwise specified.
* The actions will preserve the selection on the notebook widget unless
* otherwise specified.
*/
export class NotebookActions {
/**
* A signal that emits whenever a cell completes execution.
*/
static get executed() {
return Private.executed;
}
/**
* A signal that emits whenever a cell execution is scheduled.
*/
static get executionScheduled() {
return Private.executionScheduled;
}
/**
* A signal that emits whenever a cell execution is scheduled.
*/
static get selectionExecuted() {
return Private.selectionExecuted;
}
/**
* A private constructor for the `NotebookActions` class.
*
* #### Notes
* This class can never be instantiated. Its static member `executed` will be
* merged with the `NotebookActions` namespace. The reason it exists as a
* standalone class is because at run time, the `Private.executed` variable
* does not yet exist, so it needs to be referenced via a getter.
*/
constructor() {
// Intentionally empty.
}
}
/**
* A namespace for `NotebookActions` static methods.
*/
(function (NotebookActions) {
/**
* Split the active cell into two or more cells.
*
* @param notebook The target notebook widget.
*
* #### Notes
* It will preserve the existing mode.
* The last cell will be activated if no selection is found.
* If text was selected, the cell containing the selection will
* be activated.
* The existing selection will be cleared.
* The activated cell will have focus and the cursor will
* remain in the initial position.
* The leading whitespace in the second cell will be removed.
* If there is no content, two empty cells will be created.
* Both cells will have the same type as the original cell.
* This action can be undone.
*/
function splitCell(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
notebook.deselectAll();
const nbModel = notebook.model;
const index = notebook.activeCellIndex;
const child = notebook.widgets[index];
const editor = child.editor;
const selections = editor.getSelections();
const orig = child.model.value.text;
const offsets = [0];
let start = -1;
let end = -1;
for (let i = 0; i < selections.length; i++) {
// append start and end to handle selections
// cursors will have same start and end
start = editor.getOffsetAt(selections[i].start);
end = editor.getOffsetAt(selections[i].end);
if (start < end) {
offsets.push(start);
offsets.push(end);
}
else if (end < start) {
offsets.push(end);
offsets.push(start);
}
else {
offsets.push(start);
}
}
offsets.push(orig.length);
const clones = [];
for (let i = 0; i + 1 < offsets.length; i++) {
const clone = Private.cloneCell(nbModel, child.model);
clones.push(clone);
}
for (let i = 0; i < clones.length; i++) {
if (i !== clones.length - 1 && clones[i].type === 'code') {
clones[i].outputs.clear();
}
clones[i].value.text = orig
.slice(offsets[i], offsets[i + 1])
.replace(/^\n+/, '')
.replace(/\n+$/, '');
}
const cells = nbModel.cells;
cells.beginCompoundOperation();
for (let i = 0; i < clones.length; i++) {
if (i === 0) {
cells.set(index, clones[i]);
}
else {
cells.insert(index + i, clones[i]);
}
}
cells.endCompoundOperation();
// If there is a selection the selected cell will be activated
const activeCellDelta = start !== end ? 2 : 1;
notebook.activeCellIndex = index + clones.length - activeCellDelta;
const focusedEditor = notebook.activeCell.editor;
focusedEditor.focus();
Private.handleState(notebook, state);
}
NotebookActions.splitCell = splitCell;
/**
* Merge the selected cells.
*
* @param notebook - The target notebook widget.
*
* @param mergeAbove - If only one cell is selected, indicates whether to merge it
* with the cell above (true) or below (false, default).
*
* #### Notes
* The widget mode will be preserved.
* If only one cell is selected and `mergeAbove` is true, the above cell will be selected.
* If only one cell is selected and `mergeAbove` is false, the below cell will be selected.
* If the active cell is a code cell, its outputs will be cleared.
* This action can be undone.
* The final cell will have the same type as the active cell.
* If the active cell is a markdown cell, it will be unrendered.
*/
function mergeCells(notebook, mergeAbove = false) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
const toMerge = [];
const toDelete = [];
const model = notebook.model;
const cells = model.cells;
const primary = notebook.activeCell;
const active = notebook.activeCellIndex;
const attachments = {};
// Get the cells to merge.
notebook.widgets.forEach((child, index) => {
if (notebook.isSelectedOrActive(child)) {
toMerge.push(child.model.value.text);
if (index !== active) {
toDelete.push(child.model);
}
// Collect attachments if the cell is a markdown cell or a raw cell
const model = child.model;
if (isRawCellModel(model) || isMarkdownCellModel(model)) {
for (const key of model.attachments.keys) {
attachments[key] = model.attachments.get(key).toJSON();
}
}
}
});
// Check for only a single cell selected.
if (toMerge.length === 1) {
// Merge with the cell above when mergeAbove is true
if (mergeAbove === true) {
// Bail if it is the first cell.
if (active === 0) {
return;
}
// Otherwise merge with the previous cell.
const cellModel = cells.get(active - 1);
toMerge.unshift(cellModel.value.text);
toDelete.push(cellModel);
}
else if (mergeAbove === false) {
// Bail if it is the last cell.
if (active === cells.length - 1) {
return;
}
// Otherwise merge with the next cell.
const cellModel = cells.get(active + 1);
toMerge.push(cellModel.value.text);
toDelete.push(cellModel);
}
}
notebook.deselectAll();
// Create a new cell for the source to preserve history.
const newModel = Private.cloneCell(model, primary.model);
newModel.value.text = toMerge.join('\n\n');
if (isCodeCellModel(newModel)) {
newModel.outputs.clear();
}
else if (isMarkdownCellModel(newModel) || isRawCellModel(newModel)) {
newModel.attachments.fromJSON(attachments);
}
// Make the changes while preserving history.
cells.beginCompoundOperation();
cells.set(active, newModel);
toDelete.forEach(cell => {
cells.removeValue(cell);
});
cells.endCompoundOperation();
// If the original cell is a markdown cell, make sure
// the new cell is unrendered.
if (primary instanceof MarkdownCell) {
notebook.activeCell.rendered = false;
}
Private.handleState(notebook, state);
}
NotebookActions.mergeCells = mergeCells;
/**
* Delete the selected cells.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The cell after the last selected cell will be activated.
* It will add a code cell if all cells are deleted.
* This action can be undone.
*/
function deleteCells(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
Private.deleteCells(notebook);
Private.handleState(notebook, state, true);
}
NotebookActions.deleteCells = deleteCells;
/**
* Insert a new code cell above the active cell.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget mode will be preserved.
* This action can be undone.
* The existing selection will be cleared.
* The new cell will the active cell.
*/
function insertAbove(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
const model = notebook.model;
const cell = model.contentFactory.createCell(notebook.notebookConfig.defaultCell, {});
const active = notebook.activeCellIndex;
model.cells.insert(active, cell);
// Make the newly inserted cell active.
notebook.activeCellIndex = active;
notebook.deselectAll();
Private.handleState(notebook, state, true);
}
NotebookActions.insertAbove = insertAbove;
/**
* Insert a new code cell below the active cell.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget mode will be preserved.
* This action can be undone.
* The existing selection will be cleared.
* The new cell will be the active cell.
*/
function insertBelow(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
const model = notebook.model;
const cell = model.contentFactory.createCell(notebook.notebookConfig.defaultCell, {});
model.cells.insert(notebook.activeCellIndex + 1, cell);
// Make the newly inserted cell active.
notebook.activeCellIndex++;
notebook.deselectAll();
Private.handleState(notebook, state, true);
}
NotebookActions.insertBelow = insertBelow;
/**
* Move the selected cell(s) down.
*
* @param notebook = The target notebook widget.
*/
function moveDown(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
const cells = notebook.model.cells;
const widgets = notebook.widgets;
cells.beginCompoundOperation();
for (let i = cells.length - 2; i > -1; i--) {
if (notebook.isSelectedOrActive(widgets[i])) {
if (!notebook.isSelectedOrActive(widgets[i + 1])) {
cells.move(i, i + 1);
if (notebook.activeCellIndex === i) {
notebook.activeCellIndex++;
}
notebook.select(widgets[i + 1]);
notebook.deselect(widgets[i]);
}
}
}
cells.endCompoundOperation();
Private.handleState(notebook, state, true);
}
NotebookActions.moveDown = moveDown;
/**
* Move the selected cell(s) up.
*
* @param widget - The target notebook widget.
*/
function moveUp(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
const cells = notebook.model.cells;
const widgets = notebook.widgets;
cells.beginCompoundOperation();
for (let i = 1; i < cells.length; i++) {
if (notebook.isSelectedOrActive(widgets[i])) {
if (!notebook.isSelectedOrActive(widgets[i - 1])) {
cells.move(i, i - 1);
if (notebook.activeCellIndex === i) {
notebook.activeCellIndex--;
}
notebook.select(widgets[i - 1]);
notebook.deselect(widgets[i]);
}
}
}
cells.endCompoundOperation();
Private.handleState(notebook, state, true);
}
NotebookActions.moveUp = moveUp;
/**
* Change the selected cell type(s).
*
* @param notebook - The target notebook widget.
*
* @param value - The target cell type.
*
* #### Notes
* It should preserve the widget mode.
* This action can be undone.
* The existing selection will be cleared.
* Any cells converted to markdown will be unrendered.
*/
function changeCellType(notebook, value) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
Private.changeCellType(notebook, value);
Private.handleState(notebook, state);
}
NotebookActions.changeCellType = changeCellType;
/**
* Run the selected cell(s).
*
* @param notebook - The target notebook widget.
*
* @param sessionContext - The optional client session object.
*
* #### Notes
* The last selected cell will be activated, but not scrolled into view.
* The existing selection will be cleared.
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
*/
function run(notebook, sessionContext) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runSelected(notebook, sessionContext);
Private.handleRunState(notebook, state, false);
return promise;
}
NotebookActions.run = run;
/**
* Run the selected cell(s) and advance to the next cell.
*
* @param notebook - The target notebook widget.
*
* @param sessionContext - The optional client session object.
*
* #### Notes
* The existing selection will be cleared.
* The cell after the last selected cell will be activated and scrolled into view.
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
* If the last selected cell is the last cell, a new code cell
* will be created in `'edit'` mode. The new cell creation can be undone.
*/
function runAndAdvance(notebook, sessionContext) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runSelected(notebook, sessionContext);
const model = notebook.model;
if (notebook.activeCellIndex === notebook.widgets.length - 1) {
const cell = model.contentFactory.createCell(notebook.notebookConfig.defaultCell, {});
// Do not use push here, as we want an widget insertion
// to make sure no placeholder widget is rendered.
model.cells.insert(notebook.widgets.length, cell);
notebook.activeCellIndex++;
notebook.mode = 'edit';
}
else {
notebook.activeCellIndex++;
}
Private.handleRunState(notebook, state, true);
return promise;
}
NotebookActions.runAndAdvance = runAndAdvance;
/**
* Run the selected cell(s) and insert a new code cell.
*
* @param notebook - The target notebook widget.
*
* @param sessionContext - The optional client session object.
*
* #### Notes
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
* The widget mode will be set to `'edit'` after running.
* The existing selection will be cleared.
* The cell insert can be undone.
* The new cell will be scrolled into view.
*/
function runAndInsert(notebook, sessionContext) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
if (!Private.isNotebookRendered(notebook)) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runSelected(notebook, sessionContext);
const model = notebook.model;
const cell = model.contentFactory.createCell(notebook.notebookConfig.defaultCell, {});
model.cells.insert(notebook.activeCellIndex + 1, cell);
notebook.activeCellIndex++;
notebook.mode = 'edit';
Private.handleRunState(notebook, state, true);
return promise;
}
NotebookActions.runAndInsert = runAndInsert;
/**
* Run all of the cells in the notebook.
*
* @param notebook - The target notebook widget.
*
* @param sessionContext - The optional client session object.
*
* #### Notes
* The existing selection will be cleared.
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
* The last cell in the notebook will be activated and scrolled into view.
*/
function runAll(notebook, sessionContext) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
notebook.widgets.forEach(child => {
notebook.select(child);
});
const promise = Private.runSelected(notebook, sessionContext);
Private.handleRunState(notebook, state, true);
return promise;
}
NotebookActions.runAll = runAll;
function renderAllMarkdown(notebook, sessionContext) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const previousIndex = notebook.activeCellIndex;
const state = Private.getState(notebook);
notebook.widgets.forEach((child, index) => {
if (child.model.type === 'markdown') {
notebook.select(child);
// This is to make sure that the activeCell
// does not get executed
notebook.activeCellIndex = index;
}
});
if (notebook.activeCell.model.type !== 'markdown') {
return Promise.resolve(true);
}
const promise = Private.runSelected(notebook, sessionContext);
notebook.activeCellIndex = previousIndex;
Private.handleRunState(notebook, state, true);
return promise;
}
NotebookActions.renderAllMarkdown = renderAllMarkdown;
/**
* Run all of the cells before the currently active cell (exclusive).
*
* @param notebook - The target notebook widget.
*
* @param sessionContext - The optional client session object.
*
* #### Notes
* The existing selection will be cleared.
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
* The currently active cell will remain selected.
*/
function runAllAbove(notebook, sessionContext) {
const { activeCell, activeCellIndex, model } = notebook;
if (!model || !activeCell || activeCellIndex < 1) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
notebook.activeCellIndex--;
notebook.deselectAll();
for (let i = 0; i < notebook.activeCellIndex; ++i) {
notebook.select(notebook.widgets[i]);
}
const promise = Private.runSelected(notebook, sessionContext);
notebook.activeCellIndex++;
Private.handleRunState(notebook, state, true);
return promise;
}
NotebookActions.runAllAbove = runAllAbove;
/**
* Run all of the cells after the currently active cell (inclusive).
*
* @param notebook - The target notebook widget.
*
* @param sessionContext - The optional client session object.
*
* #### Notes
* The existing selection will be cleared.
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
* The last cell in the notebook will be activated and scrolled into view.
*/
function runAllBelow(notebook, sessionContext) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
notebook.deselectAll();
for (let i = notebook.activeCellIndex; i < notebook.widgets.length; ++i) {
notebook.select(notebook.widgets[i]);
}
const promise = Private.runSelected(notebook, sessionContext);
Private.handleRunState(notebook, state, true);
return promise;
}
NotebookActions.runAllBelow = runAllBelow;
/**
* Replaces the selection in the active cell of the notebook.
*
* @param notebook - The target notebook widget.
* @param text - The text to replace the selection.
*/
function replaceSelection(notebook, text) {
var _a, _b;
if (!notebook.model || !notebook.activeCell) {
return;
}
(_b = (_a = notebook.activeCell.editor).replaceSelection) === null || _b === void 0 ? void 0 : _b.call(_a, text);
}
NotebookActions.replaceSelection = replaceSelection;
/**
* Select the above the active cell.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget mode will be preserved.
* This is a no-op if the first cell is the active cell.
* This will skip any collapsed cells.
* The existing selection will be cleared.
*/
function selectAbove(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (notebook.activeCellIndex === 0) {
return;
}
let possibleNextCellIndex = notebook.activeCellIndex - 1;
// find first non hidden cell above current cell
while (possibleNextCellIndex >= 0) {
const possibleNextCell = notebook.widgets[possibleNextCellIndex];
if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) {
break;
}
possibleNextCellIndex -= 1;
}
const state = Private.getState(notebook);
notebook.activeCellIndex = possibleNextCellIndex;
notebook.deselectAll();
Private.handleState(notebook, state, true);
}
NotebookActions.selectAbove = selectAbove;
/**
* Select the cell below the active cell.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget mode will be preserved.
* This is a no-op if the last cell is the active cell.
* This will skip any collapsed cells.
* The existing selection will be cleared.
*/
function selectBelow(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
let maxCellIndex = notebook.widgets.length - 1;
// Find last non-hidden cell
while (notebook.widgets[maxCellIndex].isHidden ||
notebook.widgets[maxCellIndex].inputHidden) {
maxCellIndex -= 1;
}
if (notebook.activeCellIndex === maxCellIndex) {
return;
}
let possibleNextCellIndex = notebook.activeCellIndex + 1;
// find first non hidden cell below current cell
while (possibleNextCellIndex < maxCellIndex) {
let possibleNextCell = notebook.widgets[possibleNextCellIndex];
if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) {
break;
}
possibleNextCellIndex += 1;
}
const state = Private.getState(notebook);
notebook.activeCellIndex = possibleNextCellIndex;
notebook.deselectAll();
Private.handleState(notebook, state, true);
}
NotebookActions.selectBelow = selectBelow;
/**
* Extend the selection to the cell above.
*
* @param notebook - The target notebook widget.
* @param toTop - If true, denotes selection to extend to the top.
*
* #### Notes
* This is a no-op if the first cell is the active cell.
* The new cell will be activated.
*/
function extendSelectionAbove(notebook, toTop = false) {
if (!notebook.model || !notebook.activeCell) {
return;
}
// Do not wrap around.
if (notebook.activeCellIndex === 0) {
return;
}
const state = Private.getState(notebook);
notebook.mode = 'command';
// Check if toTop is true, if yes, selection is made to the top.
if (toTop) {
notebook.extendContiguousSelectionTo(0);
}
else {
notebook.extendContiguousSelectionTo(notebook.activeCellIndex - 1);
}
Private.handleState(notebook, state, true);
}
NotebookActions.extendSelectionAbove = extendSelectionAbove;
/**
* Extend the selection to the cell below.
*
* @param notebook - The target notebook widget.
* @param toBottom - If true, denotes selection to extend to the bottom.
*
* #### Notes
* This is a no-op if the last cell is the active cell.
* The new cell will be activated.
*/
function extendSelectionBelow(notebook, toBottom = false) {
if (!notebook.model || !notebook.activeCell) {
return;
}
// Do not wrap around.
if (notebook.activeCellIndex === notebook.widgets.length - 1) {
return;
}
const state = Private.getState(notebook);
notebook.mode = 'command';
// Check if toBottom is true, if yes selection is made to the bottom.
if (toBottom) {
notebook.extendContiguousSelectionTo(notebook.widgets.length - 1);
}
else {
notebook.extendContiguousSelectionTo(notebook.activeCellIndex + 1);
}
Private.handleState(notebook, state, true);
}
NotebookActions.extendSelectionBelow = extendSelectionBelow;
/**
* Select all of the cells of the notebook.
*
* @param notebook - the target notebook widget.
*/
function selectAll(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
notebook.widgets.forEach(child => {
notebook.select(child);
});
}
NotebookActions.selectAll = selectAll;
/**
* Deselect all of the cells of the notebook.
*
* @param notebook - the target notebook widget.
*/
function deselectAll(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
notebook.deselectAll();
}
NotebookActions.deselectAll = deselectAll;
/**
* Copy the selected cell(s) data to a clipboard.
*
* @param notebook - The target notebook widget.
*/
function copy(notebook) {
Private.copyOrCut(notebook, false);
}
NotebookActions.copy = copy;
/**
* Cut the selected cell data to a clipboard.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* This action can be undone.
* A new code cell is added if all cells are cut.
*/
function cut(notebook) {
if (!Private.isNotebookRendered(notebook)) {
return;
}
Private.copyOrCut(notebook, true);
}
NotebookActions.cut = cut;
/**
* Paste cells from the application clipboard.
*
* @param notebook - The target notebook widget.
*
* @param mode - the mode of adding cells:
* 'below' (default) adds cells below the active cell,
* 'belowSelected' adds cells below all selected cells,
* 'above' adds cells above the active cell, and
* 'replace' removes the currently selected cells and adds cells in their place.
*
* #### Notes
* The last pasted cell becomes the active cell.
* This is a no-op if there is no cell data on the clipboard.
* This action can be undone.
*/
function paste(notebook, mode = 'below') {
if (!Private.isNotebookRendered(notebook)) {
return;
}
const clipboard = Clipboard.getInstance();
if (!clipboard.hasData(JUPYTER_CELL_MIME)) {
return;
}
const values = clipboard.getData(JUPYTER_CELL_MIME);
addCells(notebook, mode, values, true);
}
NotebookActions.paste = paste;
/**
* Duplicate selected cells in the notebook without using the application clipboard.
*
* @param notebook - The target notebook widget.
*
* @param mode - the mode of adding cells:
* 'below' (default) adds cells below the active cell,
* 'belowSelected' adds cells below all selected cells,
* 'above' adds cells above the active cell, and
* 'replace' removes the currently selected cells and adds cells in their place.
*
* #### Notes
* The last pasted cell becomes the active cell.
* This is a no-op if there is no cell data on the clipboard.
* This action can be undone.
*/
function duplicate(notebook, mode = 'below') {
const values = Private.selectedCells(notebook);
if (!values || values.length === 0) {
return;
}
addCells(notebook, mode, values, false); // Cells not from the clipboard
}
NotebookActions.duplicate = duplicate;
/**
* Adds cells to the notebook.
*
* @param notebook - The target notebook widget.
*
* @param mode - the mode of adding cells:
* 'below' (default) adds cells below the active cell,
* 'belowSelected' adds cells below all selected cells,
* 'above' adds cells above the active cell, and
* 'replace' removes the currently selected cells and adds cells in their place.
*
* @param values — The cells to add to the notebook.
*
* @param cellsFromClipboard — True if the cells were sourced from the clipboard.
*
* #### Notes
* The last added cell becomes the active cell.
* This is a no-op if values is an empty array.
* This action can be undone.
*/
function addCells(notebook, mode = 'below', values, cellsFromClipboard = false) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
const model = notebook.model;
notebook.mode = 'command';
const newCells = values.map(cell => {
switch (cell.cell_type) {
case 'code':
if (notebook.lastClipboardInteraction === 'cut' &&
typeof cell.id === 'string') {
let cell_id = cell.id;
return model.contentFactory.createCodeCell({
id: cell_id,
cell: cell
});
}
else {
return model.contentFactory.createCodeCell({ cell });
}
case 'markdown':
return model.contentFactory.createMarkdownCell({ cell });
default:
return model.contentFactory.createRawCell({ cell });
}
});
const cells = notebook.model.cells;
let index;
cells.beginCompoundOperation();
// Set the starting index of the paste operation depending upon the mode.
switch (mode) {
case 'below':
index = notebook.activeCellIndex;
break;
case 'belowSelected':
notebook.widgets.forEach((child, childIndex) => {
if (notebook.isSelectedOrActive(child)) {
index = childIndex;
}
});
break;
case 'above':
index = notebook.activeCellIndex - 1;
break;
case 'replace': {
// Find the cells to delete.
const toDelete = [];
notebook.widgets.forEach((child, index) => {
const deletable = child.model.metadata.get('deletable') !== false;
if (notebook.isSelectedOrActive(child) && deletable) {
toDelete.push(index);
}
});
// If cells are not deletable, we may not have anything to delete.
if (toDelete.length > 0) {
// Delete the cells as one undo event.
toDelete.reverse().forEach(i => {
cells.remove(i);
});
}
index = toDelete[0];
break;
}
default:
break;
}
newCells.forEach(cell => {
cells.insert(++index, cell);
});
cells.endCompoundOperation();
notebook.activeCellIndex += newCells.length;
notebook.deselectAll();
if (cellsFromClipboard) {
notebook.lastClipboardInteraction = 'paste';
}
Private.handleState(notebook, state);
}
/**
* Undo a cell action.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* This is a no-op if if there are no cell actions to undo.
*/
function undo(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (!Private.isNotebookRendered(notebook)) {
return;
}
const state = Private.getState(notebook);
notebook.mode = 'command';
notebook.model.sharedModel.undo();
notebook.deselectAll();
Private.handleState(notebook, state);
}
NotebookActions.undo = undo;
/**
* Redo a cell action.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* This is a no-op if there are no cell actions to redo.
*/
function redo(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.mode = 'command';
notebook.model.sharedModel.redo();
notebook.deselectAll();
Private.handleState(notebook, state);
}
NotebookActions.redo = redo;
/**
* Toggle the line number of all cells.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The original state is based on the state of the active cell.
* The `mode` of the widget will be preserved.
*/
function toggleAllLineNumbers(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
const config = notebook.editorConfig;
const lineNumbers = !(config.code.lineNumbers &&
config.markdown.lineNumbers &&
config.raw.lineNumbers);
const newConfig = {
code: Object.assign(Object.assign({}, config.code), { lineNumbers }),
markdown: Object.assign(Object.assign({}, config.markdown), { lineNumbers }),
raw: Object.assign(Object.assign({}, config.raw), { lineNumbers })
};
notebook.editorConfig = newConfig;
Private.handleState(notebook, state);
}
NotebookActions.toggleAllLineNumbers = toggleAllLineNumbers;
/**
* Clear the code outputs of the selected cells.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget `mode` will be preserved.
*/
function clearOutputs(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
each(notebook.model.cells, (cell, index) => {
const child = notebook.widgets[index];
if (notebook.isSelectedOrActive(child) && cell.type === 'code') {
cell.clearExecution();
child.outputHidden = false;
}
});
Private.handleState(notebook, state, true);
}
NotebookActions.clearOutputs = clearOutputs;
/**
* Clear all the code outputs on the widget.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget `mode` will be preserved.
*/
function clearAllOutputs(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
each(notebook.model.cells, (cell, index) => {
const child = notebook.widgets[index];
if (cell.type === 'code') {
cell.clearExecution();
child.outputHidden = false;
}
});
Private.handleState(notebook, state, true);
}
NotebookActions.clearAllOutputs = clearAllOutputs;
/**
* Hide the code on selected code cells.
*
* @param notebook - The target notebook widget.
*/
function hideCode(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.inputHidden = true;
}
});
Private.handleState(notebook, state);
}
NotebookActions.hideCode = hideCode;
/**
* Show the code on selected code cells.
*
* @param notebook - The target notebook widget.
*/
function showCode(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.inputHidden = false;
}
});
Private.handleState(notebook, state);
}
NotebookActions.showCode = showCode;
/**
* Hide the code on all code cells.
*
* @param notebook - The target notebook widget.
*/
function hideAllCode(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (cell.model.type === 'code') {
cell.inputHidden = true;
}
});
Private.handleState(notebook, state);
}
NotebookActions.hideAllCode = hideAllCode;
/**
* Show the code on all code cells.
*
* @param widget - The target notebook widget.
*/
function showAllCode(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (cell.model.type === 'code') {
cell.inputHidden = false;
}
});
Private.handleState(notebook, state);
}
NotebookActions.showAllCode = showAllCode;
/**
* Hide the output on selected code cells.
*
* @param notebook - The target notebook widget.
*/
function hideOutput(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.outputHidden = true;
}
});
Private.handleState(notebook, state, true);
}
NotebookActions.hideOutput = hideOutput;
/**
* Show the output on selected code cells.
*
* @param notebook - The target notebook widget.
*/
function showOutput(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.outputHidden = false;
}
});
Private.handleState(notebook, state);
}
NotebookActions.showOutput = showOutput;
/**
* Hide the output on all code cells.
*
* @param notebook - The target notebook widget.
*/
function hideAllOutputs(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (cell.model.type === 'code') {
cell.outputHidden = true;
}
});
Private.handleState(notebook, state, true);
}
NotebookActions.hideAllOutputs = hideAllOutputs;
/**
* Render side-by-side.
*
* @param notebook - The target notebook widget.
*/
function renderSideBySide(notebook) {
notebook.renderingLayout = 'side-by-side';
}
NotebookActions.renderSideBySide = renderSideBySide;
/**
* Render not side-by-side.
*
* @param notebook - The target notebook widget.
*/
function renderDefault(notebook) {
notebook.renderingLayout = 'default';
}
NotebookActions.renderDefault = renderDefault;
/**
* Show the output on all code cells.
*
* @param notebook - The target notebook widget.
*/
function showAllOutputs(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (cell.model.type === 'code') {
cell.outputHidden = false;
}
});
Private.handleState(notebook, state);
}
NotebookActions.showAllOutputs = showAllOutputs;
/**
* Enable output scrolling for all selected cells.
*
* @param notebook - The target notebook widget.
*/
function enableOutputScrolling(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.outputsScrolled = true;
}
});
Private.handleState(notebook, state, true);
}
NotebookActions.enableOutputScrolling = enableOutputScrolling;
/**
* Disable output scrolling for all selected cells.
*
* @param notebook - The target notebook widget.
*/
function disableOutputScrolling(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.outputsScrolled = false;
}
});
Private.handleState(notebook, state);
}
NotebookActions.disableOutputScrolling = disableOutputScrolling;
/**
* Go to the last cell that is run or current if it is running.
*
* Note: This requires execution timing to be toggled on or this will have
* no effect.
*
* @param notebook - The target notebook widget.
*/
function selectLastRunCell(notebook) {
let latestTime = null;
let latestCellIdx = null;
notebook.widgets.forEach((cell, cellIndx) => {
if (cell.model.type === 'code') {
const execution = cell.model.metadata.get('execution');
if (execution &&
JSONExt.isObject(execution) &&
execution['iopub.status.busy'] !== undefined) {
// The busy status is used as soon as a request is received:
// https://jupyter-client.readthedocs.io/en/stable/messaging.html
const timestamp = execution['iopub.status.busy'].toString();
if (timestamp) {
const startTime = new Date(timestamp);
if (!latestTime || startTime >= latestTime) {
latestTime = startTime;
latestCellIdx = cellIndx;
}
}
}
}
});
if (latestCellIdx !== null) {
notebook.activeCellIndex = latestCellIdx;
}
}
NotebookActions.selectLastRunCell = selectLastRunCell;
/**
* Set the markdown header level.
*
* @param notebook - The target notebook widget.
*
* @param level - The header level.
*
* #### Notes
* All selected cells will be switched to markdown.
* The level will be clamped between 1 and 6.
* If there is an existing header, it will be replaced.
* There will always be one blank space after the header.
* The cells will be unrendered.
*/
function setMarkdownHeader(notebook, level) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
const cells = notebook.model.cells;
level = Math.min(Math.max(level, 1), 6);
notebook.widgets.forEach((child, index) => {
if (notebook.isSelectedOrActive(child)) {
Private.setMarkdownHeader(cells.get(index), level);
}
});
Private.changeCellType(notebook, 'markdown');
Private.handleState(notebook, state);
}
NotebookActions.setMarkdownHeader = setMarkdownHeader;
/**
* Collapse all cells in given notebook.
*
* @param notebook - The target notebook widget.
*/
function collapseAll(notebook) {
for (const cell of notebook.widgets) {
if (NotebookActions.getHeadingInfo(cell).isHeading) {
NotebookActions.setHeadingCollapse(cell, true, notebook);
NotebookActions.setCellCollapse(cell, true);
}
}
}
NotebookActions.collapseAll = collapseAll;
/**
* Un-collapse all cells in given notebook.
*
* @param notebook - The target notebook widget.
*/
function expandAllHeadings(notebook) {
for (const cell of notebook.widgets) {
if (NotebookActions.getHeadingInfo(cell).isHeading) {
NotebookActions.setHeadingCollapse(cell, false, notebook);
// similar to collapseAll.
NotebookActions.setCellCollapse(cell, false);
}
}
}
NotebookActions.expandAllHeadings = expandAllHeadings;
function findNearestParentHeader(cell, notebook) {
const index = findIndex(notebook.widgets, (possibleCell, index) => {
return cell.model.id === possibleCell.model.id;
});
if