123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { KernelMessage } from '@jupyterlab/services';
- import {
- IClientSession,
- Clipboard,
- Dialog,
- showDialog
- } from '@jupyterlab/apputils';
- import { nbformat } from '@jupyterlab/coreutils';
- import {
- ICellModel,
- ICodeCellModel,
- CodeCell,
- Cell,
- MarkdownCell
- } from '@jupyterlab/cells';
- import { ArrayExt, each, toArray } from '@phosphor/algorithm';
- import { JSONObject } from '@phosphor/coreutils';
- import { ElementExt } from '@phosphor/domutils';
- import { ISignal, Signal } from '@phosphor/signaling';
- import * as React from 'react';
- import { INotebookModel } from './model';
- import { Notebook } from './widget';
- // The message to display to the user when prompting to trust the notebook.
- const TRUST_MESSAGE = (
- <p>
- A trusted Jupyter notebook may execute hidden malicious code when you open
- it.
- <br />
- Selecting trust will re-render this notebook in a trusted state.
- <br />
- For more information, see the
- <a href="https://jupyter-notebook.readthedocs.io/en/stable/security.html">
- Jupyter security documentation
- </a>
- </p>
- );
- /**
- * The mimetype used for Jupyter cell data.
- */
- const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
- /**
- * 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 is run.
- */
- static get executed(): ISignal<any, { notebook: Notebook; cell: Cell }> {
- return Private.executed;
- }
- /**
- * 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.
- */
- private constructor() {}
- }
- /**
- * A namespace for `NotebookActions` static methods.
- */
- export namespace NotebookActions {
- /**
- * Split the active cell into two cells.
- *
- * @param widget - The target notebook widget.
- *
- * #### Notes
- * It will preserve the existing mode.
- * The second cell will be activated.
- * The existing selection will be cleared.
- * 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.
- */
- export function splitCell(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- 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 position = editor.getCursorPosition();
- const offset = editor.getOffsetAt(position);
- const orig = child.model.value.text;
- // Create new models to preserve history.
- const clone0 = Private.cloneCell(nbModel, child.model);
- const clone1 = Private.cloneCell(nbModel, child.model);
- if (clone0.type === 'code') {
- (clone0 as ICodeCellModel).outputs.clear();
- }
- clone0.value.text = orig
- .slice(0, offset)
- .replace(/^\n+/, '')
- .replace(/\n+$/, '');
- clone1.value.text = orig
- .slice(offset)
- .replace(/^\n+/, '')
- .replace(/\n+$/, '');
- // Make the changes while preserving history.
- const cells = nbModel.cells;
- cells.beginCompoundOperation();
- cells.set(index, clone0);
- cells.insert(index + 1, clone1);
- cells.endCompoundOperation();
- notebook.activeCellIndex++;
- Private.handleState(notebook, state);
- }
- /**
- * Merge the selected cells.
- *
- * @param notebook - The target notebook widget.
- *
- * #### Notes
- * The widget mode will be preserved.
- * If only one cell is selected, the next 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.
- */
- export function mergeCells(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- const toMerge: string[] = [];
- const toDelete: ICellModel[] = [];
- const model = notebook.model;
- const cells = model.cells;
- const primary = notebook.activeCell;
- const active = notebook.activeCellIndex;
- // 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);
- }
- }
- });
- // Check for only a single cell selected.
- if (toMerge.length === 1) {
- // 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 (newModel.type === 'code') {
- (newModel as ICodeCellModel).outputs.clear();
- }
- // 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 as MarkdownCell).rendered = false;
- }
- Private.handleState(notebook, state);
- }
- /**
- * 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.
- */
- export function deleteCells(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- Private.deleteCells(notebook);
- Private.handleState(notebook, state);
- }
- /**
- * 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.
- */
- export function insertAbove(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- 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);
- }
- /**
- * 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.
- */
- export function insertBelow(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- 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);
- }
- /**
- * Move the selected cell(s) down.
- *
- * @param notebook = The target notebook widget.
- */
- export function moveDown(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- 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);
- }
- /**
- * Move the selected cell(s) up.
- *
- * @param widget - The target notebook widget.
- */
- export function moveUp(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- 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);
- }
- /**
- * 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.
- */
- export function changeCellType(
- notebook: Notebook,
- value: nbformat.CellType
- ): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- Private.changeCellType(notebook, value);
- Private.handleState(notebook, state);
- }
- /**
- * Run the selected cell(s).
- *
- * @param notebook - The target notebook widget.
- *
- * @param session - 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.
- */
- export function run(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- if (!notebook.model || !notebook.activeCell) {
- return Promise.resolve(false);
- }
- const state = Private.getState(notebook);
- const promise = Private.runSelected(notebook, session);
- Private.handleRunState(notebook, state, false);
- return promise;
- }
- /**
- * Run the selected cell(s) and advance to the next cell.
- *
- * @param notebook - The target notebook widget.
- *
- * @param session - 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.
- */
- export function runAndAdvance(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- if (!notebook.model || !notebook.activeCell) {
- return Promise.resolve(false);
- }
- const state = Private.getState(notebook);
- const promise = Private.runSelected(notebook, session);
- const model = notebook.model;
- if (notebook.activeCellIndex === notebook.widgets.length - 1) {
- const cell = model.contentFactory.createCell(
- notebook.notebookConfig.defaultCell,
- {}
- );
- model.cells.push(cell);
- notebook.activeCellIndex++;
- notebook.mode = 'edit';
- } else {
- notebook.activeCellIndex++;
- }
- Private.handleRunState(notebook, state, true);
- return promise;
- }
- /**
- * Run the selected cell(s) and insert a new code cell.
- *
- * @param notebook - The target notebook widget.
- *
- * @param session - 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.
- */
- export function runAndInsert(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- if (!notebook.model || !notebook.activeCell) {
- return Promise.resolve(false);
- }
- const state = Private.getState(notebook);
- const promise = Private.runSelected(notebook, session);
- 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;
- }
- /**
- * Run all of the cells in the notebook.
- *
- * @param notebook - The target notebook widget.
- *
- * @param session - 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.
- */
- export function runAll(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- 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, session);
- Private.handleRunState(notebook, state, true);
- return promise;
- }
- export function renderAllMarkdown(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- 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, session);
- notebook.activeCellIndex = previousIndex;
- Private.handleRunState(notebook, state, true);
- return promise;
- }
- /**
- * Run all of the cells before the currently active cell (exclusive).
- *
- * @param notebook - The target notebook widget.
- *
- * @param session - 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.
- */
- export function runAllAbove(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- 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, session);
- notebook.activeCellIndex++;
- Private.handleRunState(notebook, state, true);
- return promise;
- }
- /**
- * Run all of the cells after the currently active cell (inclusive).
- *
- * @param notebook - The target notebook widget.
- *
- * @param session - 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.
- */
- export function runAllBelow(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- 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, session);
- Private.handleRunState(notebook, state, true);
- return promise;
- }
- /**
- * 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.
- */
- export function selectAbove(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- if (notebook.activeCellIndex === 0) {
- return;
- }
- let possibleNextCell = notebook.activeCellIndex - 1;
- // find first non hidden cell above current cell
- if (notebook.mode === 'edit') {
- while (notebook.widgets[possibleNextCell].inputHidden) {
- // If we are at the top cell, we cannot change selection.
- if (possibleNextCell === 0) {
- return;
- }
- possibleNextCell -= 1;
- }
- }
- const state = Private.getState(notebook);
- notebook.activeCellIndex = possibleNextCell;
- notebook.deselectAll();
- Private.handleState(notebook, state, true);
- }
- /**
- * 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.
- */
- export function selectBelow(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const maxCellIndex = notebook.widgets.length - 1;
- if (notebook.activeCellIndex === maxCellIndex) {
- return;
- }
- let possibleNextCell = notebook.activeCellIndex + 1;
- // find first non hidden cell below current cell
- if (notebook.mode === 'edit') {
- while (notebook.widgets[possibleNextCell].inputHidden) {
- // If we are at the bottom cell, we cannot change selection.
- if (possibleNextCell === maxCellIndex) {
- return;
- }
- possibleNextCell += 1;
- }
- }
- const state = Private.getState(notebook);
- notebook.activeCellIndex = possibleNextCell;
- notebook.deselectAll();
- Private.handleState(notebook, state, true);
- }
- /**
- * Extend the selection to the cell above.
- *
- * @param notebook - The target notebook widget.
- *
- * #### Notes
- * This is a no-op if the first cell is the active cell.
- * The new cell will be activated.
- */
- export function extendSelectionAbove(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- // Do not wrap around.
- if (notebook.activeCellIndex === 0) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.mode = 'command';
- notebook.extendContiguousSelectionTo(notebook.activeCellIndex - 1);
- Private.handleState(notebook, state, true);
- }
- /**
- * Extend the selection to the cell below.
- *
- * @param notebook - The target notebook widget.
- *
- * #### Notes
- * This is a no-op if the last cell is the active cell.
- * The new cell will be activated.
- */
- export function extendSelectionBelow(notebook: Notebook): void {
- 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';
- notebook.extendContiguousSelectionTo(notebook.activeCellIndex + 1);
- Private.handleState(notebook, state, true);
- }
- /**
- * Select all of the cells of the notebook.
- *
- * @param notebook - the target notebook widget.
- */
- export function selectAll(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- notebook.widgets.forEach(child => {
- notebook.select(child);
- });
- }
- /**
- * Deselect all of the cells of the notebook.
- *
- * @param notebook - the targe notebook widget.
- */
- export function deselectAll(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- notebook.deselectAll();
- }
- /**
- * Copy the selected cell data to a clipboard.
- *
- * @param notebook - The target notebook widget.
- */
- export function copy(notebook: Notebook): void {
- Private.copyOrCut(notebook, false);
- }
- /**
- * 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.
- */
- export function cut(notebook: Notebook): void {
- Private.copyOrCut(notebook, true);
- }
- /**
- * Paste cells from the application clipboard.
- *
- * @param notebook - The target notebook widget.
- *
- * @param mode - the mode of the paste operation: 'below' pastes cells
- * below the active cell, 'above' pastes cells above the active cell,
- * and 'replace' removes the currently selected cells and pastes 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.
- */
- export function paste(
- notebook: Notebook,
- mode: 'below' | 'above' | 'replace' = 'below'
- ): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const clipboard = Clipboard.getInstance();
- if (!clipboard.hasData(JUPYTER_CELL_MIME)) {
- return;
- }
- const state = Private.getState(notebook);
- const values = clipboard.getData(JUPYTER_CELL_MIME) as nbformat.IBaseCell[];
- const model = notebook.model;
- notebook.mode = 'command';
- const newCells = values.map(cell => {
- switch (cell.cell_type) {
- case 'code':
- 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: number;
- cells.beginCompoundOperation();
- // Set the starting index of the paste operation depending upon the mode.
- switch (mode) {
- case 'below':
- index = notebook.activeCellIndex;
- break;
- case 'above':
- index = notebook.activeCellIndex - 1;
- break;
- case 'replace':
- // Find the cells to delete.
- const toDelete: number[] = [];
- 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();
- 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.
- */
- export function undo(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.mode = 'command';
- notebook.model.cells.undo();
- notebook.deselectAll();
- Private.handleState(notebook, state);
- }
- /**
- * Redo a cell action.
- *
- * @param notebook - The target notebook widget.
- *
- * #### Notes
- * This is a no-op if there are no cell actions to redo.
- */
- export function redo(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.mode = 'command';
- notebook.model.cells.redo();
- notebook.deselectAll();
- Private.handleState(notebook, state);
- }
- /**
- * 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.
- */
- export function toggleAllLineNumbers(notebook: Notebook): void {
- 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;
- Private.handleState(notebook, state);
- }
- /**
- * Clear the code outputs of the selected cells.
- *
- * @param notebook - The target notebook widget.
- *
- * #### Notes
- * The widget `mode` will be preserved.
- */
- export function clearOutputs(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- each(notebook.model.cells, (cell: ICodeCellModel, index) => {
- const child = notebook.widgets[index];
- if (notebook.isSelectedOrActive(child) && cell.type === 'code') {
- cell.outputs.clear();
- (child as CodeCell).outputHidden = false;
- cell.executionCount = null;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Clear all the code outputs on the widget.
- *
- * @param notebook - The target notebook widget.
- *
- * #### Notes
- * The widget `mode` will be preserved.
- */
- export function clearAllOutputs(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- each(notebook.model.cells, (cell: ICodeCellModel, index) => {
- const child = notebook.widgets[index];
- if (cell.type === 'code') {
- cell.outputs.clear();
- cell.executionCount = null;
- (child as CodeCell).outputHidden = false;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Hide the code on selected code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function hideCode(notebook: Notebook): void {
- 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);
- }
- /**
- * Show the code on selected code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function showCode(notebook: Notebook): void {
- 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);
- }
- /**
- * Hide the code on all code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function hideAllCode(notebook: Notebook): void {
- 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);
- }
- /**
- * Show the code on all code cells.
- *
- * @param widget - The target notebook widget.
- */
- export function showAllCode(notebook: Notebook): void {
- 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);
- }
- /**
- * Hide the output on selected code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function hideOutput(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.widgets.forEach(cell => {
- if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
- (cell as CodeCell).outputHidden = true;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Show the output on selected code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function showOutput(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.widgets.forEach(cell => {
- if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
- (cell as CodeCell).outputHidden = false;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Hide the output on all code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function hideAllOutputs(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.widgets.forEach(cell => {
- if (cell.model.type === 'code') {
- (cell as CodeCell).outputHidden = true;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Show the output on all code cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function showAllOutputs(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.widgets.forEach(cell => {
- if (cell.model.type === 'code') {
- (cell as CodeCell).outputHidden = false;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Enable output scrolling for all selected cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function enableOutputScrolling(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.widgets.forEach(cell => {
- if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
- (cell as CodeCell).outputsScrolled = true;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * Disable output scrolling for all selected cells.
- *
- * @param notebook - The target notebook widget.
- */
- export function disableOutputScrolling(notebook: Notebook): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = Private.getState(notebook);
- notebook.widgets.forEach(cell => {
- if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
- (cell as CodeCell).outputsScrolled = false;
- }
- });
- Private.handleState(notebook, state);
- }
- /**
- * 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.
- */
- export function setMarkdownHeader(notebook: Notebook, level: number) {
- 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);
- }
- /**
- * Trust the notebook after prompting the user.
- *
- * @param notebook - The target notebook widget.
- *
- * @returns a promise that resolves when the transaction is finished.
- *
- * #### Notes
- * No dialog will be presented if the notebook is already trusted.
- */
- export function trust(notebook: Notebook): Promise<void> {
- if (!notebook.model) {
- return Promise.resolve();
- }
- // Do nothing if already trusted.
- const cells = toArray(notebook.model.cells);
- const trusted = cells.every(cell => cell.trusted);
- if (trusted) {
- return showDialog({
- body: 'Notebook is already trusted',
- buttons: [Dialog.okButton()]
- }).then(() => undefined);
- }
- return showDialog({
- body: TRUST_MESSAGE,
- title: 'Trust this notebook?',
- buttons: [Dialog.cancelButton(), Dialog.warnButton()]
- }).then(result => {
- if (result.button.accept) {
- cells.forEach(cell => {
- cell.trusted = true;
- });
- }
- });
- }
- }
- /**
- * A namespace for private data.
- */
- namespace Private {
- /**
- * A signal that emits whenever a cell is run.
- */
- export const executed = new Signal<any, { notebook: Notebook; cell: Cell }>(
- {}
- );
- /**
- * The interface for a widget state.
- */
- export interface IState {
- /**
- * Whether the widget had focus.
- */
- wasFocused: boolean;
- /**
- * The active cell before the action.
- */
- activeCell: Cell;
- }
- /**
- * Get the state of a widget before running an action.
- */
- export function getState(notebook: Notebook): IState {
- return {
- wasFocused: notebook.node.contains(document.activeElement),
- activeCell: notebook.activeCell
- };
- }
- /**
- * Handle the state of a widget after running an action.
- */
- export function handleState(
- notebook: Notebook,
- state: IState,
- scrollIfNeeded = false
- ): void {
- const { activeCell, node } = notebook;
- if (state.wasFocused || notebook.mode === 'edit') {
- notebook.activate();
- }
- if (scrollIfNeeded) {
- ElementExt.scrollIntoViewIfNeeded(node, activeCell.node);
- }
- }
- /**
- * Handle the state of a widget after running a run action.
- */
- export function handleRunState(
- notebook: Notebook,
- state: IState,
- scroll = false
- ): void {
- if (state.wasFocused || notebook.mode === 'edit') {
- notebook.activate();
- }
- if (scroll) {
- // Scroll to the top of the previous active cell output.
- const rect = state.activeCell.inputArea.node.getBoundingClientRect();
- notebook.scrollToPosition(rect.bottom, 45);
- }
- }
- /**
- * Clone a cell model.
- */
- export function cloneCell(
- model: INotebookModel,
- cell: ICellModel
- ): ICellModel {
- switch (cell.type) {
- case 'code':
- // TODO why isn't modeldb or id passed here?
- return model.contentFactory.createCodeCell({ cell: cell.toJSON() });
- case 'markdown':
- // TODO why isn't modeldb or id passed here?
- return model.contentFactory.createMarkdownCell({ cell: cell.toJSON() });
- default:
- // TODO why isn't modeldb or id passed here?
- return model.contentFactory.createRawCell({ cell: cell.toJSON() });
- }
- }
- /**
- * Run the selected cells.
- */
- export function runSelected(
- notebook: Notebook,
- session?: IClientSession
- ): Promise<boolean> {
- 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 Promise.all(selected.map(child => runCell(notebook, child, session)))
- .then(results => {
- if (notebook.isDisposed) {
- return false;
- }
- // Post an update request.
- notebook.update();
- return results.every(result => result);
- })
- .catch(reason => {
- if (reason.message === 'KernelReplyNotOK') {
- selected.map(cell => {
- // Remove '*' prompt from cells that didn't execute
- if (
- cell.model.type === 'code' &&
- (cell as CodeCell).model.executionCount == null
- ) {
- cell.setPrompt('');
- }
- });
- } else {
- throw reason;
- }
- notebook.update();
- return false;
- });
- }
- /**
- * Run a cell.
- */
- function runCell(
- notebook: Notebook,
- cell: Cell,
- session?: IClientSession
- ): Promise<boolean> {
- switch (cell.model.type) {
- case 'markdown':
- (cell as MarkdownCell).rendered = true;
- cell.inputHidden = false;
- executed.emit({ notebook, cell });
- break;
- case 'code':
- if (session) {
- return CodeCell.execute(cell as CodeCell, session, {
- deletedCells: notebook.model.deletedCells
- })
- .then(reply => {
- notebook.model.deletedCells.splice(
- 0,
- notebook.model.deletedCells.length
- );
- if (cell.isDisposed) {
- return false;
- }
- if (!reply) {
- return true;
- }
- if (reply.content.status === 'ok') {
- const content = reply.content;
- if (content.payload && content.payload.length) {
- handlePayload(content, notebook, cell);
- }
- return true;
- } else {
- throw new Error('KernelReplyNotOK');
- }
- })
- .catch(reason => {
- if (reason.message !== 'Canceled') {
- throw reason;
- }
- return false;
- })
- .then(ran => {
- if (ran) {
- executed.emit({ notebook, cell });
- }
- return ran;
- });
- }
- (cell.model as ICodeCellModel).executionCount = null;
- break;
- default:
- break;
- }
- return Promise.resolve(true);
- }
- /**
- * Handle payloads from an execute reply.
- *
- * #### Notes
- * Payloads are deprecated and there are no official interfaces for them in
- * the kernel type definitions.
- * See [Payloads (DEPRECATED)](https://jupyter-client.readthedocs.io/en/latest/messaging.html#payloads-deprecated).
- */
- function handlePayload(
- content: KernelMessage.IExecuteReply,
- notebook: Notebook,
- cell: Cell
- ) {
- const setNextInput = content.payload.filter(i => {
- return (i as any).source === 'set_next_input';
- })[0];
- if (!setNextInput) {
- return;
- }
- const text = (setNextInput as any).text;
- const replace = (setNextInput as any).replace;
- if (replace) {
- cell.model.value.text = text;
- return;
- }
- // Create a new code cell and add as the next cell.
- const newCell = notebook.model.contentFactory.createCodeCell({});
- const cells = notebook.model.cells;
- const index = ArrayExt.firstIndexOf(toArray(cells), cell.model);
- newCell.value.text = text;
- if (index === -1) {
- cells.push(newCell);
- } else {
- cells.insert(index + 1, newCell);
- }
- }
- /**
- * Copy or cut the selected cell data to the application clipboard.
- *
- * @param notebook - The target notebook widget.
- *
- * @param cut - Whether to copy or cut.
- */
- export function copyOrCut(notebook: Notebook, cut: boolean): void {
- if (!notebook.model || !notebook.activeCell) {
- return;
- }
- const state = getState(notebook);
- const clipboard = Clipboard.getInstance();
- notebook.mode = 'command';
- clipboard.clear();
- let data = notebook.widgets
- .filter(cell => notebook.isSelectedOrActive(cell))
- .map(cell => cell.model.toJSON())
- .map(cellJSON => {
- if ((cellJSON.metadata as JSONObject).deletable !== undefined) {
- delete (cellJSON.metadata as JSONObject).deletable;
- }
- return cellJSON;
- });
- clipboard.setData(JUPYTER_CELL_MIME, data);
- if (cut) {
- deleteCells(notebook);
- } else {
- notebook.deselectAll();
- }
- handleState(notebook, state);
- }
- /**
- * 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.
- */
- export function changeCellType(
- notebook: Notebook,
- value: nbformat.CellType
- ): void {
- const model = notebook.model;
- const cells = model.cells;
- cells.beginCompoundOperation();
- notebook.widgets.forEach((child, index) => {
- if (!notebook.isSelectedOrActive(child)) {
- return;
- }
- if (child.model.type !== value) {
- const cell = child.model.toJSON();
- let newCell: ICellModel;
- switch (value) {
- case 'code':
- newCell = model.contentFactory.createCodeCell({ cell });
- break;
- case 'markdown':
- newCell = model.contentFactory.createMarkdownCell({ cell });
- if (child.model.type === 'code') {
- newCell.trusted = false;
- }
- break;
- default:
- newCell = model.contentFactory.createRawCell({ cell });
- if (child.model.type === 'code') {
- newCell.trusted = false;
- }
- }
- cells.set(index, newCell);
- }
- if (value === 'markdown') {
- // Fetch the new widget and unrender it.
- child = notebook.widgets[index];
- (child as MarkdownCell).rendered = false;
- }
- });
- cells.endCompoundOperation();
- notebook.deselectAll();
- }
- /**
- * 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.
- */
- export function deleteCells(notebook: Notebook): void {
- const model = notebook.model;
- const cells = model.cells;
- const toDelete: number[] = [];
- notebook.mode = 'command';
- // Find the cells to delete.
- notebook.widgets.forEach((child, index) => {
- const deletable = child.model.metadata.get('deletable') !== false;
- if (notebook.isSelectedOrActive(child) && deletable) {
- toDelete.push(index);
- notebook.model.deletedCells.push(child.model.id);
- }
- });
- // If cells are not deletable, we may not have anything to delete.
- if (toDelete.length > 0) {
- // Delete the cells as one undo event.
- cells.beginCompoundOperation();
- // Delete cells in reverse order to maintain the correct indices.
- toDelete.reverse().forEach(index => {
- cells.remove(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 (!cells.length) {
- cells.push(
- model.contentFactory.createCell(
- notebook.notebookConfig.defaultCell,
- {}
- )
- );
- }
- cells.endCompoundOperation();
- // 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();
- }
- /**
- * Set the markdown header level of a cell.
- */
- export function setMarkdownHeader(cell: ICellModel, level: number) {
- // Remove existing header or leading white space.
- let source = cell.value.text;
- const regex = /^(#+\s*)|^(\s*)/;
- const newHeader = Array(level + 1).join('#') + ' ';
- const matches = regex.exec(source);
- if (matches) {
- source = source.slice(matches[0].length);
- }
- cell.value.text = newHeader + source;
- }
- }
|