@jupyterlab/notebook
Version:
JupyterLab - Notebook
1,289 lines (1,288 loc) • 96.3 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Clipboard, Dialog, showDialog } from '@jupyterlab/apputils';
import { CodeCellModel, isMarkdownCellModel, isRawCellModel, MarkdownCell } from '@jupyterlab/cells';
import { Notification } from '@jupyterlab/apputils';
import { signalToPromise } from '@jupyterlab/coreutils';
import { nullTranslator } from '@jupyterlab/translation';
import { every, findIndex } from '@lumino/algorithm';
import { JSONExt } from '@lumino/coreutils';
import { Signal } from '@lumino/signaling';
import * as React from 'react';
import { runCell as defaultRunCell } from './cellexecutor';
/**
* 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 when one notebook's cells are all executed.
*/
static get selectionExecuted() {
return Private.selectionExecuted;
}
/**
* A signal that emits when a cell's output is cleared.
*/
static get outputCleared() {
return Private.outputCleared;
}
/**
* 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.
* The original cell is preserved to maintain kernel connections.
*/
function splitCell(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
// We force the notebook back in edit mode as splitting a cell
// requires using the cursor position within a cell (aka it was recently in edit mode)
// However the focus may be stolen if the action is triggered
// from the menu entry; switching the notebook in command mode.
notebook.mode = 'edit';
notebook.deselectAll();
const nbModel = notebook.model;
const index = notebook.activeCellIndex;
const child = notebook.widgets[index];
const editor = child.editor;
if (!editor) {
// TODO
return;
}
const selections = editor.getSelections();
const orig = child.model.sharedModel.getSource();
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);
// Create new cells for all content pieces EXCEPT the last one
// The last piece will remain in the original cell to preserve kernel connection
const newCells = offsets.slice(0, -2).map((offset, offsetIdx) => {
const { cell_type, metadata } = child.model.sharedModel.toJSON();
return {
cell_type,
metadata,
source: orig
.slice(offset, offsets[offsetIdx + 1])
.replace(/^\n+/, '')
.replace(/\n+$/, ''),
outputs: undefined
};
});
// Prepare the content for the original cell (last piece)
const lastPieceStart = offsets[offsets.length - 2];
const lastPieceEnd = offsets[offsets.length - 1];
const lastPieceContent = orig
.slice(lastPieceStart, lastPieceEnd)
.replace(/^\n+/, '')
.replace(/\n+$/, '');
nbModel.sharedModel.transact(() => {
// Insert new cells above the current cell (if any)
if (newCells.length > 0) {
nbModel.sharedModel.insertCells(index, newCells);
}
// Update the original cell with the last piece of content
child.model.sharedModel.setSource(lastPieceContent);
// Mark cell as dirty if it is running
if (child.model instanceof CodeCellModel) {
const codeCellModel = child.model;
if (codeCellModel.executionState === 'running') {
codeCellModel.isDirty = true;
}
}
});
// If there was a selection, activate the cell containing the selection
let targetCellIndex;
if (start !== end) {
// Find which piece contains the selection
let selectionPieceIndex = 0;
for (let i = 0; i < offsets.length - 1; i++) {
if (start >= offsets[i] && start < offsets[i + 1]) {
selectionPieceIndex = i;
break;
}
}
targetCellIndex = index + selectionPieceIndex;
}
else {
// No selection, activate the original cell (now at the end)
targetCellIndex = index + newCells.length;
}
notebook.activeCellIndex = targetCellIndex;
notebook
.scrollToItem(notebook.activeCellIndex)
.then(() => {
var _a;
(_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.editor.focus();
})
.catch(reason => {
// no-op
});
void 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;
}
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.sharedModel.getSource());
if (index !== active) {
toDelete.push(index);
}
// 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.sharedModel.getSource());
toDelete.push(active - 1);
}
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.sharedModel.getSource());
toDelete.push(active + 1);
}
}
notebook.deselectAll();
const primaryModel = primary.model.sharedModel;
const { cell_type, metadata } = primaryModel.toJSON();
if (primaryModel.cell_type === 'code') {
// We can trust this cell because the outputs will be removed.
metadata.trusted = true;
}
const newModel = {
cell_type,
metadata,
source: toMerge.join('\n\n'),
attachments: primaryModel.cell_type === 'markdown' ||
primaryModel.cell_type === 'raw'
? attachments
: undefined
};
// Make the changes while preserving history.
model.sharedModel.transact(() => {
model.sharedModel.deleteCell(active);
model.sharedModel.insertCell(active, newModel);
toDelete
.sort((a, b) => b - a)
.forEach(index => {
model.sharedModel.deleteCell(index);
});
});
// If the original cell is a markdown cell, make sure
// the new cell is unrendered.
if (primary instanceof MarkdownCell) {
notebook.activeCell.rendered = false;
}
void 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;
}
const state = Private.getState(notebook);
Private.deleteCells(notebook);
void Private.handleState(notebook, state, true);
}
NotebookActions.deleteCells = deleteCells;
/**
* Insert a new code cell above the active cell or in index 0 if the notebook is empty.
*
* @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) {
return;
}
const state = Private.getState(notebook);
const model = notebook.model;
const newIndex = notebook.activeCell ? notebook.activeCellIndex : 0;
model.sharedModel.insertCell(newIndex, {
cell_type: notebook.notebookConfig.defaultCell,
metadata: notebook.notebookConfig.defaultCell === 'code'
? {
// This is an empty cell created by user, thus is trusted
trusted: true
}
: {}
});
// Make the newly inserted cell active.
notebook.activeCellIndex = newIndex;
notebook.deselectAll();
void Private.handleState(notebook, state, true);
}
NotebookActions.insertAbove = insertAbove;
/**
* Insert a new code cell below the active cell or in index 0 if the notebook is empty.
*
* @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) {
return;
}
const state = Private.getState(notebook);
const model = notebook.model;
const newIndex = notebook.activeCell ? notebook.activeCellIndex + 1 : 0;
model.sharedModel.insertCell(newIndex, {
cell_type: notebook.notebookConfig.defaultCell,
metadata: notebook.notebookConfig.defaultCell === 'code'
? {
// This is an empty cell created by user, thus is trusted
trusted: true
}
: {}
});
// Make the newly inserted cell active.
notebook.activeCellIndex = newIndex;
notebook.deselectAll();
void Private.handleState(notebook, state, true);
}
NotebookActions.insertBelow = insertBelow;
function move(notebook, shift) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
const firstIndex = notebook.widgets.findIndex(w => notebook.isSelectedOrActive(w));
let lastIndex = notebook.widgets
.slice(firstIndex + 1)
.findIndex(w => !notebook.isSelectedOrActive(w));
if (lastIndex >= 0) {
lastIndex += firstIndex + 1;
}
else {
lastIndex = notebook.model.cells.length;
}
if (shift > 0) {
notebook.moveCell(firstIndex, lastIndex, lastIndex - firstIndex);
}
else {
notebook.moveCell(firstIndex, firstIndex + shift, lastIndex - firstIndex);
}
void Private.handleState(notebook, state, true);
}
/**
* Move the selected cell(s) down.
*
* @param notebook = The target notebook widget.
*/
function moveDown(notebook) {
move(notebook, 1);
}
NotebookActions.moveDown = moveDown;
/**
* Move the selected cell(s) up.
*
* @param notebook - The target notebook widget.
*/
function moveUp(notebook) {
move(notebook, -1);
}
NotebookActions.moveUp = moveUp;
/**
* Change the selected cell type(s).
*
* @param notebook - The target notebook widget.
* @param value - The target cell type.
* @param translator - The application translator.
*
* #### 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, translator) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
Private.changeCellType(notebook, value, translator);
void Private.handleState(notebook, state);
}
NotebookActions.changeCellType = changeCellType;
/**
* Run the selected cell(s).
*
* @param notebook - The target notebook widget.
* @param sessionContext - The client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### 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, sessionDialogs, translator) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runSelected(notebook, sessionContext, sessionDialogs, translator);
void Private.handleRunState(notebook, state);
return promise;
}
NotebookActions.run = run;
/**
* Run specified cells.
*
* @param notebook - The target notebook widget.
* @param cells - The cells to run.
* @param sessionContext - The client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### Notes
* The existing selection will be preserved.
* The mode will be changed to command.
* An execution error will prevent the remaining code cells from executing.
* All markdown cells will be rendered.
*/
function runCells(notebook, cells, sessionContext, sessionDialogs, translator) {
if (!notebook.model) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runCells(notebook, cells, sessionContext, sessionDialogs, translator);
void Private.handleRunState(notebook, state);
return promise;
}
NotebookActions.runCells = runCells;
/**
* Run the selected cell(s) and advance to the next cell.
*
* @param notebook - The target notebook widget.
* @param sessionContext - The client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### 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.
*/
async function runAndAdvance(notebook, sessionContext, sessionDialogs, translator) {
var _a;
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runSelected(notebook, sessionContext, sessionDialogs, translator);
const model = notebook.model;
if (notebook.activeCellIndex === notebook.widgets.length - 1) {
// Do not use push here, as we want an widget insertion
// to make sure no placeholder widget is rendered.
model.sharedModel.insertCell(notebook.widgets.length, {
cell_type: notebook.notebookConfig.defaultCell,
metadata: notebook.notebookConfig.defaultCell === 'code'
? {
// This is an empty cell created by user, thus is trusted
trusted: true
}
: {}
});
notebook.activeCellIndex++;
if (((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.inViewport) === false) {
await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(() => {
// no-op
});
}
notebook.mode = 'edit';
}
else {
notebook.activeCellIndex++;
}
// If a cell is outside of viewport and scrolling is needed, the `smart`
// logic in `handleRunState` will choose appropriate alignment, except
// for the case of a small cell less than one viewport away for which it
// would use the `auto` heuristic, for which we set the preferred alignment
// to `center` as in most cases there will be space below and above a cell
// that is smaller than (or approximately equal to) the viewport size.
void Private.handleRunState(notebook, state, 'center');
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 client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### 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.
*/
async function runAndInsert(notebook, sessionContext, sessionDialogs, translator) {
var _a;
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runSelected(notebook, sessionContext, sessionDialogs, translator);
const model = notebook.model;
model.sharedModel.insertCell(notebook.activeCellIndex + 1, {
cell_type: notebook.notebookConfig.defaultCell,
metadata: notebook.notebookConfig.defaultCell === 'code'
? {
// This is an empty cell created by user, thus is trusted
trusted: true
}
: {}
});
notebook.activeCellIndex++;
if (((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.inViewport) === false) {
await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(() => {
// no-op
});
}
notebook.mode = 'edit';
void Private.handleRunState(notebook, state, 'center');
return promise;
}
NotebookActions.runAndInsert = runAndInsert;
/**
* Run all of the cells in the notebook.
*
* @param notebook - The target notebook widget.
* @param sessionContext - The client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### 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, sessionDialogs, translator) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const lastIndex = notebook.widgets.length;
const promise = Private.runCells(notebook, notebook.widgets, sessionContext, sessionDialogs, translator);
notebook.activeCellIndex = lastIndex;
notebook.deselectAll();
void Private.handleRunState(notebook, state);
return promise;
}
NotebookActions.runAll = runAll;
function renderAllMarkdown(notebook) {
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);
notebook.activeCellIndex = previousIndex;
void Private.handleRunState(notebook, state);
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 client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### 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, sessionDialogs, translator) {
const { activeCell, activeCellIndex, model } = notebook;
if (!model || !activeCell || activeCellIndex < 1) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const promise = Private.runCells(notebook, notebook.widgets.slice(0, notebook.activeCellIndex), sessionContext, sessionDialogs, translator);
notebook.deselectAll();
void Private.handleRunState(notebook, state);
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 client session object.
* @param sessionDialogs - The session dialogs.
* @param translator - The application translator.
*
* #### 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, sessionDialogs, translator) {
if (!notebook.model || !notebook.activeCell) {
return Promise.resolve(false);
}
const state = Private.getState(notebook);
const lastIndex = notebook.widgets.length;
const promise = Private.runCells(notebook, notebook.widgets.slice(notebook.activeCellIndex), sessionContext, sessionDialogs, translator);
notebook.activeCellIndex = lastIndex;
notebook.deselectAll();
void Private.handleRunState(notebook, state);
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, _c;
if (!notebook.model || !((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.editor)) {
return;
}
(_c = (_b = notebook.activeCell.editor).replaceSelection) === null || _c === void 0 ? void 0 : _c.call(_b, 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;
}
const footer = notebook.layout.footer;
if (footer && document.activeElement === footer.node) {
footer.node.blur();
notebook.mode = 'command';
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();
void 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) {
const footer = notebook.layout.footer;
footer === null || footer === void 0 ? void 0 : footer.node.focus();
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();
void Private.handleState(notebook, state, true);
}
NotebookActions.selectBelow = selectBelow;
/** Insert new heading of same level above active cell.
*
* @param notebook - The target notebook widget
*/
async function insertSameLevelHeadingAbove(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
let headingLevel = Private.Headings.determineHeadingLevel(notebook.activeCell, notebook);
if (headingLevel == -1) {
await Private.Headings.insertHeadingAboveCellIndex(0, 1, notebook);
}
else {
await Private.Headings.insertHeadingAboveCellIndex(notebook.activeCellIndex, headingLevel, notebook);
}
}
NotebookActions.insertSameLevelHeadingAbove = insertSameLevelHeadingAbove;
/** Insert new heading of same level at end of current section.
*
* @param notebook - The target notebook widget
*/
async function insertSameLevelHeadingBelow(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
let headingLevel = Private.Headings.determineHeadingLevel(notebook.activeCell, notebook);
headingLevel = headingLevel > -1 ? headingLevel : 1;
let cellIdxOfHeadingBelow = Private.Headings.findLowerEqualLevelHeadingBelow(notebook.activeCell, notebook, true);
await Private.Headings.insertHeadingAboveCellIndex(cellIdxOfHeadingBelow == -1
? notebook.model.cells.length
: cellIdxOfHeadingBelow, headingLevel, notebook);
}
NotebookActions.insertSameLevelHeadingBelow = insertSameLevelHeadingBelow;
/**
* Select the heading above the active cell or, if already at heading, collapse it.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget mode will be preserved.
* This is a no-op if the active cell is the topmost heading in collapsed state
* The existing selection will be cleared.
*/
function selectHeadingAboveOrCollapseHeading(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
let hInfoActiveCell = getHeadingInfo(notebook.activeCell);
// either collapse or find the right heading to jump to
if (hInfoActiveCell.isHeading && !hInfoActiveCell.collapsed) {
setHeadingCollapse(notebook.activeCell, true, notebook);
}
else {
let targetHeadingCellIdx = Private.Headings.findLowerEqualLevelParentHeadingAbove(notebook.activeCell, notebook, true);
if (targetHeadingCellIdx > -1) {
notebook.activeCellIndex = targetHeadingCellIdx;
}
}
// clear selection and handle state
notebook.deselectAll();
void Private.handleState(notebook, state, true);
}
NotebookActions.selectHeadingAboveOrCollapseHeading = selectHeadingAboveOrCollapseHeading;
/**
* Select the heading below the active cell or, if already at heading, expand it.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The widget mode will be preserved.
* This is a no-op if the active cell is the last heading in expanded state
* The existing selection will be cleared.
*/
function selectHeadingBelowOrExpandHeading(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
let hInfo = getHeadingInfo(notebook.activeCell);
if (hInfo.isHeading && hInfo.collapsed) {
setHeadingCollapse(notebook.activeCell, false, notebook);
}
else {
let targetHeadingCellIdx = Private.Headings.findHeadingBelow(notebook.activeCell, notebook, true // return index of heading cell
);
if (targetHeadingCellIdx > -1) {
notebook.activeCellIndex = targetHeadingCellIdx;
}
}
notebook.deselectAll();
void Private.handleState(notebook, state, true);
}
NotebookActions.selectHeadingBelowOrExpandHeading = selectHeadingBelowOrExpandHeading;
/**
* 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);
}
void 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);
}
void 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) {
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') {
const clipboard = Clipboard.getInstance();
if (!clipboard.hasData(JUPYTER_CELL_MIME)) {
return;
}
const values = clipboard.getData(JUPYTER_CELL_MIME);
addCells(notebook, mode, values, true);
void focusActiveCell(notebook);
}
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';
let index = 0;
const prevActiveCellIndex = notebook.activeCellIndex;
model.sharedModel.transact(() => {
// Set the starting index of the paste operation depending upon the mode.
switch (mode) {
case 'below':
index = notebook.activeCellIndex + 1;
break;
case 'belowSelected':
notebook.widgets.forEach((child, childIndex) => {
if (notebook.isSelectedOrActive(child)) {
index = childIndex + 1;
}
});
break;
case 'above':
index = notebook.activeCellIndex;
break;
case 'replace': {
// Find the cells to delete.
const toDelete = [];
notebook.widgets.forEach((child, index) => {
const deletable = child.model.sharedModel.getMetadata('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 => {
model.sharedModel.deleteCell(i);
});
}
index = toDelete[0];
break;
}
default:
break;
}
model.sharedModel.insertCells(index, values.map(cell => {
cell.id =
cell.cell_type === 'code' &&
notebook.lastClipboardInteraction === 'cut' &&
typeof cell.id === 'string'
? cell.id
: undefined;
return cell;
}));
});
notebook.activeCellIndex = prevActiveCellIndex + values.length;
notebook.deselectAll();
if (cellsFromClipboard) {
notebook.lastClipboardInteraction = 'paste';
}
void Private.handleState(notebook, state, true);
}
/**
* Undo a cell action.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* This is a no-op if there are no cell actions to undo.
*/
function undo(notebook) {
if (!notebook.model) {
return;
}
const state = Private.getState(notebook);
notebook.mode = 'command';
notebook.model.sharedModel.undo();
notebook.deselectAll();
void 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();
void 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: { ...config.code, lineNumbers },
markdown: { ...config.markdown, lineNumbers },
raw: { ...config.raw, lineNumbers }
};
notebook.editorConfig = newConfig;
void 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);
let index = -1;
for (const cell of notebook.model.cells) {
const child = notebook.widgets[++index];
if (notebook.isSelectedOrActive(child) && cell.type === 'code') {
cell.sharedModel.transact(() => {
cell.clearExecution();
child.outputHidden = false;
}, false);
Private.outputCleared.emit({ notebook, cell: child });
}
}
void 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);
let index = -1;
for (const cell of notebook.model.cells) {
const child = notebook.widgets[++index];
if (cell.type === 'code') {
cell.sharedModel.transact(() => {
cell.clearExecution();
child.outputHidden = false;
}, false);
Private.outputCleared.emit({ notebook, cell: child });
}
}
void 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;
}
});
void 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;