@jupyterlab/notebook
Version:
JupyterLab - Notebook
2,881 lines • 117 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Clipboard, Dialog, showDialog, SystemClipboard } from '@jupyterlab/apputils';
import { CodeCell, CodeCellModel, isMarkdownCellModel, isRawCellModel, MarkdownCell } from '@jupyterlab/cells';
import { Notification } from '@jupyterlab/apputils';
import { signalToPromise } from '@jupyterlab/coreutils';
import * as nbformat from '@jupyterlab/nbformat';
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) {
const READ_ONLY_ACTION_AUTO_CLOSE = 5000;
function notifySplitReadOnlyAction(translator) {
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
Notification.error(trans.__('The cell is read-only and cannot be split.'), {
autoClose: READ_ONLY_ACTION_AUTO_CLOSE
});
}
function notifyMergeReadOnlyAction(translator) {
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
Notification.error(trans.__('The cell is read-only and cannot be merged.'), {
autoClose: READ_ONLY_ACTION_AUTO_CLOSE
});
}
/**
* Split the active cell into two or more cells.
*
* @param notebook The target notebook widget.
*
* @param translator - Application translator.
*
* #### 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, translator) {
if (!notebook.model || !notebook.activeCell) {
return;
}
if (notebook.activeCell.model.getMetadata('editable') === false) {
notifySplitReadOnlyAction(translator);
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);
const { cell_type, metadata } = child.model.sharedModel.toJSON();
const baseMetadata = JSON.parse(JSON.stringify(metadata !== null && metadata !== void 0 ? metadata : {}));
// If execution metadata is present but missing execute_reply (i.e., it is in running state),
// remove execution metadata entirely for new cells
if (cell_type === 'code' &&
baseMetadata.execution &&
baseMetadata.execution['iopub.execute_input'] &&
!baseMetadata.execution['shell.execute_reply']) {
delete baseMetadata.execution;
}
// 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) => ({
cell_type,
metadata: JSON.parse(JSON.stringify(baseMetadata)),
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).
*
* @param addExtraLine - Whether to add an extra newline between merged cell contents
* (true, default) or use only a single newline (false).
*
* @param translator - Application translator.
*
* #### 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, addExtraLine = true, translator) {
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 = {};
let hasReadOnlyCell = false;
// Get the cells to merge.
notebook.widgets.forEach((child, index) => {
if (notebook.isSelectedOrActive(child)) {
if (child.model.getMetadata('editable') === false) {
hasReadOnlyCell = true;
return;
}
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();
}
}
}
});
if (hasReadOnlyCell) {
notifyMergeReadOnlyAction(translator);
return;
}
// 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;
}
if (notebook.widgets[active - 1].model.getMetadata('editable') === false) {
notifyMergeReadOnlyAction(translator);
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;
}
if (notebook.widgets[active + 1].model.getMetadata('editable') === false) {
notifyMergeReadOnlyAction(translator);
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(addExtraLine ? '\n\n' : '\n'),
attachments: primaryModel.cell_type === 'markdown' ||
primaryModel.cell_type === 'raw'
? attachments
: undefined
};
// Detach kernel futures from cells about to be deleted so OutputArea.dispose()
// does not terminate them - they stay live in kernel._futures for reconnection
// after undo. Handlers are cleared by detachFuture() so the future no
// longer holds references to the (soon-to-be-disposed) output area.
const storedExecutions = [];
[active, ...toDelete].forEach(index => {
const cell = notebook.widgets[index];
if (!(cell instanceof CodeCell)) {
return;
}
const stored = Private.captureExecution(cell);
if (stored) {
storedExecutions.push(stored);
}
});
// 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);
});
});
// Store execution context in the undo stack item so undo() can restore state.
if (storedExecutions.length > 0) {
const undoManager = model.sharedModel.undoManager;
const lastItem = undoManager.undoStack[undoManager.undoStack.length - 1];
lastItem === null || lastItem === void 0 ? void 0 : lastItem.meta.set(Private.CELL_EXECUTION_META_KEY, storedExecutions);
}
// 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;
}
const toIndex = shift > 0 ? lastIndex : firstIndex + shift;
moveCells(notebook, firstIndex, toIndex, lastIndex - firstIndex);
void Private.handleState(notebook, state, true);
}
/**
* Move cells while preserving in-flight kernel futures.
*
* The underlying `jupyter-ydoc` `moveCells` implementation currently
* serializes cells to JSON and recreates them via a delete + insert
* transaction, which disposes any active kernel futures attached to the
* old widgets. This wrapper detaches futures before the move and
* reattaches them to the new widgets afterwards, and stores them in the
* undo stack so that undoing the move also restores execution state.
*
* @param notebook - The target notebook.
* @param from - Index of the first cell to move.
* @param to - Target index (as passed to `notebook.moveCell`).
* @param n - Number of cells to move.
*/
function moveCells(notebook, from, to, n = 1) {
if (!notebook.model) {
return;
}
// Mirror `Notebook.moveCell`'s bounding/no-op logic so that we do not
// capture futures or touch the undo stack when the move won't happen.
// Otherwise we could attach execution metadata to an unrelated previous
// undo item, corrupting subsequent undo behavior.
const boundedTo = Math.min(notebook.model.cells.length - 1, Math.max(0, to));
if (boundedTo === from) {
return;
}
// moveCells serializes cells to JSON and recreates widgets (delete+insert),
// which would dispose any in-flight futures. Capture them first.
const storedExecutions = [];
notebook.widgets.slice(from, from + n).forEach(child => {
if (!(child instanceof CodeCell)) {
return;
}
const stored = Private.captureExecution(child);
if (stored) {
storedExecutions.push(stored);
}
});
notebook.moveCell(from, to, n);
// Immediately reconnect futures to the newly created widgets.
for (const stored of storedExecutions) {
Private.restoreExecution(notebook, stored);
}
// Store in the undo stack so that undoing the move can also restore state.
if (storedExecutions.length > 0) {
const undoManager = notebook.model.sharedModel.undoManager;
const lastItem = undoManager.undoStack[undoManager.undoStack.length - 1];
lastItem === null || lastItem === void 0 ? void 0 : lastItem.meta.set(Private.CELL_EXECUTION_META_KEY, storedExecutions);
}
}
NotebookActions.moveCells = moveCells;
/**
* 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;
/**
* Copy the selected cell(s) data to the system clipboard.
*
* @param notebook - The target notebook widget.
*/
async function copyToSystemClipboard(notebook) {
await Private.copyOrCutToSystemClipboard(notebook, false);
}
NotebookActions.copyToSystemClipboard = copyToSystemClipboard;
/**
* 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;
/**
* Cut the selected cell data to the system clipboard.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* This action can be undone.
* A new code cell is added if all cells are cut.
*/
async function cutToSystemClipboard(notebook) {
await Private.copyOrCutToSystemClipboard(notebook, true);
}
NotebookActions.cutToSystemClipboard = cutToSystemClipboard;
/**
* 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.
*
* @param options - Optional. Set `stripOutputs: true` to paste code cells without their outputs.
*
* #### 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', options) {
const clipboard = Clipboard.getInstance();
if (!clipboard.hasData(JUPYTER_CELL_MIME)) {
return;
}
let values = clipboard.getData(JUPYTER_CELL_MIME);
if (options === null || options === void 0 ? void 0 : options.stripOutputs) {
values = Private.stripCodeCellOutputs(values);
}
addCells(notebook, mode, values, true);
void focusActiveCell(notebook);
}
NotebookActions.paste = paste;
/**
* Paste cells from the system 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.
*
* @param options - Optional. Set `stripOutputs: true` to paste code cells without their outputs.
*
* #### 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.
*/
async function pasteFromSystemClipboard(notebook, mode = 'below', options) {
const clipboard = SystemClipboard.getInstance();
const stored = await clipboard.getData(JUPYTER_CELL_MIME);
if (stored === null || stored === undefined) {
return;
}
let values = stored;
if (options === null || options === void 0 ? void 0 : options.stripOutputs) {
values = Private.stripCodeCellOutputs(values);
}
addCells(notebook, mode, values, true);
void focusActiveCell(notebook);
}
NotebookActions.pasteFromSystemClipboard = pasteFromSystemClipboard;
/**
* 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.recordCellClipboardInteraction('paste', values);
}
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';
const undoManager = notebook.model.sharedModel.undoManager;
// For cells that will be MOVED by the undo (i.e. they still exist in the
// notebook at their current position), pre-capture their futures now before
// sharedModel.undo() destroys those widgets. This prevents OutputArea.dispose()
// from cancelling the future during the Y.js delete+insert that implements the move.
const topItem = undoManager.undoStack[undoManager.undoStack.length - 1];
const pendingExecutions = topItem === null || topItem === void 0 ? void 0 : topItem.meta.get(Private.CELL_EXECUTION_META_KEY);
const preCaptured = new Map();
pendingExecutions === null || pendingExecutions === void 0 ? void 0 : pendingExecutions.forEach(stored => {
var _a;
const cell = notebook.widgets.find(w => w.model.id === stored.cellId);
if (!(cell instanceof CodeCell)) {
return; // cell was deleted (not moved) — handled via stored future below
}
// Cell still present → move undo. Fresh capture protects the future.
const fresh = (_a = Private.captureExecution(cell)) !== null && _a !== void 0 ? _a : {
...stored,
isDone: () => true,
buffered: []
};
// The undo will roll the outputs back to their state at the time of
// the move; snapshot the current outputs so anything received since
// then can be re-applied after the undo.
fresh.outputs = cell.model.outputs.toJSON();
preCaptured.set(stored.cellId, fresh);
});
// Capture execution context from the stack item being popped.
let storedExecutions;
const onStackItemPopped = ({ stackItem }) => {
storedExecutions = stackItem.meta.get(Private.CELL_EXECUTION_META_KEY);
};
undoManager.on('stack-item-popped', onStackItemPopped);
notebook.model.sharedModel.undo();
undoManager.off('stack-item-popped', onStackItemPopped);
// Restore execution state on resurrected/moved cell widgets.
// For move-undo: use freshly pre-captured data (stored data is stale).
// For delete-undo: stored data has the futures captured at deletion time.
storedExecutions === null || storedExecutions === void 0 ? void 0 : storedExecutions.forEach(stored => {
var _a;
Private.restoreExecution(notebook, (_a = preCaptured.get(stored.cellId)) !== null && _a !== void 0 ? _a : stored);
});
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;
}
const state = Private.getState(notebook);
notebook.widgets.forEach(cell => {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
cell.inputHidden = false;
}
});
void 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;
}
});
void Private.handleState(notebook, state);
}
NotebookActions.hideAllCode = hideAllCode;
/**
* Show the code on all code cells.
*
* @param notebook 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;
}
});
void 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;
}
});
void 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;
}
});
void Private.handleState(notebook, state);
}
NotebookActions.showOutput = showOutput;
/**
* Toggle output visibility on selected code cells.
* If at least one output is visible, all outputs are hidden.
* If no outputs are visible, all outputs are made visible.
*
* @param notebook - The target notebook widget.
*/
function toggleOutput(notebook) {
if (!notebook.model || !notebook.activeCell) {
return;
}
for (const cell of notebook.widgets) {
if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
if (cell.outputHidden === false) {
// We found at least one visible output; hide outputs for this cell
return hideOutput(notebook);
}
}
}
// We found no selected cells or no selected cells with visible output;
// show outputs for selected cells
return showOutput(notebook);
}
NotebookActions.toggleOutput = toggleOutput;
/**
* 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;
}
});
void 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;
}
});
void 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;
}
});
void 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;
}
});
void 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.getMetadata('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;
/**
* Select the last modified cell and pop it from the back stack
*
* @param notebook - The target notebook widget.
*/
async function selectLastModifiedCell(notebook) {
const cell = notebook.popLastModifiedCell();
if (cell && cell !== notebook.activeCell && !cell.isDisposed) {
notebook.activeCellIndex = notebook.widgets.indexOf(cell);
await notebook.scrollToCell(cell);
}
}
NotebookActions.selectLastModifiedCell = selectLastModifiedCell;
/**
* Select the next modified cell and pop it from the forward stack
*
* @param notebook - The target notebook widget.
*/
async function selectNextModifiedCell(notebook) {
const cell = notebook.popNextModifiedCell();
if (cell && cell !== notebook.activeCell && !cell.isDisposed) {
notebook.activeCellIndex = notebook.widgets.indexOf(cell);
await notebook.scrollToCell(cell);
}
}
NotebookActions.selectNextModifiedCell = selectNextModifiedCell;
/**
* Set the markdown header level.
*
* @param notebook - The target notebook widget.
* @param level - The header level.
* @param translator - The application translator.
*
* #### 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, translator) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = Private.getState(notebook);
level = Math.min(Math.max(level, 1), 6);
Private.changeCellType(notebook, 'markdown', {
translator,
headingLevel: level
});
void Private.handleState(notebook, state);
}
NotebookActions.setMarkdownHeader = setMarkdownHeader;
/**
* Collapse all cells in given notebook.
*
* @param notebook - The target notebook widget.
*/
function collapseAllHeadings(notebook) {
const state = Private.getState(notebook);
for (const cell of notebook.widgets) {
if (NotebookActions.getHeadingInfo(cell).isHeading) {
NotebookActions.setHeadingCollapse(cell, true, notebook);
NotebookActions.setCellCollapse(cell, true);
}
}
notebook.activeCellIndex = 0;
void Private.handleState(notebook, state, true);
}
NotebookActions.collapseAllHeadings = collapseAllHeadings;
/**
* 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 (index === -1) {
return;
}
// Finds the nearest header above the given cell. If the cell is a header itself, it does not return itself;
// this can be checked directly by calling functions.
if (index >= notebook.widgets.length) {
return;
}
let childHeaderInfo = getHeadingInfo(notebook.widgets[index]);
for (let cellN = index - 1; cellN >= 0; cellN--) {
if (cellN < notebook.widgets.length) {
let hInfo = getHeadingInfo(notebook.widgets[cellN]);
if (hInfo.isHeading &&
hInfo.headingLevel < childHeaderInfo.headingLevel) {
return notebook.widgets[cellN];
}
}
}
// else no parent header found.
return;
}
/**
* Finds the "parent" heading of the given cell and expands.
* Used for the case that a cell becomes active that is within a collapsed heading.
* @param cell - "Child" cell that has become the active cell
* @param notebook - The target notebook widget.
*/
function expandParent(cell, notebook) {
let nearestParentCell = findNearestParentHeader(cell, notebook);
if (!nearestParentCell) {
return;
}
if (!getHeadingInfo(nearestParentCell).collapsed &&
!nearestParentCell.isHidden) {
return;
}
if (nearestParentCell.isHidden) {
expandParent(nearestParentCell, notebook);
}
if (getHeadingInfo(nearestParentCell).collapsed) {
setHeadingCollapse(nearestParentCell, false, notebook);
}
}
NotebookActions.expandParent = expandParent;
/**
* Finds the next heading that isn't a child of the given markdown heading.
* @param cell - "Child" cell that has become the active cell
* @param notebook - The target notebook widget.
*/
function findNextParentHeading(cell, notebook) {
let index = findIndex(notebook.widgets, (possibleCell, index) => {
return cell.model.id === possibleCell.model.id;
});
if (index === -1) {
return -1;
}
let childHeaderInfo = getHeadingInfo(cell);
for (index = index + 1; index < notebook.widgets.length; index++) {
let hInfo = getHeadingInfo(notebook.widgets[index]);
if (hInfo.isHeading &&
hInfo.headingLevel <= childHeaderInfo.headingLevel) {
return index;
}
}
// else no parent header found. return the index of the last cell
return notebook.widgets.length;
}
NotebookActions.findNextParentHeading = findNextParentHeading;
/**
* Set the given cell and ** all "child" cells **
* to the given collapse / expand if cell is
* a markdown header.
*
* @param cell - The cell
* @param collapsing - Whether to collapse or expand the cell
* @param notebook - The target notebook widget.
*/
function setHeadingCollapse(cell, collapsing, notebook) {
const which = findIndex(notebook.widgets, (possibleCell, index) => {
return cell.model.id === possibleCell.model.id;
});
if (which === -1) {
return -1;
}
if (!notebook.widgets.length) {
return which + 1;
}
let selectedHeadingInfo = NotebookActions.getHeadingInfo(cell);
if (cell.isHidden ||
!(cell instanceof MarkdownCell) ||
!selectedHeadingInfo.isHeading) {
// otherwise collapsing and uncollapsing already hidden stuff can
// cause some funny looking bugs.
return which + 1;
}
let localCollapsed = false;
let localCollapsedLevel = 0;
// iterate through all cells after the active cell.
let cellNum;
for (cellNum = which + 1; cellNum < notebook.widgets.length; cellNum++) {
let subCell = notebook.widgets[cellNum];
let subCellHeadingInfo = NotebookActions.getHeadingInfo(subCell);
const isHeadingResolved = subCell
.headingsResolved;
// Defer until subCell's asynchronous heading parsing is complete
if (subCellHeadingInfo.headingLevel === -1 && !isHeadingResolved) {
requestAnimationFrame(() => {
setHeadingCollapse(cell, collapsing, notebook);
});
break;
}
if (subCellHeadingInfo.isHeading &&
subCellHeadingInfo.headingLevel <= selectedHeadingInfo.headingLevel) {
// then reached an equivalent or higher heading level than the
// original the end of the collapse.
cellNum -= 1;
break;
}
if (localCollapsed &&
subCellHeadingInfo.isHeading &&
subCellHeadingInfo.headingLevel <= localCollapsedLevel) {
// then reached the end of the local collapsed, so unset NotebookActions.
localCollapsed = false;
}
if (collapsing || localCollapsed) {
// then no extra handling is needed for further locally collapsed
// headings.
subCell.setHidden(true);
continue;
}
if (subCellHeadingInfo.collapsed && subCellHeadingInfo.isHeading) {
localCollapsed = true;
localCollapsedLevel = subCellHeadingInfo.headingLevel;
// but don't collapse the locally collapsed heading, so continue to
// expand the heading. This will get noticed in the next round.
}
subCell.setHidden(false);
}
if (cellNum === notebook.widgets.length) {
cell.numberChildNodes = cellNum - which - 1;
}
else {
cell.numberChildNodes = cellNum - which;
}
NotebookActions.setCellCollapse(cell, collapsing);
return cellNum + 1;
}
NotebookActions.setHeadingCollapse = setHeadingCollapse;
/**
* Toggles the collapse state of the active cell of the given notebook
* and ** all of its "child" cells ** if the cell is a heading.
*
* @param notebook - The target notebook widget.
*/
function toggleCurrentHeadingCollapse(notebook) {
if (!notebook.activeCell || notebook.activeCellIndex === undefined) {
return;
}
let headingInfo = NotebookActions.getHeadingInfo(notebook.activeCell);
if (headingInfo.isHeading) {
// Then toggle!
NotebookActions.setHeadingCollapse(notebook.activeCell, !headingInfo.collapsed, notebook);
}
notebook.scrollToItem(notebook.activeCellIndex).catch(reason => {
// no-op
});
}
NotebookActions.toggleCurrentHeadingCollapse = toggleCurrentHeadingCollapse;
/**
* If cell is a markdown heading, sets the headingCollapsed field,
* and otherwise hides the cell.
*
* @param cell - The cell to collapse / expand
* @param collapsing - Whether to collapse or expand the given cell
*/
function setCellCollapse(cell, collapsing) {
if (cell instanceof MarkdownCell) {
cell.headingCollapsed = collapsing;
}
else {
cell.setHidden(collapsing);
}
}
NotebookActions.setCellCollapse = setCellCollapse;
/**
* If given cell is a markdown heading, returns the heading level.
* If given cell is not markdown, returns 7 (there are only 6 levels of markdown headings)
*
* @param cell - The target cell widget.
*/
function getHeadingInfo(cell) {
if (!(cell instanceof MarkdownCell)) {
return { isHeading: false, headingLevel: 7 };
}
let level = cell.headingInfo.level;
let collapsed = cell.headingCollapsed;
return { isHeading: level > 0, headingLevel: level, collapsed: collapsed };
}
NotebookActions.getHeadingInfo = getHeadingInfo;
/**
* Trust the notebook after prompting the user.
*
* @param notebook - The target notebook widget.
* @param translator - The application translator.
*
* @returns a promise that resolves when the transaction is finished.
*
* #### Notes
* No dialog will be presented if the notebook is already trusted.
*/
function trust(notebook, translator) {
translator = translator || nullTranslator;
const trans = translator.load('jupyterlab');
if (!notebook.model) {
return Promise.resolve({ trusted: false });
}
// Do nothing if already trusted.
const trusted = every(notebook.model.cells, cell => cell.trusted);
// FIXME
const trustMessage = (React.createElement("p", null,
trans.__('A trusted Jupyter notebook may execute hidden malicious code when you open it.'),
React.createElement("br", null),
trans.__('Selecting "Trust" will re-render this notebook in a trusted state.'),
React.createElement("br", null),
trans.__('For more information, see'),
' ',
React.createElement("a", { href: "https://jupyter-server.readthedocs.io/en/stable/operators/security.html", target: "_blank", rel: "noopener noreferrer" }, trans.__('the Jupyter security documentation')),
"."));
if (trusted) {
return showDialog({
body: trans.__('Notebook is already trusted'),
buttons: [Dialog.okButton()]
}).then(() => ({ trusted: true }));
}
return showDialog({
body: trustMessage,
title: trans.__('Trust this notebook?'),
buttons: [
Dialog.cancelButton(),
Dialog.warnButton({
label: trans.__('Trust'),
ariaLabel: trans.__('Confirm Trusting this notebook')
})
] // FIXME?
}).then(result => {
if (result.button.accept) {
if (notebook.model) {
for (const cell of notebook.model.cells) {
cell.trusted = true;
}
}
return { trusted: true };
}
return { trusted: false };
});
}
NotebookActions.trust = trust;
/**
* If the notebook has an active cell, focus it.
*
* @param notebook The target notebook widget
* @param options Optional options to change the behavior of this function
* @param options.waitUntilReady If true, do not call focus until activeCell.ready is resolved
* @param options.preventScroll If true, do not scroll the active cell into view
*
* @returns a promise that resolves when focus has been called on the active
* cell's node.
*
* #### Notes
* By default, waits until after the active cell has been attached unless
* called with { waitUntilReady: false }
*/
async function focusActiveCell(notebook, options = { waitUntilReady: true, preventScroll: false }) {
const { activeCell } = notebook;
const { waitUntilReady, preventScroll } = options;
if (!activeCell) {
return;
}
if (waitUntilReady) {
await activeCell.ready;
}
if (notebook.isDisposed || activeCell.isDisposed) {
return;
}
activeCell.node.focus({
preventScroll
});
}
NotebookActions.focusActiveCell = focusActiveCell;
/*
* Access last notebook history.
*
* @param notebook - The target notebook widget.
*/
async function accessPreviousHistory(notebook) {
if (!notebook.notebookConfig.accessKernelHistory) {
return;
}
const activeCell = notebook.activeCell;
if (activeCell) {
if (notebook.kernelHistory) {
const previousHistory = await notebook.kernelHistory.back(activeCell);
notebook.kernelHistory.updateEditor(activeCell, previousHistory);
}
}
}
NotebookActions.accessPreviousHistory = accessPreviousHistory;
/**
* Access next notebook history.
*
* @param notebook - The target notebook widget.
*/
async function accessNextHistory(notebook) {
if (!notebook.notebookConfig.accessKernelHistory) {
return;
}
const activeCell = notebook.activeCell;
if (activeCell) {
if (notebook.kernelHistory) {
const nextHistory = await notebook.kernelHistory.forward(activeCell);
notebook.kernelHistory.updateEditor(activeCell, nextHistory);
}
}
}
NotebookActions.accessNextHistory = accessNextHistory;
})(NotebookActions || (NotebookActions = {}));
/**
* Set the notebook cell executor and the related signals.
*/
export function setCellExecutor(executor) {
if (Private.executor) {
throw new Error('Cell executor can only be set once.');
}
Private.executor = executor;
}
/**
* A namespace for private data.
*/
var Private;
(function (Private) {
/** Key used to store cell execution state in Y.js undo stack item metadata. */
Private.CELL_EXECUTION_META_KEY = Symbol('cellExecutionState');
/**
* Detach the kernel future from a code cell, buffering any messages that
* arrive while it is detached so they can be replayed on reattach.
*
* `detachFuture` clears the IOPub, stdin and reply handlers, so all three
* channels are buffered here; otherwise a message arriving during the
* detached window (e.g. an `input()` request on stdin) would be dropped.
*
* Returns null if the cell has no active future.
*/
function captureExecution(cell) {
const future = cell.outputArea.detachFuture();
if (!future) {
return null;
}
let done = false;
void future.done.finally(() => {
done = true;
});
const buffered = [];
future.onIOPub = msg => {
buffered.push({ channel: 'iopub', msg });
};
future.onStdin = msg => {
buffered.push({ channel: 'stdin', msg });
};
future.onReply = msg => {
buffered.push({ channel: 'reply', msg });
};
return { cellId: cell.model.id, future, isDone: () => done, buffered };
}
Private.captureExecution = captureExecution;
/**
* Reconnect a captured execution to the cell widget that now holds the model.
*
* Handles both the "still running" and "already finished" cases.
*/
function restoreExecution(notebook, { cellId, future, isDone, buffered, outputs }) {
const cell = notebook.widgets.find(w => w.model.id === cellId);
if (!(cell instanceof CodeCell)) {
return;
}
if (outputs && !JSONExt.deepEqual(outputs, cell.model.outputs.toJSON())) {
// Re-apply the output snapshot taken just before the undo: the Y.js
// undo rolled the outputs back to their state at the time of the
// undone action. Going through the output area model keeps the
// in-memory model and the shared model in sync (output changes are
// not tracked by the undo manager, so this does not pollute history).
cell.model.outputs.fromJSON(outputs);
}
// Reattach the future (without clearing existing outputs) and replay any
// messages buffered while it was detached, in arrival order. This is done
// whether or not the execution has already finished, so that final outputs
// (or a pending stdin request) that arrived while detached are not lost
// (e.g. if the kernel completed in the brief window during the undo).
cell.outputArea.reattachFuture(future);
for (const buf of buffered) {
switch (buf.channel) {
case 'iopub':
void future.onIOPub(buf.msg);
break;
case 'stdin':
void future.onStdin(buf.msg);
break;
case 'reply':
void future.onReply(buf.msg);
break;
}
}
if (isDone()) {
// The execution already finished (e.g. it completed or was interrupted
// during the undo). The resurrected cell may carry a stale 'running'
// state from its restored snapshot, so reset it synchronously rather
// than relying solely on the asynchronous `future.done` handler below.
cell.model.executionState = 'idle';
}
else {
// Restore the running state on the recreated cell widget.
cell.model.executionState = 'running';
}
const cellRef = cell;
void future.done.then(reply => {
if (!cellRef.isDisposed) {
// The future is authoritative for the prompt number; a snapshot
// taken before completion would be stale (still null). Setting a
// non-null execution count also flips the state back to 'idle'.
cellRef.model.executionCount = reply.content.execution_count;
cellRef.model.executionState = 'idle';
}
}, () => {
if (!cellRef.isDisposed) {
cellRef.model.executionState = 'idle';
}
});
}
Private.restoreExecution = restoreExecution;
/**
* A signal that emits whenever a cell completes execution.
*/
Private.executed = new Signal({});
/**
* A signal that emits whenever a cell execution is scheduled.
*/
Private.executionScheduled = new Signal({});
/**
* A signal that emits when one notebook's cells are all executed.
*/
Private.selectionExecuted = new Signal({});
/**
* A signal that emits when one notebook's cells are all executed.
*/
Private.outputCleared = new Signal({});
/**
* Get the state of a widget before running an action.
*/
function getState(notebook) {
var _a, _b;
return {
wasFocused: notebook.node.contains(document.activeElement),
activeCellId: (_b = (_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.model.id) !== null && _b !== void 0 ? _b : null
};
}
Private.getState = getState;
/**
* Handle the state of a widget after running an action.
*/
async function handleState(notebook, state, scrollIfNeeded = false) {
const { activeCell, activeCellIndex } = notebook;
if (scrollIfNeeded && activeCell) {
await notebook.scrollToItem(activeCellIndex, 'auto', 0).catch(reason => {
// no-op
});
}
if (state.wasFocused || notebook.mode === 'edit') {
notebook.activate();
}
}
Private.handleState = handleState;
/**
* Handle the state of a widget after running a run action.
*/
async function handleRunState(notebook, state, alignPreference) {
const { activeCell, activeCellIndex } = notebook;
if (activeCell) {
await notebook
.scrollToItem(activeCellIndex, 'smart', 0, alignPreference)
.catch(reason => {
// no-op
});
}
if (state.wasFocused || notebook.mode === 'edit') {
notebook.activate();
}
}
Private.handleRunState = handleRunState;
/**
* Run the selected cells.
*
* @param notebook Notebook
* @param cells Cells to run
* @param sessionContext Notebook session context
* @param sessionDialogs Session dialogs
* @param translator Application translator
*/
function runCells(notebook, cells, sessionContext, sessionDialogs, translator) {
const lastCell = cells[cells.length - 1];
notebook.mode = 'command';
let initializingDialogShown = false;
return Promise.all(cells.map(cell => {
if (cell.model.type === 'code' &&
notebook.notebookConfig.enableKernelInitNotification &&
sessionContext &&
sessionContext.kernelDisplayStatus === 'initializing' &&
!initializingDialogShown) {
initializingDialogShown = true;
translator = translator || nullTranslator;
const trans = translator.load('jupyterlab');
Notification.emit(trans.__(`Kernel '${sessionContext.kernelDisplayName}' for '${sessionContext.path}' is still initializing. You can run code cells when the kernel has initialized.`), 'warning', { autoClose: false });
return Promise.resolve(false);
}
if (cell.model.type === 'code' &&
notebook.notebookConfig.enableKernelInitNotification &&
initializingDialogShown) {
return Promise.resolve(false);
}
return runCell(notebook, cell, sessionContext, sessionDialogs, translator);
}))
.then(results => {
if (notebook.isDisposed) {
return false;
}
Private.selectionExecuted.emit({
notebook,
lastCell
});
// Post an update request.
notebook.update();
return results.every(result => result);
})
.catch(reason => {
if (reason.message.startsWith('KernelReplyNotOK')) {
cells.map(cell => {
// Remove '*' prompt from cells that didn't execute
if (cell.model.type === 'code' &&
cell.model.executionCount == null) {
cell.model.executionState = 'idle';
}
});
}
else {
throw reason;
}
Private.selectionExecuted.emit({
notebook,
lastCell
});
notebook.update();
return false;
});
}
Private.runCells = runCells;
/**
* Run the selected cells.
*
* @param notebook Notebook
* @param sessionContext Notebook session context
* @param sessionDialogs Session dialogs
* @param translator Application translator
*/
function runSelected(notebook, sessionContext, sessionDialogs, translator) {
notebook.mode = 'command';
let lastIndex = notebook.activeCellIndex;
const selected = notebook.widgets.filter((child, index) => {
const active = notebook.isSelectedOrActive(child);
if (active) {
lastIndex = index;
}
return active;
});
notebook.activeCellIndex = lastIndex;
notebook.deselectAll();
return runCells(notebook, selected, sessionContext, sessionDialogs, translator);
}
Private.runSelected = runSelected;
/**
* Run a cell.
*/
async function runCell(notebook, cell, sessionContext, sessionDialogs, translator) {
if (!Private.executor) {
console.warn('Requesting cell execution without any cell executor defined. Falling back to default execution.');
}
const options = {
cell,
notebook: notebook.model,
notebookConfig: notebook.notebookConfig,
onCellExecuted: args => {
Private.executed.emit({ notebook, ...args });
},
onCellExecutionScheduled: args => {
Private.executionScheduled.emit({ notebook, ...args });
},
sessionContext,
sessionDialogs,
translator
};
return Private.executor ? Private.executor.runCell(options) : defaultRunCell(options);
}
/**
* Return a deep copy of cells with code cell outputs and execution_count cleared.
*
* @param cells - The cells to process.
* @returns New cell objects.
*/
function stripCodeCellOutputs(cells) {
return cells.map(cell => {
const copy = JSONExt.deepCopy(cell);
if (copy && nbformat.isCode(copy)) {
copy.outputs = [];
copy.execution_count = null;
}
return copy;
});
}
Private.stripCodeCellOutputs = stripCodeCellOutputs;
/**
* Get the selected cell(s) without affecting the clipboard.
*
* @param notebook - The target notebook widget.
*
* @returns A list of 0 or more selected cells
*/
function selectedCells(notebook) {
return notebook.widgets
.filter(cell => notebook.isSelectedOrActive(cell))
.map(cell => cell.model.toJSON())
.map(cellJSON => {
if (cellJSON.metadata.deletable !== undefined) {
delete cellJSON.metadata.deletable;
}
return cellJSON;
});
}
Private.selectedCells = selectedCells;
/**
* Copy or cut the selected cell data to the application clipboard.
*
* @param notebook - The target notebook widget.
*
* @param cut - True if the cells should be cut, false if they should be copied.
*/
function copyOrCut(notebook, cut) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = getState(notebook);
const clipboard = Clipboard.getInstance();
notebook.mode = 'command';
clipboard.clear();
const data = Private.selectedCells(notebook);
clipboard.setData(JUPYTER_CELL_MIME, data);
if (cut) {
deleteCells(notebook);
}
else {
notebook.deselectAll();
}
if (cut) {
notebook.recordCellClipboardInteraction('cut', data);
}
else {
notebook.recordCellClipboardInteraction('copy', data);
}
void handleState(notebook, state);
}
Private.copyOrCut = copyOrCut;
/**
* Copy or cut the selected cell data to the system clipboard.
*
* @param notebook - The target notebook widget.
*
* @param cut - True if the cells should be cut, false if they should be copied.
*/
async function copyOrCutToSystemClipboard(notebook, cut) {
if (!notebook.model || !notebook.activeCell) {
return;
}
const state = getState(notebook);
const clipboard = SystemClipboard.getInstance();
notebook.mode = 'command';
clipboard.clear();
const data = Private.selectedCells(notebook);
await clipboard.setData(JUPYTER_CELL_MIME, data);
if (cut) {
deleteCells(notebook);
}
else {
notebook.deselectAll();
}
if (cut) {
notebook.recordCellClipboardInteraction('cut', data);
}
else {
notebook.recordCellClipboardInteraction('copy', data);
}
void handleState(notebook, state);
}
Private.copyOrCutToSystemClipboard = copyOrCutToSystemClipboard;
/**
* 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, options) {
const { translator, headingLevel } = options !== null && options !== void 0 ? options : {};
const notebookSharedModel = notebook.model.sharedModel;
notebook.widgets.forEach((child, index) => {
if (!notebook.isSelectedOrActive(child)) {
return;
}
if (child.model.type === 'code' &&
child.outputArea.pendingInput) {
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
// Do not permit changing cell type when input is pending
void showDialog({
title: trans.__('Cell type not changed due to pending input'),
body: trans.__('The cell type has not been changed to avoid kernel deadlock as this cell has pending input! Submit your pending input and try again.'),
buttons: [Dialog.okButton()]
});
return;
}
if (child.model.getMetadata('editable') == false) {
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
// Do not permit changing cell type when the cell is readonly
void showDialog({
title: trans.__('Cell is read-only'),
body: trans.__('The cell is read-only, its type cannot be changed!'),
buttons: [Dialog.okButton()]
});
return;
}
if (child.model.type !== value) {
const raw = child.model.toJSON();
let newSource = raw.source;
if (headingLevel !== undefined) {
newSource = Private.setMarkdownHeader(newSource, headingLevel);
}
// Detach future before the transaction so dispose() does not cancel it.
const storedExecution = child instanceof CodeCell
? Private.captureExecution(child)
: undefined;
notebookSharedModel.transact(() => {
notebookSharedModel.deleteCell(index);
if (value === 'code') {
// After change of type outputs are deleted so cell can be trusted.
raw.metadata.trusted = true;
}
else {
// Otherwise clear the metadata as trusted is only "valid" on code
// cells (since other cell types cannot have outputs).
raw.metadata.trusted = undefined;
}
const newCell = notebookSharedModel.insertCell(index, {
id: raw.id,
cell_type: value,
source: newSource,
metadata: raw.metadata
});
if (raw.attachments && ['markdown', 'raw'].includes(value)) {
newCell.attachments =
raw.attachments;
}
});
if (storedExecution) {
const undoManager = notebookSharedModel.undoManager;
const lastItem = undoManager.undoStack[undoManager.undoStack.length - 1];
lastItem === null || lastItem === void 0 ? void 0 : lastItem.meta.set(Private.CELL_EXECUTION_META_KEY, [
storedExecution
]);
}
}
else if (value === 'markdown' && headingLevel !== undefined) {
notebookSharedModel.transact(() => {
child.model.sharedModel.setSource(Private.setMarkdownHeader(child.model.sharedModel.getSource(), headingLevel));
});
}
if (value === 'markdown') {
// Fetch the new widget and unrender it.
child = notebook.widgets[index];
child.rendered = false;
}
});
notebook.deselectAll();
}
Private.changeCellType = changeCellType;
/**
* Delete the selected cells.
*
* @param notebook - The target notebook widget.
*
* #### Notes
* The cell after the last selected cell will be activated.
* If the last cell is deleted, then the previous one will be activated.
* It will add a code cell if all cells are deleted.
* This action can be undone.
*/
function deleteCells(notebook) {
const model = notebook.model;
const sharedModel = model.sharedModel;
const toDelete = [];
notebook.mode = 'command';
// Find the cells to delete.
notebook.widgets.forEach((child, index) => {
var _a;
const deletable = child.model.getMetadata('deletable') !== false;
if (notebook.isSelectedOrActive(child) && deletable) {
toDelete.push(index);
(_a = notebook.model) === null || _a === void 0 ? void 0 : _a.deletedCells.push(child.model.id);
}
});
// If cells are not deletable, we may not have anything to delete.
if (toDelete.length > 0) {
// Detach futures before the transaction so dispose() does not cancel them.
const storedExecutions = [];
toDelete.forEach(index => {
const cell = notebook.widgets[index];
if (!(cell instanceof CodeCell)) {
return;
}
const stored = Private.captureExecution(cell);
if (stored) {
storedExecutions.push(stored);
}
});
// Delete the cells as one undo event.
sharedModel.transact(() => {
// Delete cells in reverse order to maintain the correct indices.
toDelete.reverse().forEach(index => {
sharedModel.deleteCell(index);
});
// Add a new cell if the notebook is empty. This is done
// within the compound operation to make the deletion of
// a notebook's last cell undoable.
if (sharedModel.cells.length == toDelete.length) {
sharedModel.insertCell(0, {
cell_type: notebook.notebookConfig.defaultCell,
metadata: notebook.notebookConfig.defaultCell === 'code'
? {
// This is an empty cell created in empty notebook, thus is trusted
trusted: true
}
: {}
});
}
});
if (storedExecutions.length > 0) {
const undoManager = sharedModel.undoManager;
const lastItem = undoManager.undoStack[undoManager.undoStack.length - 1];
lastItem === null || lastItem === void 0 ? void 0 : lastItem.meta.set(Private.CELL_EXECUTION_META_KEY, storedExecutions);
}
// Select the *first* interior cell not deleted or the cell
// *after* the last selected cell.
// Note: The activeCellIndex is clamped to the available cells,
// so if the last cell is deleted the previous cell will be activated.
// The *first* index is the index of the last cell in the initial
// toDelete list due to the `reverse` operation above.
notebook.activeCellIndex = toDelete[0] - toDelete.length + 1;
}
// Deselect any remaining, undeletable cells. Do this even if we don't
// delete anything so that users are aware *something* happened.
notebook.deselectAll();
}
Private.deleteCells = deleteCells;
/**
* Set the markdown header level of a cell source.
*/
function setMarkdownHeader(source, level) {
// Remove existing header or leading white space.
const regex = /^#+\s*|^\s*/;
const newHeader = Array(level + 1).join('#') + ' ';
const matches = regex.exec(source);
if (matches) {
source = source.slice(matches[0].length);
}
return newHeader + source;
}
Private.setMarkdownHeader = setMarkdownHeader;
/** Functionality related to collapsible headings */
let Headings;
(function (Headings) {
/** Find the heading that is parent to cell.
*
* @param childCell - The cell that is child to the sought heading
* @param notebook - The target notebook widget
* @param includeChildCell [default=false] - if set to true and childCell is a heading itself, the childCell will be returned
* @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
*
* @returns the (index | Cell object) of the parent heading or (-1 | null) if there is no parent heading.
*/
function findParentHeading(childCell, notebook, includeChildCell = false, returnIndex = false) {
let cellIdx = notebook.widgets.indexOf(childCell) - (includeChildCell ? 1 : 0);
while (cellIdx >= 0) {
let headingInfo = NotebookActions.getHeadingInfo(notebook.widgets[cellIdx]);
if (headingInfo.isHeading) {
return returnIndex ? cellIdx : notebook.widgets[cellIdx];
}
cellIdx--;
}
return returnIndex ? -1 : null;
}
Headings.findParentHeading = findParentHeading;
/** Find heading above with leq level than baseCell heading level.
*
* @param baseCell - cell relative to which so search
* @param notebook - target notebook widget
* @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
*
* @returns the (index | Cell object) of the found heading or (-1 | null) if no heading found.
*/
function findLowerEqualLevelParentHeadingAbove(baseCell, notebook, returnIndex = false) {
let baseHeadingLevel = Private.Headings.determineHeadingLevel(baseCell, notebook);
if (baseHeadingLevel == -1) {
baseHeadingLevel = 1; // if no heading level can be determined, assume we're on level 1
}
// find the heading above with heading level <= baseHeadingLevel and return its index
let cellIdx = notebook.widgets.indexOf(baseCell) - 1;
while (cellIdx >= 0) {
let cell = notebook.widgets[cellIdx];
let headingInfo = NotebookActions.getHeadingInfo(cell);
if (headingInfo.isHeading &&
headingInfo.headingLevel <= baseHeadingLevel) {
return returnIndex ? cellIdx : cell;
}
cellIdx--;
}
return returnIndex ? -1 : null; // no heading found
}
Headings.findLowerEqualLevelParentHeadingAbove = findLowerEqualLevelParentHeadingAbove;
/** Find next heading with equal or lower level.
*
* @param baseCell - cell relative to which so search
* @param notebook - target notebook widget
* @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
*
* @returns the (index | Cell object) of the found heading or (-1 | null) if no heading found.
*/
function findLowerEqualLevelHeadingBelow(baseCell, notebook, returnIndex = false) {
let baseHeadingLevel = Private.Headings.determineHeadingLevel(baseCell, notebook);
if (baseHeadingLevel == -1) {
baseHeadingLevel = 1; // if no heading level can be determined, assume we're on level 1
}
let cellIdx = notebook.widgets.indexOf(baseCell) + 1;
while (cellIdx < notebook.widgets.length) {
let cell = notebook.widgets[cellIdx];
let headingInfo = NotebookActions.getHeadingInfo(cell);
if (headingInfo.isHeading &&
headingInfo.headingLevel <= baseHeadingLevel) {
return returnIndex ? cellIdx : cell;
}
cellIdx++;
}
return returnIndex ? -1 : null;
}
Headings.findLowerEqualLevelHeadingBelow = findLowerEqualLevelHeadingBelow;
/** Find next heading.
*
* @param baseCell - cell relative to which so search
* @param notebook - target notebook widget
* @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
*
* @returns the (index | Cell object) of the found heading or (-1 | null) if no heading found.
*/
function findHeadingBelow(baseCell, notebook, returnIndex = false) {
let cellIdx = notebook.widgets.indexOf(baseCell) + 1;
while (cellIdx < notebook.widgets.length) {
let cell = notebook.widgets[cellIdx];
let headingInfo = NotebookActions.getHeadingInfo(cell);
if (headingInfo.isHeading) {
return returnIndex ? cellIdx : cell;
}
cellIdx++;
}
return returnIndex ? -1 : null;
}
Headings.findHeadingBelow = findHeadingBelow;
/** Determine the heading level of a cell.
*
* @param baseCell - The cell of which the heading level shall be determined
* @param notebook - The target notebook widget
*
* @returns the heading level or -1 if there is no parent heading
*
* #### Notes
* If the baseCell is a heading itself, the heading level of baseCell is returned.
* If the baseCell is not a heading itself, the level of the parent heading is returned.
* If there is no parent heading, -1 is returned.
*/
function determineHeadingLevel(baseCell, notebook) {
let headingInfoBaseCell = NotebookActions.getHeadingInfo(baseCell);
// fill baseHeadingLevel or return null if there is no heading at or above baseCell
if (headingInfoBaseCell.isHeading) {
return headingInfoBaseCell.headingLevel;
}
else {
let parentHeading = findParentHeading(baseCell, notebook, true);
if (parentHeading == null) {
return -1;
}
return NotebookActions.getHeadingInfo(parentHeading).headingLevel;
}
}
Headings.determineHeadingLevel = determineHeadingLevel;
/** Insert a new heading cell at given position.
*
* @param cellIndex - where to insert
* @param headingLevel - level of the new heading
* @param notebook - target notebook
*
* #### Notes
* Enters edit mode after insert.
*/
async function insertHeadingAboveCellIndex(cellIndex, headingLevel, notebook) {
var _a;
headingLevel = Math.min(Math.max(headingLevel, 1), 6);
const state = Private.getState(notebook);
const model = notebook.model;
const sharedModel = model.sharedModel;
sharedModel.insertCell(cellIndex, {
cell_type: 'markdown',
source: '#'.repeat(headingLevel) + ' '
});
notebook.activeCellIndex = cellIndex;
if (((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.inViewport) === false) {
await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(() => {
// no-op
});
}
notebook.deselectAll();
void Private.handleState(notebook, state, true);
notebook.mode = 'edit';
notebook.widgets[cellIndex].setHidden(false);
}
Headings.insertHeadingAboveCellIndex = insertHeadingAboveCellIndex;
})(Headings = Private.Headings || (Private.Headings = {}));
})(Private || (Private = {}));
//# sourceMappingURL=actions.js.map