UNPKG

@jupyterlab/notebook

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