UNPKG

@jupyterlab/notebook

Version:
1,289 lines (1,288 loc) 96.3 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { Clipboard, Dialog, showDialog } from '@jupyterlab/apputils'; import { CodeCellModel, isMarkdownCellModel, isRawCellModel, MarkdownCell } from '@jupyterlab/cells'; import { Notification } from '@jupyterlab/apputils'; import { signalToPromise } from '@jupyterlab/coreutils'; import { nullTranslator } from '@jupyterlab/translation'; import { every, findIndex } from '@lumino/algorithm'; import { JSONExt } from '@lumino/coreutils'; import { Signal } from '@lumino/signaling'; import * as React from 'react'; import { runCell as defaultRunCell } from './cellexecutor'; /** * The mimetype used for Jupyter cell data. */ const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells'; export class KernelError extends Error { /** * Construct the kernel error. */ constructor(content) { const errorContent = content; const errorName = errorContent.ename; const errorValue = errorContent.evalue; super(`KernelReplyNotOK: ${errorName} ${errorValue}`); this.errorName = errorName; this.errorValue = errorValue; this.traceback = errorContent.traceback; Object.setPrototypeOf(this, KernelError.prototype); } } /** * A collection of actions that run against notebooks. * * #### Notes * All of the actions are a no-op if there is no model on the notebook. * The actions set the widget `mode` to `'command'` unless otherwise specified. * The actions will preserve the selection on the notebook widget unless * otherwise specified. */ export class NotebookActions { /** * A signal that emits whenever a cell completes execution. */ static get executed() { return Private.executed; } /** * A signal that emits whenever a cell execution is scheduled. */ static get executionScheduled() { return Private.executionScheduled; } /** * A signal that emits when one notebook's cells are all executed. */ static get selectionExecuted() { return Private.selectionExecuted; } /** * A signal that emits when a cell's output is cleared. */ static get outputCleared() { return Private.outputCleared; } /** * A private constructor for the `NotebookActions` class. * * #### Notes * This class can never be instantiated. Its static member `executed` will be * merged with the `NotebookActions` namespace. The reason it exists as a * standalone class is because at run time, the `Private.executed` variable * does not yet exist, so it needs to be referenced via a getter. */ constructor() { // Intentionally empty. } } /** * A namespace for `NotebookActions` static methods. */ (function (NotebookActions) { /** * Split the active cell into two or more cells. * * @param notebook The target notebook widget. * * #### Notes * It will preserve the existing mode. * The last cell will be activated if no selection is found. * If text was selected, the cell containing the selection will * be activated. * The existing selection will be cleared. * The activated cell will have focus and the cursor will * remain in the initial position. * The leading whitespace in the second cell will be removed. * If there is no content, two empty cells will be created. * Both cells will have the same type as the original cell. * This action can be undone. * The original cell is preserved to maintain kernel connections. */ function splitCell(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); // We force the notebook back in edit mode as splitting a cell // requires using the cursor position within a cell (aka it was recently in edit mode) // However the focus may be stolen if the action is triggered // from the menu entry; switching the notebook in command mode. notebook.mode = 'edit'; notebook.deselectAll(); const nbModel = notebook.model; const index = notebook.activeCellIndex; const child = notebook.widgets[index]; const editor = child.editor; if (!editor) { // TODO return; } const selections = editor.getSelections(); const orig = child.model.sharedModel.getSource(); const offsets = [0]; let start = -1; let end = -1; for (let i = 0; i < selections.length; i++) { // append start and end to handle selections // cursors will have same start and end start = editor.getOffsetAt(selections[i].start); end = editor.getOffsetAt(selections[i].end); if (start < end) { offsets.push(start); offsets.push(end); } else if (end < start) { offsets.push(end); offsets.push(start); } else { offsets.push(start); } } offsets.push(orig.length); // Create new cells for all content pieces EXCEPT the last one // The last piece will remain in the original cell to preserve kernel connection const newCells = offsets.slice(0, -2).map((offset, offsetIdx) => { const { cell_type, metadata } = child.model.sharedModel.toJSON(); return { cell_type, metadata, source: orig .slice(offset, offsets[offsetIdx + 1]) .replace(/^\n+/, '') .replace(/\n+$/, ''), outputs: undefined }; }); // Prepare the content for the original cell (last piece) const lastPieceStart = offsets[offsets.length - 2]; const lastPieceEnd = offsets[offsets.length - 1]; const lastPieceContent = orig .slice(lastPieceStart, lastPieceEnd) .replace(/^\n+/, '') .replace(/\n+$/, ''); nbModel.sharedModel.transact(() => { // Insert new cells above the current cell (if any) if (newCells.length > 0) { nbModel.sharedModel.insertCells(index, newCells); } // Update the original cell with the last piece of content child.model.sharedModel.setSource(lastPieceContent); // Mark cell as dirty if it is running if (child.model instanceof CodeCellModel) { const codeCellModel = child.model; if (codeCellModel.executionState === 'running') { codeCellModel.isDirty = true; } } }); // If there was a selection, activate the cell containing the selection let targetCellIndex; if (start !== end) { // Find which piece contains the selection let selectionPieceIndex = 0; for (let i = 0; i < offsets.length - 1; i++) { if (start >= offsets[i] && start < offsets[i + 1]) { selectionPieceIndex = i; break; } } targetCellIndex = index + selectionPieceIndex; } else { // No selection, activate the original cell (now at the end) targetCellIndex = index + newCells.length; } notebook.activeCellIndex = targetCellIndex; notebook .scrollToItem(notebook.activeCellIndex) .then(() => { var _a; (_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.editor.focus(); }) .catch(reason => { // no-op }); void Private.handleState(notebook, state); } NotebookActions.splitCell = splitCell; /** * Merge the selected cells. * * @param notebook - The target notebook widget. * * @param mergeAbove - If only one cell is selected, indicates whether to merge it * with the cell above (true) or below (false, default). * * #### Notes * The widget mode will be preserved. * If only one cell is selected and `mergeAbove` is true, the above cell will be selected. * If only one cell is selected and `mergeAbove` is false, the below cell will be selected. * If the active cell is a code cell, its outputs will be cleared. * This action can be undone. * The final cell will have the same type as the active cell. * If the active cell is a markdown cell, it will be unrendered. */ function mergeCells(notebook, mergeAbove = false) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); const toMerge = []; const toDelete = []; const model = notebook.model; const cells = model.cells; const primary = notebook.activeCell; const active = notebook.activeCellIndex; const attachments = {}; // Get the cells to merge. notebook.widgets.forEach((child, index) => { if (notebook.isSelectedOrActive(child)) { toMerge.push(child.model.sharedModel.getSource()); if (index !== active) { toDelete.push(index); } // Collect attachments if the cell is a markdown cell or a raw cell const model = child.model; if (isRawCellModel(model) || isMarkdownCellModel(model)) { for (const key of model.attachments.keys) { attachments[key] = model.attachments.get(key).toJSON(); } } } }); // Check for only a single cell selected. if (toMerge.length === 1) { // Merge with the cell above when mergeAbove is true if (mergeAbove === true) { // Bail if it is the first cell. if (active === 0) { return; } // Otherwise merge with the previous cell. const cellModel = cells.get(active - 1); toMerge.unshift(cellModel.sharedModel.getSource()); toDelete.push(active - 1); } else if (mergeAbove === false) { // Bail if it is the last cell. if (active === cells.length - 1) { return; } // Otherwise merge with the next cell. const cellModel = cells.get(active + 1); toMerge.push(cellModel.sharedModel.getSource()); toDelete.push(active + 1); } } notebook.deselectAll(); const primaryModel = primary.model.sharedModel; const { cell_type, metadata } = primaryModel.toJSON(); if (primaryModel.cell_type === 'code') { // We can trust this cell because the outputs will be removed. metadata.trusted = true; } const newModel = { cell_type, metadata, source: toMerge.join('\n\n'), attachments: primaryModel.cell_type === 'markdown' || primaryModel.cell_type === 'raw' ? attachments : undefined }; // Make the changes while preserving history. model.sharedModel.transact(() => { model.sharedModel.deleteCell(active); model.sharedModel.insertCell(active, newModel); toDelete .sort((a, b) => b - a) .forEach(index => { model.sharedModel.deleteCell(index); }); }); // If the original cell is a markdown cell, make sure // the new cell is unrendered. if (primary instanceof MarkdownCell) { notebook.activeCell.rendered = false; } void Private.handleState(notebook, state); } NotebookActions.mergeCells = mergeCells; /** * Delete the selected cells. * * @param notebook - The target notebook widget. * * #### Notes * The cell after the last selected cell will be activated. * It will add a code cell if all cells are deleted. * This action can be undone. */ function deleteCells(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); Private.deleteCells(notebook); void Private.handleState(notebook, state, true); } NotebookActions.deleteCells = deleteCells; /** * Insert a new code cell above the active cell or in index 0 if the notebook is empty. * * @param notebook - The target notebook widget. * * #### Notes * The widget mode will be preserved. * This action can be undone. * The existing selection will be cleared. * The new cell will the active cell. */ function insertAbove(notebook) { if (!notebook.model) { return; } const state = Private.getState(notebook); const model = notebook.model; const newIndex = notebook.activeCell ? notebook.activeCellIndex : 0; model.sharedModel.insertCell(newIndex, { cell_type: notebook.notebookConfig.defaultCell, metadata: notebook.notebookConfig.defaultCell === 'code' ? { // This is an empty cell created by user, thus is trusted trusted: true } : {} }); // Make the newly inserted cell active. notebook.activeCellIndex = newIndex; notebook.deselectAll(); void Private.handleState(notebook, state, true); } NotebookActions.insertAbove = insertAbove; /** * Insert a new code cell below the active cell or in index 0 if the notebook is empty. * * @param notebook - The target notebook widget. * * #### Notes * The widget mode will be preserved. * This action can be undone. * The existing selection will be cleared. * The new cell will be the active cell. */ function insertBelow(notebook) { if (!notebook.model) { return; } const state = Private.getState(notebook); const model = notebook.model; const newIndex = notebook.activeCell ? notebook.activeCellIndex + 1 : 0; model.sharedModel.insertCell(newIndex, { cell_type: notebook.notebookConfig.defaultCell, metadata: notebook.notebookConfig.defaultCell === 'code' ? { // This is an empty cell created by user, thus is trusted trusted: true } : {} }); // Make the newly inserted cell active. notebook.activeCellIndex = newIndex; notebook.deselectAll(); void Private.handleState(notebook, state, true); } NotebookActions.insertBelow = insertBelow; function move(notebook, shift) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); const firstIndex = notebook.widgets.findIndex(w => notebook.isSelectedOrActive(w)); let lastIndex = notebook.widgets .slice(firstIndex + 1) .findIndex(w => !notebook.isSelectedOrActive(w)); if (lastIndex >= 0) { lastIndex += firstIndex + 1; } else { lastIndex = notebook.model.cells.length; } if (shift > 0) { notebook.moveCell(firstIndex, lastIndex, lastIndex - firstIndex); } else { notebook.moveCell(firstIndex, firstIndex + shift, lastIndex - firstIndex); } void Private.handleState(notebook, state, true); } /** * Move the selected cell(s) down. * * @param notebook = The target notebook widget. */ function moveDown(notebook) { move(notebook, 1); } NotebookActions.moveDown = moveDown; /** * Move the selected cell(s) up. * * @param notebook - The target notebook widget. */ function moveUp(notebook) { move(notebook, -1); } NotebookActions.moveUp = moveUp; /** * Change the selected cell type(s). * * @param notebook - The target notebook widget. * @param value - The target cell type. * @param translator - The application translator. * * #### Notes * It should preserve the widget mode. * This action can be undone. * The existing selection will be cleared. * Any cells converted to markdown will be unrendered. */ function changeCellType(notebook, value, translator) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); Private.changeCellType(notebook, value, translator); void Private.handleState(notebook, state); } NotebookActions.changeCellType = changeCellType; /** * Run the selected cell(s). * * @param notebook - The target notebook widget. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * The last selected cell will be activated, but not scrolled into view. * The existing selection will be cleared. * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. */ function run(notebook, sessionContext, sessionDialogs, translator) { if (!notebook.model || !notebook.activeCell) { return Promise.resolve(false); } const state = Private.getState(notebook); const promise = Private.runSelected(notebook, sessionContext, sessionDialogs, translator); void Private.handleRunState(notebook, state); return promise; } NotebookActions.run = run; /** * Run specified cells. * * @param notebook - The target notebook widget. * @param cells - The cells to run. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * The existing selection will be preserved. * The mode will be changed to command. * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. */ function runCells(notebook, cells, sessionContext, sessionDialogs, translator) { if (!notebook.model) { return Promise.resolve(false); } const state = Private.getState(notebook); const promise = Private.runCells(notebook, cells, sessionContext, sessionDialogs, translator); void Private.handleRunState(notebook, state); return promise; } NotebookActions.runCells = runCells; /** * Run the selected cell(s) and advance to the next cell. * * @param notebook - The target notebook widget. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * The existing selection will be cleared. * The cell after the last selected cell will be activated and scrolled into view. * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. * If the last selected cell is the last cell, a new code cell * will be created in `'edit'` mode. The new cell creation can be undone. */ async function runAndAdvance(notebook, sessionContext, sessionDialogs, translator) { var _a; if (!notebook.model || !notebook.activeCell) { return Promise.resolve(false); } const state = Private.getState(notebook); const promise = Private.runSelected(notebook, sessionContext, sessionDialogs, translator); const model = notebook.model; if (notebook.activeCellIndex === notebook.widgets.length - 1) { // Do not use push here, as we want an widget insertion // to make sure no placeholder widget is rendered. model.sharedModel.insertCell(notebook.widgets.length, { cell_type: notebook.notebookConfig.defaultCell, metadata: notebook.notebookConfig.defaultCell === 'code' ? { // This is an empty cell created by user, thus is trusted trusted: true } : {} }); notebook.activeCellIndex++; if (((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.inViewport) === false) { await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(() => { // no-op }); } notebook.mode = 'edit'; } else { notebook.activeCellIndex++; } // If a cell is outside of viewport and scrolling is needed, the `smart` // logic in `handleRunState` will choose appropriate alignment, except // for the case of a small cell less than one viewport away for which it // would use the `auto` heuristic, for which we set the preferred alignment // to `center` as in most cases there will be space below and above a cell // that is smaller than (or approximately equal to) the viewport size. void Private.handleRunState(notebook, state, 'center'); return promise; } NotebookActions.runAndAdvance = runAndAdvance; /** * Run the selected cell(s) and insert a new code cell. * * @param notebook - The target notebook widget. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. * The widget mode will be set to `'edit'` after running. * The existing selection will be cleared. * The cell insert can be undone. * The new cell will be scrolled into view. */ async function runAndInsert(notebook, sessionContext, sessionDialogs, translator) { var _a; if (!notebook.model || !notebook.activeCell) { return Promise.resolve(false); } const state = Private.getState(notebook); const promise = Private.runSelected(notebook, sessionContext, sessionDialogs, translator); const model = notebook.model; model.sharedModel.insertCell(notebook.activeCellIndex + 1, { cell_type: notebook.notebookConfig.defaultCell, metadata: notebook.notebookConfig.defaultCell === 'code' ? { // This is an empty cell created by user, thus is trusted trusted: true } : {} }); notebook.activeCellIndex++; if (((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.inViewport) === false) { await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(() => { // no-op }); } notebook.mode = 'edit'; void Private.handleRunState(notebook, state, 'center'); return promise; } NotebookActions.runAndInsert = runAndInsert; /** * Run all of the cells in the notebook. * * @param notebook - The target notebook widget. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * The existing selection will be cleared. * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. * The last cell in the notebook will be activated and scrolled into view. */ function runAll(notebook, sessionContext, sessionDialogs, translator) { if (!notebook.model || !notebook.activeCell) { return Promise.resolve(false); } const state = Private.getState(notebook); const lastIndex = notebook.widgets.length; const promise = Private.runCells(notebook, notebook.widgets, sessionContext, sessionDialogs, translator); notebook.activeCellIndex = lastIndex; notebook.deselectAll(); void Private.handleRunState(notebook, state); return promise; } NotebookActions.runAll = runAll; function renderAllMarkdown(notebook) { if (!notebook.model || !notebook.activeCell) { return Promise.resolve(false); } const previousIndex = notebook.activeCellIndex; const state = Private.getState(notebook); notebook.widgets.forEach((child, index) => { if (child.model.type === 'markdown') { notebook.select(child); // This is to make sure that the activeCell // does not get executed notebook.activeCellIndex = index; } }); if (notebook.activeCell.model.type !== 'markdown') { return Promise.resolve(true); } const promise = Private.runSelected(notebook); notebook.activeCellIndex = previousIndex; void Private.handleRunState(notebook, state); return promise; } NotebookActions.renderAllMarkdown = renderAllMarkdown; /** * Run all of the cells before the currently active cell (exclusive). * * @param notebook - The target notebook widget. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * The existing selection will be cleared. * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. * The currently active cell will remain selected. */ function runAllAbove(notebook, sessionContext, sessionDialogs, translator) { const { activeCell, activeCellIndex, model } = notebook; if (!model || !activeCell || activeCellIndex < 1) { return Promise.resolve(false); } const state = Private.getState(notebook); const promise = Private.runCells(notebook, notebook.widgets.slice(0, notebook.activeCellIndex), sessionContext, sessionDialogs, translator); notebook.deselectAll(); void Private.handleRunState(notebook, state); return promise; } NotebookActions.runAllAbove = runAllAbove; /** * Run all of the cells after the currently active cell (inclusive). * * @param notebook - The target notebook widget. * @param sessionContext - The client session object. * @param sessionDialogs - The session dialogs. * @param translator - The application translator. * * #### Notes * The existing selection will be cleared. * An execution error will prevent the remaining code cells from executing. * All markdown cells will be rendered. * The last cell in the notebook will be activated and scrolled into view. */ function runAllBelow(notebook, sessionContext, sessionDialogs, translator) { if (!notebook.model || !notebook.activeCell) { return Promise.resolve(false); } const state = Private.getState(notebook); const lastIndex = notebook.widgets.length; const promise = Private.runCells(notebook, notebook.widgets.slice(notebook.activeCellIndex), sessionContext, sessionDialogs, translator); notebook.activeCellIndex = lastIndex; notebook.deselectAll(); void Private.handleRunState(notebook, state); return promise; } NotebookActions.runAllBelow = runAllBelow; /** * Replaces the selection in the active cell of the notebook. * * @param notebook - The target notebook widget. * @param text - The text to replace the selection. */ function replaceSelection(notebook, text) { var _a, _b, _c; if (!notebook.model || !((_a = notebook.activeCell) === null || _a === void 0 ? void 0 : _a.editor)) { return; } (_c = (_b = notebook.activeCell.editor).replaceSelection) === null || _c === void 0 ? void 0 : _c.call(_b, text); } NotebookActions.replaceSelection = replaceSelection; /** * Select the above the active cell. * * @param notebook - The target notebook widget. * * #### Notes * The widget mode will be preserved. * This is a no-op if the first cell is the active cell. * This will skip any collapsed cells. * The existing selection will be cleared. */ function selectAbove(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const footer = notebook.layout.footer; if (footer && document.activeElement === footer.node) { footer.node.blur(); notebook.mode = 'command'; return; } if (notebook.activeCellIndex === 0) { return; } let possibleNextCellIndex = notebook.activeCellIndex - 1; // find first non hidden cell above current cell while (possibleNextCellIndex >= 0) { const possibleNextCell = notebook.widgets[possibleNextCellIndex]; if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) { break; } possibleNextCellIndex -= 1; } const state = Private.getState(notebook); notebook.activeCellIndex = possibleNextCellIndex; notebook.deselectAll(); void Private.handleState(notebook, state, true); } NotebookActions.selectAbove = selectAbove; /** * Select the cell below the active cell. * * @param notebook - The target notebook widget. * * #### Notes * The widget mode will be preserved. * This is a no-op if the last cell is the active cell. * This will skip any collapsed cells. * The existing selection will be cleared. */ function selectBelow(notebook) { if (!notebook.model || !notebook.activeCell) { return; } let maxCellIndex = notebook.widgets.length - 1; // Find last non-hidden cell while (notebook.widgets[maxCellIndex].isHidden || notebook.widgets[maxCellIndex].inputHidden) { maxCellIndex -= 1; } if (notebook.activeCellIndex === maxCellIndex) { const footer = notebook.layout.footer; footer === null || footer === void 0 ? void 0 : footer.node.focus(); return; } let possibleNextCellIndex = notebook.activeCellIndex + 1; // find first non hidden cell below current cell while (possibleNextCellIndex < maxCellIndex) { let possibleNextCell = notebook.widgets[possibleNextCellIndex]; if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) { break; } possibleNextCellIndex += 1; } const state = Private.getState(notebook); notebook.activeCellIndex = possibleNextCellIndex; notebook.deselectAll(); void Private.handleState(notebook, state, true); } NotebookActions.selectBelow = selectBelow; /** Insert new heading of same level above active cell. * * @param notebook - The target notebook widget */ async function insertSameLevelHeadingAbove(notebook) { if (!notebook.model || !notebook.activeCell) { return; } let headingLevel = Private.Headings.determineHeadingLevel(notebook.activeCell, notebook); if (headingLevel == -1) { await Private.Headings.insertHeadingAboveCellIndex(0, 1, notebook); } else { await Private.Headings.insertHeadingAboveCellIndex(notebook.activeCellIndex, headingLevel, notebook); } } NotebookActions.insertSameLevelHeadingAbove = insertSameLevelHeadingAbove; /** Insert new heading of same level at end of current section. * * @param notebook - The target notebook widget */ async function insertSameLevelHeadingBelow(notebook) { if (!notebook.model || !notebook.activeCell) { return; } let headingLevel = Private.Headings.determineHeadingLevel(notebook.activeCell, notebook); headingLevel = headingLevel > -1 ? headingLevel : 1; let cellIdxOfHeadingBelow = Private.Headings.findLowerEqualLevelHeadingBelow(notebook.activeCell, notebook, true); await Private.Headings.insertHeadingAboveCellIndex(cellIdxOfHeadingBelow == -1 ? notebook.model.cells.length : cellIdxOfHeadingBelow, headingLevel, notebook); } NotebookActions.insertSameLevelHeadingBelow = insertSameLevelHeadingBelow; /** * Select the heading above the active cell or, if already at heading, collapse it. * * @param notebook - The target notebook widget. * * #### Notes * The widget mode will be preserved. * This is a no-op if the active cell is the topmost heading in collapsed state * The existing selection will be cleared. */ function selectHeadingAboveOrCollapseHeading(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); let hInfoActiveCell = getHeadingInfo(notebook.activeCell); // either collapse or find the right heading to jump to if (hInfoActiveCell.isHeading && !hInfoActiveCell.collapsed) { setHeadingCollapse(notebook.activeCell, true, notebook); } else { let targetHeadingCellIdx = Private.Headings.findLowerEqualLevelParentHeadingAbove(notebook.activeCell, notebook, true); if (targetHeadingCellIdx > -1) { notebook.activeCellIndex = targetHeadingCellIdx; } } // clear selection and handle state notebook.deselectAll(); void Private.handleState(notebook, state, true); } NotebookActions.selectHeadingAboveOrCollapseHeading = selectHeadingAboveOrCollapseHeading; /** * Select the heading below the active cell or, if already at heading, expand it. * * @param notebook - The target notebook widget. * * #### Notes * The widget mode will be preserved. * This is a no-op if the active cell is the last heading in expanded state * The existing selection will be cleared. */ function selectHeadingBelowOrExpandHeading(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); let hInfo = getHeadingInfo(notebook.activeCell); if (hInfo.isHeading && hInfo.collapsed) { setHeadingCollapse(notebook.activeCell, false, notebook); } else { let targetHeadingCellIdx = Private.Headings.findHeadingBelow(notebook.activeCell, notebook, true // return index of heading cell ); if (targetHeadingCellIdx > -1) { notebook.activeCellIndex = targetHeadingCellIdx; } } notebook.deselectAll(); void Private.handleState(notebook, state, true); } NotebookActions.selectHeadingBelowOrExpandHeading = selectHeadingBelowOrExpandHeading; /** * Extend the selection to the cell above. * * @param notebook - The target notebook widget. * @param toTop - If true, denotes selection to extend to the top. * * #### Notes * This is a no-op if the first cell is the active cell. * The new cell will be activated. */ function extendSelectionAbove(notebook, toTop = false) { if (!notebook.model || !notebook.activeCell) { return; } // Do not wrap around. if (notebook.activeCellIndex === 0) { return; } const state = Private.getState(notebook); notebook.mode = 'command'; // Check if toTop is true, if yes, selection is made to the top. if (toTop) { notebook.extendContiguousSelectionTo(0); } else { notebook.extendContiguousSelectionTo(notebook.activeCellIndex - 1); } void Private.handleState(notebook, state, true); } NotebookActions.extendSelectionAbove = extendSelectionAbove; /** * Extend the selection to the cell below. * * @param notebook - The target notebook widget. * @param toBottom - If true, denotes selection to extend to the bottom. * * #### Notes * This is a no-op if the last cell is the active cell. * The new cell will be activated. */ function extendSelectionBelow(notebook, toBottom = false) { if (!notebook.model || !notebook.activeCell) { return; } // Do not wrap around. if (notebook.activeCellIndex === notebook.widgets.length - 1) { return; } const state = Private.getState(notebook); notebook.mode = 'command'; // Check if toBottom is true, if yes selection is made to the bottom. if (toBottom) { notebook.extendContiguousSelectionTo(notebook.widgets.length - 1); } else { notebook.extendContiguousSelectionTo(notebook.activeCellIndex + 1); } void Private.handleState(notebook, state, true); } NotebookActions.extendSelectionBelow = extendSelectionBelow; /** * Select all of the cells of the notebook. * * @param notebook - the target notebook widget. */ function selectAll(notebook) { if (!notebook.model || !notebook.activeCell) { return; } notebook.widgets.forEach(child => { notebook.select(child); }); } NotebookActions.selectAll = selectAll; /** * Deselect all of the cells of the notebook. * * @param notebook - the target notebook widget. */ function deselectAll(notebook) { if (!notebook.model || !notebook.activeCell) { return; } notebook.deselectAll(); } NotebookActions.deselectAll = deselectAll; /** * Copy the selected cell(s) data to a clipboard. * * @param notebook - The target notebook widget. */ function copy(notebook) { Private.copyOrCut(notebook, false); } NotebookActions.copy = copy; /** * Cut the selected cell data to a clipboard. * * @param notebook - The target notebook widget. * * #### Notes * This action can be undone. * A new code cell is added if all cells are cut. */ function cut(notebook) { Private.copyOrCut(notebook, true); } NotebookActions.cut = cut; /** * Paste cells from the application clipboard. * * @param notebook - The target notebook widget. * * @param mode - the mode of adding cells: * 'below' (default) adds cells below the active cell, * 'belowSelected' adds cells below all selected cells, * 'above' adds cells above the active cell, and * 'replace' removes the currently selected cells and adds cells in their place. * * #### Notes * The last pasted cell becomes the active cell. * This is a no-op if there is no cell data on the clipboard. * This action can be undone. */ function paste(notebook, mode = 'below') { const clipboard = Clipboard.getInstance(); if (!clipboard.hasData(JUPYTER_CELL_MIME)) { return; } const values = clipboard.getData(JUPYTER_CELL_MIME); addCells(notebook, mode, values, true); void focusActiveCell(notebook); } NotebookActions.paste = paste; /** * Duplicate selected cells in the notebook without using the application clipboard. * * @param notebook - The target notebook widget. * * @param mode - the mode of adding cells: * 'below' (default) adds cells below the active cell, * 'belowSelected' adds cells below all selected cells, * 'above' adds cells above the active cell, and * 'replace' removes the currently selected cells and adds cells in their place. * * #### Notes * The last pasted cell becomes the active cell. * This is a no-op if there is no cell data on the clipboard. * This action can be undone. */ function duplicate(notebook, mode = 'below') { const values = Private.selectedCells(notebook); if (!values || values.length === 0) { return; } addCells(notebook, mode, values, false); // Cells not from the clipboard } NotebookActions.duplicate = duplicate; /** * Adds cells to the notebook. * * @param notebook - The target notebook widget. * * @param mode - the mode of adding cells: * 'below' (default) adds cells below the active cell, * 'belowSelected' adds cells below all selected cells, * 'above' adds cells above the active cell, and * 'replace' removes the currently selected cells and adds cells in their place. * * @param values — The cells to add to the notebook. * * @param cellsFromClipboard — True if the cells were sourced from the clipboard. * * #### Notes * The last added cell becomes the active cell. * This is a no-op if values is an empty array. * This action can be undone. */ function addCells(notebook, mode = 'below', values, cellsFromClipboard = false) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); const model = notebook.model; notebook.mode = 'command'; let index = 0; const prevActiveCellIndex = notebook.activeCellIndex; model.sharedModel.transact(() => { // Set the starting index of the paste operation depending upon the mode. switch (mode) { case 'below': index = notebook.activeCellIndex + 1; break; case 'belowSelected': notebook.widgets.forEach((child, childIndex) => { if (notebook.isSelectedOrActive(child)) { index = childIndex + 1; } }); break; case 'above': index = notebook.activeCellIndex; break; case 'replace': { // Find the cells to delete. const toDelete = []; notebook.widgets.forEach((child, index) => { const deletable = child.model.sharedModel.getMetadata('deletable') !== false; if (notebook.isSelectedOrActive(child) && deletable) { toDelete.push(index); } }); // If cells are not deletable, we may not have anything to delete. if (toDelete.length > 0) { // Delete the cells as one undo event. toDelete.reverse().forEach(i => { model.sharedModel.deleteCell(i); }); } index = toDelete[0]; break; } default: break; } model.sharedModel.insertCells(index, values.map(cell => { cell.id = cell.cell_type === 'code' && notebook.lastClipboardInteraction === 'cut' && typeof cell.id === 'string' ? cell.id : undefined; return cell; })); }); notebook.activeCellIndex = prevActiveCellIndex + values.length; notebook.deselectAll(); if (cellsFromClipboard) { notebook.lastClipboardInteraction = 'paste'; } void Private.handleState(notebook, state, true); } /** * Undo a cell action. * * @param notebook - The target notebook widget. * * #### Notes * This is a no-op if there are no cell actions to undo. */ function undo(notebook) { if (!notebook.model) { return; } const state = Private.getState(notebook); notebook.mode = 'command'; notebook.model.sharedModel.undo(); notebook.deselectAll(); void Private.handleState(notebook, state); } NotebookActions.undo = undo; /** * Redo a cell action. * * @param notebook - The target notebook widget. * * #### Notes * This is a no-op if there are no cell actions to redo. */ function redo(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); notebook.mode = 'command'; notebook.model.sharedModel.redo(); notebook.deselectAll(); void Private.handleState(notebook, state); } NotebookActions.redo = redo; /** * Toggle the line number of all cells. * * @param notebook - The target notebook widget. * * #### Notes * The original state is based on the state of the active cell. * The `mode` of the widget will be preserved. */ function toggleAllLineNumbers(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); const config = notebook.editorConfig; const lineNumbers = !(config.code.lineNumbers && config.markdown.lineNumbers && config.raw.lineNumbers); const newConfig = { code: { ...config.code, lineNumbers }, markdown: { ...config.markdown, lineNumbers }, raw: { ...config.raw, lineNumbers } }; notebook.editorConfig = newConfig; void Private.handleState(notebook, state); } NotebookActions.toggleAllLineNumbers = toggleAllLineNumbers; /** * Clear the code outputs of the selected cells. * * @param notebook - The target notebook widget. * * #### Notes * The widget `mode` will be preserved. */ function clearOutputs(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); let index = -1; for (const cell of notebook.model.cells) { const child = notebook.widgets[++index]; if (notebook.isSelectedOrActive(child) && cell.type === 'code') { cell.sharedModel.transact(() => { cell.clearExecution(); child.outputHidden = false; }, false); Private.outputCleared.emit({ notebook, cell: child }); } } void Private.handleState(notebook, state, true); } NotebookActions.clearOutputs = clearOutputs; /** * Clear all the code outputs on the widget. * * @param notebook - The target notebook widget. * * #### Notes * The widget `mode` will be preserved. */ function clearAllOutputs(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); let index = -1; for (const cell of notebook.model.cells) { const child = notebook.widgets[++index]; if (cell.type === 'code') { cell.sharedModel.transact(() => { cell.clearExecution(); child.outputHidden = false; }, false); Private.outputCleared.emit({ notebook, cell: child }); } } void Private.handleState(notebook, state, true); } NotebookActions.clearAllOutputs = clearAllOutputs; /** * Hide the code on selected code cells. * * @param notebook - The target notebook widget. */ function hideCode(notebook) { if (!notebook.model || !notebook.activeCell) { return; } const state = Private.getState(notebook); notebook.widgets.forEach(cell => { if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') { cell.inputHidden = true; } }); void Private.handleState(notebook, state); } NotebookActions.hideCode = hideCode; /** * Show the code on selected code cells. * * @param notebook - The target notebook widget. */ function showCode(notebook) { if (!notebook.model || !notebook.activeCell) { return;