UNPKG

89.9 kBTypeScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import {
5 Clipboard,
6 Dialog,
7 ISessionContext,
8 ISessionContextDialogs,
9 showDialog
10} from '@jupyterlab/apputils';
11import {
12 Cell,
13 CodeCell,
14 ICellModel,
15 ICodeCellModel,
16 isMarkdownCellModel,
17 isRawCellModel,
18 MarkdownCell
19} from '@jupyterlab/cells';
20import { Notification } from '@jupyterlab/apputils';
21import { signalToPromise } from '@jupyterlab/coreutils';
22import * as nbformat from '@jupyterlab/nbformat';
23import { KernelMessage } from '@jupyterlab/services';
24import { ISharedAttachmentsCell } from '@jupyter/ydoc';
25import { ITranslator, nullTranslator } from '@jupyterlab/translation';
26import { every, findIndex } from '@lumino/algorithm';
27import { JSONExt, JSONObject } from '@lumino/coreutils';
28import { ISignal, Signal } from '@lumino/signaling';
29import * as React from 'react';
30import { Notebook, StaticNotebook } from './widget';
31import { NotebookWindowedLayout } from './windowing';
32
33/**
34 * The mimetype used for Jupyter cell data.
35 */
36const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
37
38export class KernelError extends Error {
39 /**
40 * Exception name
41 */
42 readonly errorName: string;
43 /**
44 * Exception value
45 */
46 readonly errorValue: string;
47 /**
48 * Traceback
49 */
50 readonly traceback: string[];
51
52 /**
53 * Construct the kernel error.
54 */
55 constructor(content: KernelMessage.IExecuteReplyMsg['content']) {
56 const errorContent = content as KernelMessage.IReplyErrorContent;
57 const errorName = errorContent.ename;
58 const errorValue = errorContent.evalue;
59 super(`KernelReplyNotOK: ${errorName} ${errorValue}`);
60
61 this.errorName = errorName;
62 this.errorValue = errorValue;
63 this.traceback = errorContent.traceback;
64 Object.setPrototypeOf(this, KernelError.prototype);
65 }
66}
67
68/**
69 * A collection of actions that run against notebooks.
70 *
71 * #### Notes
72 * All of the actions are a no-op if there is no model on the notebook.
73 * The actions set the widget `mode` to `'command'` unless otherwise specified.
74 * The actions will preserve the selection on the notebook widget unless
75 * otherwise specified.
76 */
77export class NotebookActions {
78 /**
79 * A signal that emits whenever a cell completes execution.
80 */
81 static get executed(): ISignal<
82 any,
83 {
84 notebook: Notebook;
85 cell: Cell;
86 success: boolean;
87 error?: KernelError | null;
88 }
89 > {
90 return Private.executed;
91 }
92
93 /**
94 * A signal that emits whenever a cell execution is scheduled.
95 */
96 static get executionScheduled(): ISignal<
97 any,
98 { notebook: Notebook; cell: Cell }
99 > {
100 return Private.executionScheduled;
101 }
102
103 /**
104 * A signal that emits when one notebook's cells are all executed.
105 */
106 static get selectionExecuted(): ISignal<
107 any,
108 { notebook: Notebook; lastCell: Cell }
109 > {
110 return Private.selectionExecuted;
111 }
112
113 /**
114 * A signal that emits when a cell's output is cleared.
115 */
116 static get outputCleared(): ISignal<any, { notebook: Notebook; cell: Cell }> {
117 return Private.outputCleared;
118 }
119
120 /**
121 * A private constructor for the `NotebookActions` class.
122 *
123 * #### Notes
124 * This class can never be instantiated. Its static member `executed` will be
125 * merged with the `NotebookActions` namespace. The reason it exists as a
126 * standalone class is because at run time, the `Private.executed` variable
127 * does not yet exist, so it needs to be referenced via a getter.
128 */
129 private constructor() {
130 // Intentionally empty.
131 }
132}
133
134/**
135 * A namespace for `NotebookActions` static methods.
136 */
137export namespace NotebookActions {
138 /**
139 * Split the active cell into two or more cells.
140 *
141 * @param notebook The target notebook widget.
142 *
143 * #### Notes
144 * It will preserve the existing mode.
145 * The last cell will be activated if no selection is found.
146 * If text was selected, the cell containing the selection will
147 * be activated.
148 * The existing selection will be cleared.
149 * The activated cell will have focus and the cursor will
150 * remain in the initial position.
151 * The leading whitespace in the second cell will be removed.
152 * If there is no content, two empty cells will be created.
153 * Both cells will have the same type as the original cell.
154 * This action can be undone.
155 */
156 export function splitCell(notebook: Notebook): void {
157 if (!notebook.model || !notebook.activeCell) {
158 return;
159 }
160
161 const state = Private.getState(notebook);
162 // We force the notebook back in edit mode as splitting a cell
163 // requires using the cursor position within a cell (aka it was recently in edit mode)
164 // However the focus may be stolen if the action is triggered
165 // from the menu entry; switching the notebook in command mode.
166 notebook.mode = 'edit';
167
168 notebook.deselectAll();
169
170 const nbModel = notebook.model;
171 const index = notebook.activeCellIndex;
172 const child = notebook.widgets[index];
173 const editor = child.editor;
174 if (!editor) {
175 // TODO
176 return;
177 }
178 const selections = editor.getSelections();
179 const orig = child.model.sharedModel.getSource();
180
181 const offsets = [0];
182
183 let start: number = -1;
184 let end: number = -1;
185 for (let i = 0; i < selections.length; i++) {
186 // append start and end to handle selections
187 // cursors will have same start and end
188 start = editor.getOffsetAt(selections[i].start);
189 end = editor.getOffsetAt(selections[i].end);
190 if (start < end) {
191 offsets.push(start);
192 offsets.push(end);
193 } else if (end < start) {
194 offsets.push(end);
195 offsets.push(start);
196 } else {
197 offsets.push(start);
198 }
199 }
200
201 offsets.push(orig.length);
202
203 const cellCountAfterSplit = offsets.length - 1;
204 const clones = offsets.slice(0, -1).map((offset, offsetIdx) => {
205 const { cell_type, metadata, outputs } = child.model.sharedModel.toJSON();
206
207 return {
208 cell_type,
209 metadata,
210 source: orig
211 .slice(offset, offsets[offsetIdx + 1])
212 .replace(/^\n+/, '')
213 .replace(/\n+$/, ''),
214 outputs:
215 offsetIdx === cellCountAfterSplit - 1 && cell_type === 'code'
216 ? outputs
217 : undefined
218 };
219 });
220
221 nbModel.sharedModel.transact(() => {
222 nbModel.sharedModel.deleteCell(index);
223 nbModel.sharedModel.insertCells(index, clones);
224 });
225
226 // If there is a selection the selected cell will be activated
227 const activeCellDelta = start !== end ? 2 : 1;
228 notebook.activeCellIndex = index + clones.length - activeCellDelta;
229 notebook
230 .scrollToItem(notebook.activeCellIndex)
231 .then(() => {
232 notebook.activeCell?.editor!.focus();
233 })
234 .catch(reason => {
235 // no-op
236 });
237
238 void Private.handleState(notebook, state);
239 }
240
241 /**
242 * Merge the selected cells.
243 *
244 * @param notebook - The target notebook widget.
245 *
246 * @param mergeAbove - If only one cell is selected, indicates whether to merge it
247 * with the cell above (true) or below (false, default).
248 *
249 * #### Notes
250 * The widget mode will be preserved.
251 * If only one cell is selected and `mergeAbove` is true, the above cell will be selected.
252 * If only one cell is selected and `mergeAbove` is false, the below cell will be selected.
253 * If the active cell is a code cell, its outputs will be cleared.
254 * This action can be undone.
255 * The final cell will have the same type as the active cell.
256 * If the active cell is a markdown cell, it will be unrendered.
257 */
258 export function mergeCells(
259 notebook: Notebook,
260 mergeAbove: boolean = false
261 ): void {
262 if (!notebook.model || !notebook.activeCell) {
263 return;
264 }
265
266 const state = Private.getState(notebook);
267 const toMerge: string[] = [];
268 const toDelete: number[] = [];
269 const model = notebook.model;
270 const cells = model.cells;
271 const primary = notebook.activeCell;
272 const active = notebook.activeCellIndex;
273 const attachments: nbformat.IAttachments = {};
274
275 // Get the cells to merge.
276 notebook.widgets.forEach((child, index) => {
277 if (notebook.isSelectedOrActive(child)) {
278 toMerge.push(child.model.sharedModel.getSource());
279 if (index !== active) {
280 toDelete.push(index);
281 }
282 // Collect attachments if the cell is a markdown cell or a raw cell
283 const model = child.model;
284 if (isRawCellModel(model) || isMarkdownCellModel(model)) {
285 for (const key of model.attachments.keys) {
286 attachments[key] = model.attachments.get(key)!.toJSON();
287 }
288 }
289 }
290 });
291
292 // Check for only a single cell selected.
293 if (toMerge.length === 1) {
294 // Merge with the cell above when mergeAbove is true
295 if (mergeAbove === true) {
296 // Bail if it is the first cell.
297 if (active === 0) {
298 return;
299 }
300 // Otherwise merge with the previous cell.
301 const cellModel = cells.get(active - 1);
302
303 toMerge.unshift(cellModel.sharedModel.getSource());
304 toDelete.push(active - 1);
305 } else if (mergeAbove === false) {
306 // Bail if it is the last cell.
307 if (active === cells.length - 1) {
308 return;
309 }
310 // Otherwise merge with the next cell.
311 const cellModel = cells.get(active + 1);
312
313 toMerge.push(cellModel.sharedModel.getSource());
314 toDelete.push(active + 1);
315 }
316 }
317
318 notebook.deselectAll();
319
320 const primaryModel = primary.model.sharedModel;
321 const { cell_type, metadata } = primaryModel.toJSON();
322 if (primaryModel.cell_type === 'code') {
323 // We can trust this cell because the outputs will be removed.
324 metadata.trusted = true;
325 }
326 const newModel = {
327 cell_type,
328 metadata,
329 source: toMerge.join('\n\n'),
330 attachments:
331 primaryModel.cell_type === 'markdown' ||
332 primaryModel.cell_type === 'raw'
333 ? attachments
334 : undefined
335 };
336
337 // Make the changes while preserving history.
338 model.sharedModel.transact(() => {
339 model.sharedModel.deleteCell(active);
340 model.sharedModel.insertCell(active, newModel);
341 toDelete
342 .sort((a, b) => b - a)
343 .forEach(index => {
344 model.sharedModel.deleteCell(index);
345 });
346 });
347 // If the original cell is a markdown cell, make sure
348 // the new cell is unrendered.
349 if (primary instanceof MarkdownCell) {
350 (notebook.activeCell as MarkdownCell).rendered = false;
351 }
352
353 void Private.handleState(notebook, state);
354 }
355
356 /**
357 * Delete the selected cells.
358 *
359 * @param notebook - The target notebook widget.
360 *
361 * #### Notes
362 * The cell after the last selected cell will be activated.
363 * It will add a code cell if all cells are deleted.
364 * This action can be undone.
365 */
366 export function deleteCells(notebook: Notebook): void {
367 if (!notebook.model || !notebook.activeCell) {
368 return;
369 }
370
371 const state = Private.getState(notebook);
372
373 Private.deleteCells(notebook);
374 void Private.handleState(notebook, state, true);
375 }
376
377 /**
378 * Insert a new code cell above the active cell or in index 0 if the notebook is empty.
379 *
380 * @param notebook - The target notebook widget.
381 *
382 * #### Notes
383 * The widget mode will be preserved.
384 * This action can be undone.
385 * The existing selection will be cleared.
386 * The new cell will the active cell.
387 */
388 export function insertAbove(notebook: Notebook): void {
389 if (!notebook.model) {
390 return;
391 }
392
393 const state = Private.getState(notebook);
394 const model = notebook.model;
395
396 const newIndex = notebook.activeCell ? notebook.activeCellIndex : 0;
397 model.sharedModel.insertCell(newIndex, {
398 cell_type: notebook.notebookConfig.defaultCell,
399 metadata:
400 notebook.notebookConfig.defaultCell === 'code'
401 ? {
402 // This is an empty cell created by user, thus is trusted
403 trusted: true
404 }
405 : {}
406 });
407 // Make the newly inserted cell active.
408 notebook.activeCellIndex = newIndex;
409
410 notebook.deselectAll();
411 void Private.handleState(notebook, state, true);
412 }
413
414 /**
415 * Insert a new code cell below the active cell or in index 0 if the notebook is empty.
416 *
417 * @param notebook - The target notebook widget.
418 *
419 * #### Notes
420 * The widget mode will be preserved.
421 * This action can be undone.
422 * The existing selection will be cleared.
423 * The new cell will be the active cell.
424 */
425 export function insertBelow(notebook: Notebook): void {
426 if (!notebook.model) {
427 return;
428 }
429
430 const state = Private.getState(notebook);
431 const model = notebook.model;
432
433 const newIndex = notebook.activeCell ? notebook.activeCellIndex + 1 : 0;
434 model.sharedModel.insertCell(newIndex, {
435 cell_type: notebook.notebookConfig.defaultCell,
436 metadata:
437 notebook.notebookConfig.defaultCell === 'code'
438 ? {
439 // This is an empty cell created by user, thus is trusted
440 trusted: true
441 }
442 : {}
443 });
444 // Make the newly inserted cell active.
445 notebook.activeCellIndex = newIndex;
446
447 notebook.deselectAll();
448 void Private.handleState(notebook, state, true);
449 }
450
451 function move(notebook: Notebook, shift: number): void {
452 if (!notebook.model || !notebook.activeCell) {
453 return;
454 }
455
456 const state = Private.getState(notebook);
457
458 const firstIndex = notebook.widgets.findIndex(w =>
459 notebook.isSelectedOrActive(w)
460 );
461 let lastIndex = notebook.widgets
462 .slice(firstIndex + 1)
463 .findIndex(w => !notebook.isSelectedOrActive(w));
464
465 if (lastIndex >= 0) {
466 lastIndex += firstIndex + 1;
467 } else {
468 lastIndex = notebook.model.cells.length;
469 }
470
471 if (shift > 0) {
472 notebook.moveCell(firstIndex, lastIndex, lastIndex - firstIndex);
473 } else {
474 notebook.moveCell(firstIndex, firstIndex + shift, lastIndex - firstIndex);
475 }
476
477 void Private.handleState(notebook, state, true);
478 }
479
480 /**
481 * Move the selected cell(s) down.
482 *
483 * @param notebook = The target notebook widget.
484 */
485 export function moveDown(notebook: Notebook): void {
486 move(notebook, 1);
487 }
488
489 /**
490 * Move the selected cell(s) up.
491 *
492 * @param notebook - The target notebook widget.
493 */
494 export function moveUp(notebook: Notebook): void {
495 move(notebook, -1);
496 }
497
498 /**
499 * Change the selected cell type(s).
500 *
501 * @param notebook - The target notebook widget.
502 *
503 * @param value - The target cell type.
504 *
505 * #### Notes
506 * It should preserve the widget mode.
507 * This action can be undone.
508 * The existing selection will be cleared.
509 * Any cells converted to markdown will be unrendered.
510 */
511 export function changeCellType(
512 notebook: Notebook,
513 value: nbformat.CellType
514 ): void {
515 if (!notebook.model || !notebook.activeCell) {
516 return;
517 }
518
519 const state = Private.getState(notebook);
520
521 Private.changeCellType(notebook, value);
522 void Private.handleState(notebook, state);
523 }
524
525 /**
526 * Run the selected cell(s).
527 *
528 * @param notebook - The target notebook widget.
529 * @param sessionContext - The client session object.
530 * @param sessionDialogs - The session dialogs.
531 * @param translator - The application translator.
532 *
533 * #### Notes
534 * The last selected cell will be activated, but not scrolled into view.
535 * The existing selection will be cleared.
536 * An execution error will prevent the remaining code cells from executing.
537 * All markdown cells will be rendered.
538 */
539 export function run(
540 notebook: Notebook,
541 sessionContext?: ISessionContext,
542 sessionDialogs?: ISessionContextDialogs,
543 translator?: ITranslator
544 ): Promise<boolean> {
545 if (!notebook.model || !notebook.activeCell) {
546 return Promise.resolve(false);
547 }
548
549 const state = Private.getState(notebook);
550 const promise = Private.runSelected(
551 notebook,
552 sessionContext,
553 sessionDialogs,
554 translator
555 );
556
557 void Private.handleRunState(notebook, state);
558 return promise;
559 }
560
561 /**
562 * Run specified cells.
563 *
564 * @param notebook - The target notebook widget.
565 * @param cells - The cells to run.
566 * @param sessionContext - The client session object.
567 * @param sessionDialogs - The session dialogs.
568 * @param translator - The application translator.
569 *
570 * #### Notes
571 * The existing selection will be preserved.
572 * The mode will be changed to command.
573 * An execution error will prevent the remaining code cells from executing.
574 * All markdown cells will be rendered.
575 */
576 export function runCells(
577 notebook: Notebook,
578 cells: readonly Cell[],
579 sessionContext?: ISessionContext,
580 sessionDialogs?: ISessionContextDialogs,
581 translator?: ITranslator
582 ): Promise<boolean> {
583 if (!notebook.model) {
584 return Promise.resolve(false);
585 }
586
587 const state = Private.getState(notebook);
588 const promise = Private.runCells(
589 notebook,
590 cells,
591 sessionContext,
592 sessionDialogs,
593 translator
594 );
595
596 void Private.handleRunState(notebook, state);
597 return promise;
598 }
599
600 /**
601 * Run the selected cell(s) and advance to the next cell.
602 *
603 * @param notebook - The target notebook widget.
604 * @param sessionContext - The client session object.
605 * @param sessionDialogs - The session dialogs.
606 * @param translator - The application translator.
607 *
608 * #### Notes
609 * The existing selection will be cleared.
610 * The cell after the last selected cell will be activated and scrolled into view.
611 * An execution error will prevent the remaining code cells from executing.
612 * All markdown cells will be rendered.
613 * If the last selected cell is the last cell, a new code cell
614 * will be created in `'edit'` mode. The new cell creation can be undone.
615 */
616 export async function runAndAdvance(
617 notebook: Notebook,
618 sessionContext?: ISessionContext,
619 sessionDialogs?: ISessionContextDialogs,
620 translator?: ITranslator
621 ): Promise<boolean> {
622 if (!notebook.model || !notebook.activeCell) {
623 return Promise.resolve(false);
624 }
625
626 const state = Private.getState(notebook);
627 const promise = Private.runSelected(
628 notebook,
629 sessionContext,
630 sessionDialogs,
631 translator
632 );
633 const model = notebook.model;
634
635 if (notebook.activeCellIndex === notebook.widgets.length - 1) {
636 // Do not use push here, as we want an widget insertion
637 // to make sure no placeholder widget is rendered.
638 model.sharedModel.insertCell(notebook.widgets.length, {
639 cell_type: notebook.notebookConfig.defaultCell,
640 metadata:
641 notebook.notebookConfig.defaultCell === 'code'
642 ? {
643 // This is an empty cell created by user, thus is trusted
644 trusted: true
645 }
646 : {}
647 });
648 notebook.activeCellIndex++;
649 if (notebook.activeCell?.inViewport === false) {
650 await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(
651 () => {
652 // no-op
653 }
654 );
655 }
656 notebook.mode = 'edit';
657 } else {
658 notebook.activeCellIndex++;
659 }
660
661 // If a cell is outside of viewport and scrolling is needed, the `smart`
662 // logic in `handleRunState` will choose appropriate alignment, except
663 // for the case of a small cell less than one viewport away for which it
664 // would use the `auto` heuristic, for which we set the preferred alignment
665 // to `center` as in most cases there will be space below and above a cell
666 // that is smaller than (or approximately equal to) the viewport size.
667 void Private.handleRunState(notebook, state, 'center');
668 return promise;
669 }
670
671 /**
672 * Run the selected cell(s) and insert a new code cell.
673 *
674 * @param notebook - The target notebook widget.
675 * @param sessionContext - The client session object.
676 * @param sessionDialogs - The session dialogs.
677 * @param translator - The application translator.
678 *
679 * #### Notes
680 * An execution error will prevent the remaining code cells from executing.
681 * All markdown cells will be rendered.
682 * The widget mode will be set to `'edit'` after running.
683 * The existing selection will be cleared.
684 * The cell insert can be undone.
685 * The new cell will be scrolled into view.
686 */
687 export async function runAndInsert(
688 notebook: Notebook,
689 sessionContext?: ISessionContext,
690 sessionDialogs?: ISessionContextDialogs,
691 translator?: ITranslator
692 ): Promise<boolean> {
693 if (!notebook.model || !notebook.activeCell) {
694 return Promise.resolve(false);
695 }
696
697 const state = Private.getState(notebook);
698 const promise = Private.runSelected(
699 notebook,
700 sessionContext,
701 sessionDialogs,
702 translator
703 );
704 const model = notebook.model;
705 model.sharedModel.insertCell(notebook.activeCellIndex + 1, {
706 cell_type: notebook.notebookConfig.defaultCell,
707 metadata:
708 notebook.notebookConfig.defaultCell === 'code'
709 ? {
710 // This is an empty cell created by user, thus is trusted
711 trusted: true
712 }
713 : {}
714 });
715 notebook.activeCellIndex++;
716 if (notebook.activeCell?.inViewport === false) {
717 await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(
718 () => {
719 // no-op
720 }
721 );
722 }
723 notebook.mode = 'edit';
724 void Private.handleRunState(notebook, state, 'center');
725 return promise;
726 }
727
728 /**
729 * Run all of the cells in the notebook.
730 *
731 * @param notebook - The target notebook widget.
732 * @param sessionContext - The client session object.
733 * @param sessionDialogs - The session dialogs.
734 * @param translator - The application translator.
735 *
736 * #### Notes
737 * The existing selection will be cleared.
738 * An execution error will prevent the remaining code cells from executing.
739 * All markdown cells will be rendered.
740 * The last cell in the notebook will be activated and scrolled into view.
741 */
742 export function runAll(
743 notebook: Notebook,
744 sessionContext?: ISessionContext,
745 sessionDialogs?: ISessionContextDialogs,
746 translator?: ITranslator
747 ): Promise<boolean> {
748 if (!notebook.model || !notebook.activeCell) {
749 return Promise.resolve(false);
750 }
751
752 const state = Private.getState(notebook);
753 const lastIndex = notebook.widgets.length;
754
755 const promise = Private.runCells(
756 notebook,
757 notebook.widgets,
758 sessionContext,
759 sessionDialogs,
760 translator
761 );
762
763 notebook.activeCellIndex = lastIndex;
764 notebook.deselectAll();
765
766 void Private.handleRunState(notebook, state);
767 return promise;
768 }
769
770 export function renderAllMarkdown(notebook: Notebook): Promise<boolean> {
771 if (!notebook.model || !notebook.activeCell) {
772 return Promise.resolve(false);
773 }
774 const previousIndex = notebook.activeCellIndex;
775 const state = Private.getState(notebook);
776 notebook.widgets.forEach((child, index) => {
777 if (child.model.type === 'markdown') {
778 notebook.select(child);
779 // This is to make sure that the activeCell
780 // does not get executed
781 notebook.activeCellIndex = index;
782 }
783 });
784 if (notebook.activeCell.model.type !== 'markdown') {
785 return Promise.resolve(true);
786 }
787 const promise = Private.runSelected(notebook);
788 notebook.activeCellIndex = previousIndex;
789 void Private.handleRunState(notebook, state);
790 return promise;
791 }
792
793 /**
794 * Run all of the cells before the currently active cell (exclusive).
795 *
796 * @param notebook - The target notebook widget.
797 * @param sessionContext - The client session object.
798 * @param sessionDialogs - The session dialogs.
799 * @param translator - The application translator.
800 *
801 * #### Notes
802 * The existing selection will be cleared.
803 * An execution error will prevent the remaining code cells from executing.
804 * All markdown cells will be rendered.
805 * The currently active cell will remain selected.
806 */
807 export function runAllAbove(
808 notebook: Notebook,
809 sessionContext?: ISessionContext,
810 sessionDialogs?: ISessionContextDialogs,
811 translator?: ITranslator
812 ): Promise<boolean> {
813 const { activeCell, activeCellIndex, model } = notebook;
814
815 if (!model || !activeCell || activeCellIndex < 1) {
816 return Promise.resolve(false);
817 }
818
819 const state = Private.getState(notebook);
820
821 const promise = Private.runCells(
822 notebook,
823 notebook.widgets.slice(0, notebook.activeCellIndex),
824 sessionContext,
825 sessionDialogs,
826 translator
827 );
828
829 notebook.deselectAll();
830
831 void Private.handleRunState(notebook, state);
832 return promise;
833 }
834
835 /**
836 * Run all of the cells after the currently active cell (inclusive).
837 *
838 * @param notebook - The target notebook widget.
839 * @param sessionContext - The client session object.
840 * @param sessionDialogs - The session dialogs.
841 * @param translator - The application translator.
842 *
843 * #### Notes
844 * The existing selection will be cleared.
845 * An execution error will prevent the remaining code cells from executing.
846 * All markdown cells will be rendered.
847 * The last cell in the notebook will be activated and scrolled into view.
848 */
849 export function runAllBelow(
850 notebook: Notebook,
851 sessionContext?: ISessionContext,
852 sessionDialogs?: ISessionContextDialogs,
853 translator?: ITranslator
854 ): Promise<boolean> {
855 if (!notebook.model || !notebook.activeCell) {
856 return Promise.resolve(false);
857 }
858
859 const state = Private.getState(notebook);
860 const lastIndex = notebook.widgets.length;
861
862 const promise = Private.runCells(
863 notebook,
864 notebook.widgets.slice(notebook.activeCellIndex),
865 sessionContext,
866 sessionDialogs,
867 translator
868 );
869
870 notebook.activeCellIndex = lastIndex;
871 notebook.deselectAll();
872
873 void Private.handleRunState(notebook, state);
874 return promise;
875 }
876
877 /**
878 * Replaces the selection in the active cell of the notebook.
879 *
880 * @param notebook - The target notebook widget.
881 * @param text - The text to replace the selection.
882 */
883 export function replaceSelection(notebook: Notebook, text: string): void {
884 if (!notebook.model || !notebook.activeCell?.editor) {
885 return;
886 }
887 notebook.activeCell.editor.replaceSelection?.(text);
888 }
889
890 /**
891 * Select the above the active cell.
892 *
893 * @param notebook - The target notebook widget.
894 *
895 * #### Notes
896 * The widget mode will be preserved.
897 * This is a no-op if the first cell is the active cell.
898 * This will skip any collapsed cells.
899 * The existing selection will be cleared.
900 */
901 export function selectAbove(notebook: Notebook): void {
902 if (!notebook.model || !notebook.activeCell) {
903 return;
904 }
905 const footer = (notebook.layout as NotebookWindowedLayout).footer;
906 if (footer && document.activeElement === footer.node) {
907 footer.node.blur();
908 notebook.mode = 'command';
909 return;
910 }
911
912 if (notebook.activeCellIndex === 0) {
913 return;
914 }
915
916 let possibleNextCellIndex = notebook.activeCellIndex - 1;
917
918 // find first non hidden cell above current cell
919 while (possibleNextCellIndex >= 0) {
920 const possibleNextCell = notebook.widgets[possibleNextCellIndex];
921 if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) {
922 break;
923 }
924 possibleNextCellIndex -= 1;
925 }
926
927 const state = Private.getState(notebook);
928 notebook.activeCellIndex = possibleNextCellIndex;
929 notebook.deselectAll();
930 void Private.handleState(notebook, state, true);
931 }
932
933 /**
934 * Select the cell below the active cell.
935 *
936 * @param notebook - The target notebook widget.
937 *
938 * #### Notes
939 * The widget mode will be preserved.
940 * This is a no-op if the last cell is the active cell.
941 * This will skip any collapsed cells.
942 * The existing selection will be cleared.
943 */
944 export function selectBelow(notebook: Notebook): void {
945 if (!notebook.model || !notebook.activeCell) {
946 return;
947 }
948 let maxCellIndex = notebook.widgets.length - 1;
949
950 // Find last non-hidden cell
951 while (
952 notebook.widgets[maxCellIndex].isHidden ||
953 notebook.widgets[maxCellIndex].inputHidden
954 ) {
955 maxCellIndex -= 1;
956 }
957
958 if (notebook.activeCellIndex === maxCellIndex) {
959 const footer = (notebook.layout as NotebookWindowedLayout).footer;
960 footer?.node.focus();
961 return;
962 }
963
964 let possibleNextCellIndex = notebook.activeCellIndex + 1;
965
966 // find first non hidden cell below current cell
967 while (possibleNextCellIndex < maxCellIndex) {
968 let possibleNextCell = notebook.widgets[possibleNextCellIndex];
969 if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) {
970 break;
971 }
972 possibleNextCellIndex += 1;
973 }
974
975 const state = Private.getState(notebook);
976 notebook.activeCellIndex = possibleNextCellIndex;
977 notebook.deselectAll();
978 void Private.handleState(notebook, state, true);
979 }
980
981 /** Insert new heading of same level above active cell.
982 *
983 * @param notebook - The target notebook widget
984 */
985 export async function insertSameLevelHeadingAbove(
986 notebook: Notebook
987 ): Promise<void> {
988 if (!notebook.model || !notebook.activeCell) {
989 return;
990 }
991 let headingLevel = Private.Headings.determineHeadingLevel(
992 notebook.activeCell,
993 notebook
994 );
995 if (headingLevel == -1) {
996 await Private.Headings.insertHeadingAboveCellIndex(0, 1, notebook);
997 } else {
998 await Private.Headings.insertHeadingAboveCellIndex(
999 notebook.activeCellIndex!,
1000 headingLevel,
1001 notebook
1002 );
1003 }
1004 }
1005
1006 /** Insert new heading of same level at end of current section.
1007 *
1008 * @param notebook - The target notebook widget
1009 */
1010 export async function insertSameLevelHeadingBelow(
1011 notebook: Notebook
1012 ): Promise<void> {
1013 if (!notebook.model || !notebook.activeCell) {
1014 return;
1015 }
1016 let headingLevel = Private.Headings.determineHeadingLevel(
1017 notebook.activeCell,
1018 notebook
1019 );
1020 headingLevel = headingLevel > -1 ? headingLevel : 1;
1021 let cellIdxOfHeadingBelow =
1022 Private.Headings.findLowerEqualLevelHeadingBelow(
1023 notebook.activeCell,
1024 notebook,
1025 true
1026 ) as number;
1027 await Private.Headings.insertHeadingAboveCellIndex(
1028 cellIdxOfHeadingBelow == -1
1029 ? notebook.model.cells.length
1030 : cellIdxOfHeadingBelow,
1031 headingLevel,
1032 notebook
1033 );
1034 }
1035
1036 /**
1037 * Select the heading above the active cell or, if already at heading, collapse it.
1038 *
1039 * @param notebook - The target notebook widget.
1040 *
1041 * #### Notes
1042 * The widget mode will be preserved.
1043 * This is a no-op if the active cell is the topmost heading in collapsed state
1044 * The existing selection will be cleared.
1045 */
1046 export function selectHeadingAboveOrCollapseHeading(
1047 notebook: Notebook
1048 ): void {
1049 if (!notebook.model || !notebook.activeCell) {
1050 return;
1051 }
1052 const state = Private.getState(notebook);
1053 let hInfoActiveCell = getHeadingInfo(notebook.activeCell);
1054 // either collapse or find the right heading to jump to
1055 if (hInfoActiveCell.isHeading && !hInfoActiveCell.collapsed) {
1056 setHeadingCollapse(notebook.activeCell, true, notebook);
1057 } else {
1058 let targetHeadingCellIdx =
1059 Private.Headings.findLowerEqualLevelParentHeadingAbove(
1060 notebook.activeCell,
1061 notebook,
1062 true
1063 ) as number;
1064 if (targetHeadingCellIdx > -1) {
1065 notebook.activeCellIndex = targetHeadingCellIdx;
1066 }
1067 }
1068 // clear selection and handle state
1069 notebook.deselectAll();
1070 void Private.handleState(notebook, state, true);
1071 }
1072
1073 /**
1074 * Select the heading below the active cell or, if already at heading, expand it.
1075 *
1076 * @param notebook - The target notebook widget.
1077 *
1078 * #### Notes
1079 * The widget mode will be preserved.
1080 * This is a no-op if the active cell is the last heading in expanded state
1081 * The existing selection will be cleared.
1082 */
1083 export function selectHeadingBelowOrExpandHeading(notebook: Notebook): void {
1084 if (!notebook.model || !notebook.activeCell) {
1085 return;
1086 }
1087 const state = Private.getState(notebook);
1088 let hInfo = getHeadingInfo(notebook.activeCell);
1089 if (hInfo.isHeading && hInfo.collapsed) {
1090 setHeadingCollapse(notebook.activeCell, false, notebook);
1091 } else {
1092 let targetHeadingCellIdx = Private.Headings.findHeadingBelow(
1093 notebook.activeCell,
1094 notebook,
1095 true // return index of heading cell
1096 ) as number;
1097 if (targetHeadingCellIdx > -1) {
1098 notebook.activeCellIndex = targetHeadingCellIdx;
1099 }
1100 }
1101 notebook.deselectAll();
1102 void Private.handleState(notebook, state, true);
1103 }
1104
1105 /**
1106 * Extend the selection to the cell above.
1107 *
1108 * @param notebook - The target notebook widget.
1109 * @param toTop - If true, denotes selection to extend to the top.
1110 *
1111 * #### Notes
1112 * This is a no-op if the first cell is the active cell.
1113 * The new cell will be activated.
1114 */
1115 export function extendSelectionAbove(
1116 notebook: Notebook,
1117 toTop: boolean = false
1118 ): void {
1119 if (!notebook.model || !notebook.activeCell) {
1120 return;
1121 }
1122 // Do not wrap around.
1123 if (notebook.activeCellIndex === 0) {
1124 return;
1125 }
1126
1127 const state = Private.getState(notebook);
1128
1129 notebook.mode = 'command';
1130 // Check if toTop is true, if yes, selection is made to the top.
1131 if (toTop) {
1132 notebook.extendContiguousSelectionTo(0);
1133 } else {
1134 notebook.extendContiguousSelectionTo(notebook.activeCellIndex - 1);
1135 }
1136 void Private.handleState(notebook, state, true);
1137 }
1138
1139 /**
1140 * Extend the selection to the cell below.
1141 *
1142 * @param notebook - The target notebook widget.
1143 * @param toBottom - If true, denotes selection to extend to the bottom.
1144 *
1145 * #### Notes
1146 * This is a no-op if the last cell is the active cell.
1147 * The new cell will be activated.
1148 */
1149 export function extendSelectionBelow(
1150 notebook: Notebook,
1151 toBottom: boolean = false
1152 ): void {
1153 if (!notebook.model || !notebook.activeCell) {
1154 return;
1155 }
1156 // Do not wrap around.
1157 if (notebook.activeCellIndex === notebook.widgets.length - 1) {
1158 return;
1159 }
1160
1161 const state = Private.getState(notebook);
1162
1163 notebook.mode = 'command';
1164 // Check if toBottom is true, if yes selection is made to the bottom.
1165 if (toBottom) {
1166 notebook.extendContiguousSelectionTo(notebook.widgets.length - 1);
1167 } else {
1168 notebook.extendContiguousSelectionTo(notebook.activeCellIndex + 1);
1169 }
1170 void Private.handleState(notebook, state, true);
1171 }
1172
1173 /**
1174 * Select all of the cells of the notebook.
1175 *
1176 * @param notebook - the target notebook widget.
1177 */
1178 export function selectAll(notebook: Notebook): void {
1179 if (!notebook.model || !notebook.activeCell) {
1180 return;
1181 }
1182 notebook.widgets.forEach(child => {
1183 notebook.select(child);
1184 });
1185 }
1186
1187 /**
1188 * Deselect all of the cells of the notebook.
1189 *
1190 * @param notebook - the target notebook widget.
1191 */
1192 export function deselectAll(notebook: Notebook): void {
1193 if (!notebook.model || !notebook.activeCell) {
1194 return;
1195 }
1196 notebook.deselectAll();
1197 }
1198
1199 /**
1200 * Copy the selected cell(s) data to a clipboard.
1201 *
1202 * @param notebook - The target notebook widget.
1203 */
1204 export function copy(notebook: Notebook): void {
1205 Private.copyOrCut(notebook, false);
1206 }
1207
1208 /**
1209 * Cut the selected cell data to a clipboard.
1210 *
1211 * @param notebook - The target notebook widget.
1212 *
1213 * #### Notes
1214 * This action can be undone.
1215 * A new code cell is added if all cells are cut.
1216 */
1217 export function cut(notebook: Notebook): void {
1218 Private.copyOrCut(notebook, true);
1219 }
1220
1221 /**
1222 * Paste cells from the application clipboard.
1223 *
1224 * @param notebook - The target notebook widget.
1225 *
1226 * @param mode - the mode of adding cells:
1227 * 'below' (default) adds cells below the active cell,
1228 * 'belowSelected' adds cells below all selected cells,
1229 * 'above' adds cells above the active cell, and
1230 * 'replace' removes the currently selected cells and adds cells in their place.
1231 *
1232 * #### Notes
1233 * The last pasted cell becomes the active cell.
1234 * This is a no-op if there is no cell data on the clipboard.
1235 * This action can be undone.
1236 */
1237 export function paste(
1238 notebook: Notebook,
1239 mode: 'below' | 'belowSelected' | 'above' | 'replace' = 'below'
1240 ): void {
1241 const clipboard = Clipboard.getInstance();
1242
1243 if (!clipboard.hasData(JUPYTER_CELL_MIME)) {
1244 return;
1245 }
1246
1247 const values = clipboard.getData(JUPYTER_CELL_MIME) as nbformat.IBaseCell[];
1248
1249 addCells(notebook, mode, values, true);
1250 void focusActiveCell(notebook);
1251 }
1252
1253 /**
1254 * Duplicate selected cells in the notebook without using the application clipboard.
1255 *
1256 * @param notebook - The target notebook widget.
1257 *
1258 * @param mode - the mode of adding cells:
1259 * 'below' (default) adds cells below the active cell,
1260 * 'belowSelected' adds cells below all selected cells,
1261 * 'above' adds cells above the active cell, and
1262 * 'replace' removes the currently selected cells and adds cells in their place.
1263 *
1264 * #### Notes
1265 * The last pasted cell becomes the active cell.
1266 * This is a no-op if there is no cell data on the clipboard.
1267 * This action can be undone.
1268 */
1269 export function duplicate(
1270 notebook: Notebook,
1271 mode: 'below' | 'belowSelected' | 'above' | 'replace' = 'below'
1272 ): void {
1273 const values = Private.selectedCells(notebook);
1274
1275 if (!values || values.length === 0) {
1276 return;
1277 }
1278
1279 addCells(notebook, mode, values, false); // Cells not from the clipboard
1280 }
1281
1282 /**
1283 * Adds cells to the notebook.
1284 *
1285 * @param notebook - The target notebook widget.
1286 *
1287 * @param mode - the mode of adding cells:
1288 * 'below' (default) adds cells below the active cell,
1289 * 'belowSelected' adds cells below all selected cells,
1290 * 'above' adds cells above the active cell, and
1291 * 'replace' removes the currently selected cells and adds cells in their place.
1292 *
1293 * @param values — The cells to add to the notebook.
1294 *
1295 * @param cellsFromClipboard — True if the cells were sourced from the clipboard.
1296 *
1297 * #### Notes
1298 * The last added cell becomes the active cell.
1299 * This is a no-op if values is an empty array.
1300 * This action can be undone.
1301 */
1302
1303 function addCells(
1304 notebook: Notebook,
1305 mode: 'below' | 'belowSelected' | 'above' | 'replace' = 'below',
1306 values: nbformat.IBaseCell[],
1307 cellsFromClipboard: boolean = false
1308 ): void {
1309 if (!notebook.model || !notebook.activeCell) {
1310 return;
1311 }
1312
1313 const state = Private.getState(notebook);
1314 const model = notebook.model;
1315
1316 notebook.mode = 'command';
1317
1318 let index = 0;
1319 const prevActiveCellIndex = notebook.activeCellIndex;
1320
1321 model.sharedModel.transact(() => {
1322 // Set the starting index of the paste operation depending upon the mode.
1323 switch (mode) {
1324 case 'below':
1325 index = notebook.activeCellIndex + 1;
1326 break;
1327 case 'belowSelected':
1328 notebook.widgets.forEach((child, childIndex) => {
1329 if (notebook.isSelectedOrActive(child)) {
1330 index = childIndex + 1;
1331 }
1332 });
1333
1334 break;
1335 case 'above':
1336 index = notebook.activeCellIndex;
1337 break;
1338 case 'replace': {
1339 // Find the cells to delete.
1340 const toDelete: number[] = [];
1341
1342 notebook.widgets.forEach((child, index) => {
1343 const deletable =
1344 (child.model.sharedModel.getMetadata(
1345 'deletable'
1346 ) as unknown as boolean) !== false;
1347
1348 if (notebook.isSelectedOrActive(child) && deletable) {
1349 toDelete.push(index);
1350 }
1351 });
1352
1353 // If cells are not deletable, we may not have anything to delete.
1354 if (toDelete.length > 0) {
1355 // Delete the cells as one undo event.
1356 toDelete.reverse().forEach(i => {
1357 model.sharedModel.deleteCell(i);
1358 });
1359 }
1360 index = toDelete[0];
1361 break;
1362 }
1363 default:
1364 break;
1365 }
1366
1367 model.sharedModel.insertCells(
1368 index,
1369 values.map(cell => {
1370 cell.id =
1371 cell.cell_type === 'code' &&
1372 notebook.lastClipboardInteraction === 'cut' &&
1373 typeof cell.id === 'string'
1374 ? cell.id
1375 : undefined;
1376 return cell;
1377 })
1378 );
1379 });
1380
1381 notebook.activeCellIndex = prevActiveCellIndex + values.length;
1382 notebook.deselectAll();
1383 if (cellsFromClipboard) {
1384 notebook.lastClipboardInteraction = 'paste';
1385 }
1386 void Private.handleState(notebook, state, true);
1387 }
1388
1389 /**
1390 * Undo a cell action.
1391 *
1392 * @param notebook - The target notebook widget.
1393 *
1394 * #### Notes
1395 * This is a no-op if there are no cell actions to undo.
1396 */
1397 export function undo(notebook: Notebook): void {
1398 if (!notebook.model) {
1399 return;
1400 }
1401
1402 const state = Private.getState(notebook);
1403
1404 notebook.mode = 'command';
1405 notebook.model.sharedModel.undo();
1406 notebook.deselectAll();
1407 void Private.handleState(notebook, state);
1408 }
1409
1410 /**
1411 * Redo a cell action.
1412 *
1413 * @param notebook - The target notebook widget.
1414 *
1415 * #### Notes
1416 * This is a no-op if there are no cell actions to redo.
1417 */
1418 export function redo(notebook: Notebook): void {
1419 if (!notebook.model || !notebook.activeCell) {
1420 return;
1421 }
1422
1423 const state = Private.getState(notebook);
1424
1425 notebook.mode = 'command';
1426 notebook.model.sharedModel.redo();
1427 notebook.deselectAll();
1428 void Private.handleState(notebook, state);
1429 }
1430
1431 /**
1432 * Toggle the line number of all cells.
1433 *
1434 * @param notebook - The target notebook widget.
1435 *
1436 * #### Notes
1437 * The original state is based on the state of the active cell.
1438 * The `mode` of the widget will be preserved.
1439 */
1440 export function toggleAllLineNumbers(notebook: Notebook): void {
1441 if (!notebook.model || !notebook.activeCell) {
1442 return;
1443 }
1444
1445 const state = Private.getState(notebook);
1446 const config = notebook.editorConfig;
1447 const lineNumbers = !(
1448 config.code.lineNumbers &&
1449 config.markdown.lineNumbers &&
1450 config.raw.lineNumbers
1451 );
1452 const newConfig = {
1453 code: { ...config.code, lineNumbers },
1454 markdown: { ...config.markdown, lineNumbers },
1455 raw: { ...config.raw, lineNumbers }
1456 };
1457
1458 notebook.editorConfig = newConfig;
1459 void Private.handleState(notebook, state);
1460 }
1461
1462 /**
1463 * Clear the code outputs of the selected cells.
1464 *
1465 * @param notebook - The target notebook widget.
1466 *
1467 * #### Notes
1468 * The widget `mode` will be preserved.
1469 */
1470 export function clearOutputs(notebook: Notebook): void {
1471 if (!notebook.model || !notebook.activeCell) {
1472 return;
1473 }
1474
1475 const state = Private.getState(notebook);
1476 let index = -1;
1477 for (const cell of notebook.model.cells) {
1478 const child = notebook.widgets[++index];
1479
1480 if (notebook.isSelectedOrActive(child) && cell.type === 'code') {
1481 cell.sharedModel.transact(() => {
1482 (cell as ICodeCellModel).clearExecution();
1483 (child as CodeCell).outputHidden = false;
1484 }, false);
1485 Private.outputCleared.emit({ notebook, cell: child });
1486 }
1487 }
1488 void Private.handleState(notebook, state, true);
1489 }
1490
1491 /**
1492 * Clear all the code outputs on the widget.
1493 *
1494 * @param notebook - The target notebook widget.
1495 *
1496 * #### Notes
1497 * The widget `mode` will be preserved.
1498 */
1499 export function clearAllOutputs(notebook: Notebook): void {
1500 if (!notebook.model || !notebook.activeCell) {
1501 return;
1502 }
1503
1504 const state = Private.getState(notebook);
1505 let index = -1;
1506 for (const cell of notebook.model.cells) {
1507 const child = notebook.widgets[++index];
1508
1509 if (cell.type === 'code') {
1510 cell.sharedModel.transact(() => {
1511 (cell as ICodeCellModel).clearExecution();
1512 (child as CodeCell).outputHidden = false;
1513 }, false);
1514 Private.outputCleared.emit({ notebook, cell: child });
1515 }
1516 }
1517 void Private.handleState(notebook, state, true);
1518 }
1519
1520 /**
1521 * Hide the code on selected code cells.
1522 *
1523 * @param notebook - The target notebook widget.
1524 */
1525 export function hideCode(notebook: Notebook): void {
1526 if (!notebook.model || !notebook.activeCell) {
1527 return;
1528 }
1529
1530 const state = Private.getState(notebook);
1531
1532 notebook.widgets.forEach(cell => {
1533 if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
1534 cell.inputHidden = true;
1535 }
1536 });
1537 void Private.handleState(notebook, state);
1538 }
1539
1540 /**
1541 * Show the code on selected code cells.
1542 *
1543 * @param notebook - The target notebook widget.
1544 */
1545 export function showCode(notebook: Notebook): void {
1546 if (!notebook.model || !notebook.activeCell) {
1547 return;
1548 }
1549
1550 const state = Private.getState(notebook);
1551
1552 notebook.widgets.forEach(cell => {
1553 if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
1554 cell.inputHidden = false;
1555 }
1556 });
1557 void Private.handleState(notebook, state);
1558 }
1559
1560 /**
1561 * Hide the code on all code cells.
1562 *
1563 * @param notebook - The target notebook widget.
1564 */
1565 export function hideAllCode(notebook: Notebook): void {
1566 if (!notebook.model || !notebook.activeCell) {
1567 return;
1568 }
1569
1570 const state = Private.getState(notebook);
1571
1572 notebook.widgets.forEach(cell => {
1573 if (cell.model.type === 'code') {
1574 cell.inputHidden = true;
1575 }
1576 });
1577 void Private.handleState(notebook, state);
1578 }
1579
1580 /**
1581 * Show the code on all code cells.
1582 *
1583 * @param widget - The target notebook widget.
1584 */
1585 export function showAllCode(notebook: Notebook): void {
1586 if (!notebook.model || !notebook.activeCell) {
1587 return;
1588 }
1589
1590 const state = Private.getState(notebook);
1591
1592 notebook.widgets.forEach(cell => {
1593 if (cell.model.type === 'code') {
1594 cell.inputHidden = false;
1595 }
1596 });
1597 void Private.handleState(notebook, state);
1598 }
1599
1600 /**
1601 * Hide the output on selected code cells.
1602 *
1603 * @param notebook - The target notebook widget.
1604 */
1605 export function hideOutput(notebook: Notebook): void {
1606 if (!notebook.model || !notebook.activeCell) {
1607 return;
1608 }
1609
1610 const state = Private.getState(notebook);
1611
1612 notebook.widgets.forEach(cell => {
1613 if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
1614 (cell as CodeCell).outputHidden = true;
1615 }
1616 });
1617 void Private.handleState(notebook, state, true);
1618 }
1619
1620 /**
1621 * Show the output on selected code cells.
1622 *
1623 * @param notebook - The target notebook widget.
1624 */
1625 export function showOutput(notebook: Notebook): void {
1626 if (!notebook.model || !notebook.activeCell) {
1627 return;
1628 }
1629
1630 const state = Private.getState(notebook);
1631
1632 notebook.widgets.forEach(cell => {
1633 if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
1634 (cell as CodeCell).outputHidden = false;
1635 }
1636 });
1637 void Private.handleState(notebook, state);
1638 }
1639
1640 /**
1641 * Hide the output on all code cells.
1642 *
1643 * @param notebook - The target notebook widget.
1644 */
1645 export function hideAllOutputs(notebook: Notebook): void {
1646 if (!notebook.model || !notebook.activeCell) {
1647 return;
1648 }
1649
1650 const state = Private.getState(notebook);
1651
1652 notebook.widgets.forEach(cell => {
1653 if (cell.model.type === 'code') {
1654 (cell as CodeCell).outputHidden = true;
1655 }
1656 });
1657 void Private.handleState(notebook, state, true);
1658 }
1659
1660 /**
1661 * Render side-by-side.
1662 *
1663 * @param notebook - The target notebook widget.
1664 */
1665 export function renderSideBySide(notebook: Notebook): void {
1666 notebook.renderingLayout = 'side-by-side';
1667 }
1668
1669 /**
1670 * Render not side-by-side.
1671 *
1672 * @param notebook - The target notebook widget.
1673 */
1674 export function renderDefault(notebook: Notebook): void {
1675 notebook.renderingLayout = 'default';
1676 }
1677
1678 /**
1679 * Show the output on all code cells.
1680 *
1681 * @param notebook - The target notebook widget.
1682 */
1683 export function showAllOutputs(notebook: Notebook): void {
1684 if (!notebook.model || !notebook.activeCell) {
1685 return;
1686 }
1687
1688 const state = Private.getState(notebook);
1689
1690 notebook.widgets.forEach(cell => {
1691 if (cell.model.type === 'code') {
1692 (cell as CodeCell).outputHidden = false;
1693 }
1694 });
1695 void Private.handleState(notebook, state);
1696 }
1697
1698 /**
1699 * Enable output scrolling for all selected cells.
1700 *
1701 * @param notebook - The target notebook widget.
1702 */
1703 export function enableOutputScrolling(notebook: Notebook): void {
1704 if (!notebook.model || !notebook.activeCell) {
1705 return;
1706 }
1707
1708 const state = Private.getState(notebook);
1709
1710 notebook.widgets.forEach(cell => {
1711 if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
1712 (cell as CodeCell).outputsScrolled = true;
1713 }
1714 });
1715 void Private.handleState(notebook, state, true);
1716 }
1717
1718 /**
1719 * Disable output scrolling for all selected cells.
1720 *
1721 * @param notebook - The target notebook widget.
1722 */
1723 export function disableOutputScrolling(notebook: Notebook): void {
1724 if (!notebook.model || !notebook.activeCell) {
1725 return;
1726 }
1727
1728 const state = Private.getState(notebook);
1729
1730 notebook.widgets.forEach(cell => {
1731 if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
1732 (cell as CodeCell).outputsScrolled = false;
1733 }
1734 });
1735 void Private.handleState(notebook, state);
1736 }
1737
1738 /**
1739 * Go to the last cell that is run or current if it is running.
1740 *
1741 * Note: This requires execution timing to be toggled on or this will have
1742 * no effect.
1743 *
1744 * @param notebook - The target notebook widget.
1745 */
1746 export function selectLastRunCell(notebook: Notebook): void {
1747 let latestTime: Date | null = null;
1748 let latestCellIdx: number | null = null;
1749 notebook.widgets.forEach((cell, cellIndx) => {
1750 if (cell.model.type === 'code') {
1751 const execution = cell.model.getMetadata('execution');
1752 if (
1753 execution &&
1754 JSONExt.isObject(execution) &&
1755 execution['iopub.status.busy'] !== undefined
1756 ) {
1757 // The busy status is used as soon as a request is received:
1758 // https://jupyter-client.readthedocs.io/en/stable/messaging.html
1759 const timestamp = execution['iopub.status.busy']!.toString();
1760 if (timestamp) {
1761 const startTime = new Date(timestamp);
1762 if (!latestTime || startTime >= latestTime) {
1763 latestTime = startTime;
1764 latestCellIdx = cellIndx;
1765 }
1766 }
1767 }
1768 }
1769 });
1770 if (latestCellIdx !== null) {
1771 notebook.activeCellIndex = latestCellIdx;
1772 }
1773 }
1774
1775 /**
1776 * Set the markdown header level.
1777 *
1778 * @param notebook - The target notebook widget.
1779 *
1780 * @param level - The header level.
1781 *
1782 * #### Notes
1783 * All selected cells will be switched to markdown.
1784 * The level will be clamped between 1 and 6.
1785 * If there is an existing header, it will be replaced.
1786 * There will always be one blank space after the header.
1787 * The cells will be unrendered.
1788 */
1789 export function setMarkdownHeader(notebook: Notebook, level: number): void {
1790 if (!notebook.model || !notebook.activeCell) {
1791 return;
1792 }
1793
1794 const state = Private.getState(notebook);
1795 const cells = notebook.model.cells;
1796
1797 level = Math.min(Math.max(level, 1), 6);
1798 notebook.widgets.forEach((child, index) => {
1799 if (notebook.isSelectedOrActive(child)) {
1800 Private.setMarkdownHeader(cells.get(index), level);
1801 }
1802 });
1803 Private.changeCellType(notebook, 'markdown');
1804 void Private.handleState(notebook, state);
1805 }
1806
1807 /**
1808 * Collapse all cells in given notebook.
1809 *
1810 * @param notebook - The target notebook widget.
1811 */
1812 export function collapseAllHeadings(notebook: Notebook): any {
1813 const state = Private.getState(notebook);
1814 for (const cell of notebook.widgets) {
1815 if (NotebookActions.getHeadingInfo(cell).isHeading) {
1816 NotebookActions.setHeadingCollapse(cell, true, notebook);
1817 NotebookActions.setCellCollapse(cell, true);
1818 }
1819 }
1820 notebook.activeCellIndex = 0;
1821 void Private.handleState(notebook, state, true);
1822 }
1823
1824 /**
1825 * Un-collapse all cells in given notebook.
1826 *
1827 * @param notebook - The target notebook widget.
1828 */
1829 export function expandAllHeadings(notebook: Notebook): any {
1830 for (const cell of notebook.widgets) {
1831 if (NotebookActions.getHeadingInfo(cell).isHeading) {
1832 NotebookActions.setHeadingCollapse(cell, false, notebook);
1833 // similar to collapseAll.
1834 NotebookActions.setCellCollapse(cell, false);
1835 }
1836 }
1837 }
1838
1839 function findNearestParentHeader(
1840 cell: Cell,
1841 notebook: Notebook
1842 ): Cell | undefined {
1843 const index = findIndex(
1844 notebook.widgets,
1845 (possibleCell: Cell, index: number) => {
1846 return cell.model.id === possibleCell.model.id;
1847 }
1848 );
1849 if (index === -1) {
1850 return;
1851 }
1852 // Finds the nearest header above the given cell. If the cell is a header itself, it does not return itself;
1853 // this can be checked directly by calling functions.
1854 if (index >= notebook.widgets.length) {
1855 return;
1856 }
1857 let childHeaderInfo = getHeadingInfo(notebook.widgets[index]);
1858 for (let cellN = index - 1; cellN >= 0; cellN--) {
1859 if (cellN < notebook.widgets.length) {
1860 let hInfo = getHeadingInfo(notebook.widgets[cellN]);
1861 if (
1862 hInfo.isHeading &&
1863 hInfo.headingLevel < childHeaderInfo.headingLevel
1864 ) {
1865 return notebook.widgets[cellN];
1866 }
1867 }
1868 }
1869 // else no parent header found.
1870 return;
1871 }
1872
1873 /**
1874 * Finds the "parent" heading of the given cell and expands.
1875 * Used for the case that a cell becomes active that is within a collapsed heading.
1876 * @param cell - "Child" cell that has become the active cell
1877 * @param notebook - The target notebook widget.
1878 */
1879 export function expandParent(cell: Cell, notebook: Notebook): void {
1880 let nearestParentCell = findNearestParentHeader(cell, notebook);
1881 if (!nearestParentCell) {
1882 return;
1883 }
1884 if (
1885 !getHeadingInfo(nearestParentCell).collapsed &&
1886 !nearestParentCell.isHidden
1887 ) {
1888 return;
1889 }
1890 if (nearestParentCell.isHidden) {
1891 expandParent(nearestParentCell, notebook);
1892 }
1893 if (getHeadingInfo(nearestParentCell).collapsed) {
1894 setHeadingCollapse(nearestParentCell, false, notebook);
1895 }
1896 }
1897
1898 /**
1899 * Finds the next heading that isn't a child of the given markdown heading.
1900 * @param cell - "Child" cell that has become the active cell
1901 * @param notebook - The target notebook widget.
1902 */
1903 export function findNextParentHeading(
1904 cell: Cell,
1905 notebook: Notebook
1906 ): number {
1907 let index = findIndex(
1908 notebook.widgets,
1909 (possibleCell: Cell, index: number) => {
1910 return cell.model.id === possibleCell.model.id;
1911 }
1912 );
1913 if (index === -1) {
1914 return -1;
1915 }
1916 let childHeaderInfo = getHeadingInfo(cell);
1917 for (index = index + 1; index < notebook.widgets.length; index++) {
1918 let hInfo = getHeadingInfo(notebook.widgets[index]);
1919 if (
1920 hInfo.isHeading &&
1921 hInfo.headingLevel <= childHeaderInfo.headingLevel
1922 ) {
1923 return index;
1924 }
1925 }
1926 // else no parent header found. return the index of the last cell
1927 return notebook.widgets.length;
1928 }
1929
1930 /**
1931 * Set the given cell and ** all "child" cells **
1932 * to the given collapse / expand if cell is
1933 * a markdown header.
1934 *
1935 * @param cell - The cell
1936 * @param collapsing - Whether to collapse or expand the cell
1937 * @param notebook - The target notebook widget.
1938 */
1939 export function setHeadingCollapse(
1940 cell: Cell,
1941 collapsing: boolean,
1942 notebook: StaticNotebook
1943 ): number {
1944 const which = findIndex(
1945 notebook.widgets,
1946 (possibleCell: Cell, index: number) => {
1947 return cell.model.id === possibleCell.model.id;
1948 }
1949 );
1950 if (which === -1) {
1951 return -1;
1952 }
1953 if (!notebook.widgets.length) {
1954 return which + 1;
1955 }
1956 let selectedHeadingInfo = NotebookActions.getHeadingInfo(cell);
1957 if (
1958 cell.isHidden ||
1959 !(cell instanceof MarkdownCell) ||
1960 !selectedHeadingInfo.isHeading
1961 ) {
1962 // otherwise collapsing and uncollapsing already hidden stuff can
1963 // cause some funny looking bugs.
1964 return which + 1;
1965 }
1966 let localCollapsed = false;
1967 let localCollapsedLevel = 0;
1968 // iterate through all cells after the active cell.
1969 let cellNum;
1970 for (cellNum = which + 1; cellNum < notebook.widgets.length; cellNum++) {
1971 let subCell = notebook.widgets[cellNum];
1972 let subCellHeadingInfo = NotebookActions.getHeadingInfo(subCell);
1973 if (
1974 subCellHeadingInfo.isHeading &&
1975 subCellHeadingInfo.headingLevel <= selectedHeadingInfo.headingLevel
1976 ) {
1977 // then reached an equivalent or higher heading level than the
1978 // original the end of the collapse.
1979 cellNum -= 1;
1980 break;
1981 }
1982 if (
1983 localCollapsed &&
1984 subCellHeadingInfo.isHeading &&
1985 subCellHeadingInfo.headingLevel <= localCollapsedLevel
1986 ) {
1987 // then reached the end of the local collapsed, so unset NotebookActions.
1988 localCollapsed = false;
1989 }
1990
1991 if (collapsing || localCollapsed) {
1992 // then no extra handling is needed for further locally collapsed
1993 // headings.
1994 subCell.setHidden(true);
1995 continue;
1996 }
1997
1998 if (subCellHeadingInfo.collapsed && subCellHeadingInfo.isHeading) {
1999 localCollapsed = true;
2000 localCollapsedLevel = subCellHeadingInfo.headingLevel;
2001 // but don't collapse the locally collapsed heading, so continue to
2002 // expand the heading. This will get noticed in the next round.
2003 }
2004 subCell.setHidden(false);
2005 }
2006 if (cellNum === notebook.widgets.length) {
2007 cell.numberChildNodes = cellNum - which - 1;
2008 } else {
2009 cell.numberChildNodes = cellNum - which;
2010 }
2011 NotebookActions.setCellCollapse(cell, collapsing);
2012 return cellNum + 1;
2013 }
2014
2015 /**
2016 * Toggles the collapse state of the active cell of the given notebook
2017 * and ** all of its "child" cells ** if the cell is a heading.
2018 *
2019 * @param notebook - The target notebook widget.
2020 */
2021 export function toggleCurrentHeadingCollapse(notebook: Notebook): any {
2022 if (!notebook.activeCell || notebook.activeCellIndex === undefined) {
2023 return;
2024 }
2025 let headingInfo = NotebookActions.getHeadingInfo(notebook.activeCell);
2026 if (headingInfo.isHeading) {
2027 // Then toggle!
2028 NotebookActions.setHeadingCollapse(
2029 notebook.activeCell,
2030 !headingInfo.collapsed,
2031 notebook
2032 );
2033 }
2034 notebook.scrollToItem(notebook.activeCellIndex).catch(reason => {
2035 // no-op
2036 });
2037 }
2038
2039 /**
2040 * If cell is a markdown heading, sets the headingCollapsed field,
2041 * and otherwise hides the cell.
2042 *
2043 * @param cell - The cell to collapse / expand
2044 * @param collapsing - Whether to collapse or expand the given cell
2045 */
2046 export function setCellCollapse(cell: Cell, collapsing: boolean): any {
2047 if (cell instanceof MarkdownCell) {
2048 cell.headingCollapsed = collapsing;
2049 } else {
2050 cell.setHidden(collapsing);
2051 }
2052 }
2053
2054 /**
2055 * If given cell is a markdown heading, returns the heading level.
2056 * If given cell is not markdown, returns 7 (there are only 6 levels of markdown headings)
2057 *
2058 * @param cell - The target cell widget.
2059 */
2060 export function getHeadingInfo(cell: Cell): {
2061 isHeading: boolean;
2062 headingLevel: number;
2063 collapsed?: boolean;
2064 } {
2065 if (!(cell instanceof MarkdownCell)) {
2066 return { isHeading: false, headingLevel: 7 };
2067 }
2068 let level = cell.headingInfo.level;
2069 let collapsed = cell.headingCollapsed;
2070 return { isHeading: level > 0, headingLevel: level, collapsed: collapsed };
2071 }
2072
2073 /**
2074 * Trust the notebook after prompting the user.
2075 *
2076 * @param notebook - The target notebook widget.
2077 *
2078 * @returns a promise that resolves when the transaction is finished.
2079 *
2080 * #### Notes
2081 * No dialog will be presented if the notebook is already trusted.
2082 */
2083 export function trust(
2084 notebook: Notebook,
2085 translator?: ITranslator
2086 ): Promise<void> {
2087 translator = translator || nullTranslator;
2088 const trans = translator.load('jupyterlab');
2089
2090 if (!notebook.model) {
2091 return Promise.resolve();
2092 }
2093 // Do nothing if already trusted.
2094
2095 const trusted = every(notebook.model.cells, cell => cell.trusted);
2096 // FIXME
2097 const trustMessage = (
2098 <p>
2099 {trans.__(
2100 'A trusted Jupyter notebook may execute hidden malicious code when you open it.'
2101 )}
2102 <br />
2103 {trans.__(
2104 'Selecting "Trust" will re-render this notebook in a trusted state.'
2105 )}
2106 <br />
2107 {trans.__('For more information, see')}{' '}
2108 <a
2109 href="https://jupyter-server.readthedocs.io/en/stable/operators/security.html"
2110 target="_blank"
2111 rel="noopener noreferrer"
2112 >
2113 {trans.__('the Jupyter security documentation')}
2114 </a>
2115 .
2116 </p>
2117 );
2118
2119 if (trusted) {
2120 return showDialog({
2121 body: trans.__('Notebook is already trusted'),
2122 buttons: [Dialog.okButton()]
2123 }).then(() => undefined);
2124 }
2125
2126 return showDialog({
2127 body: trustMessage,
2128 title: trans.__('Trust this notebook?'),
2129 buttons: [
2130 Dialog.cancelButton(),
2131 Dialog.warnButton({
2132 label: trans.__('Trust'),
2133 ariaLabel: trans.__('Confirm Trusting this notebook')
2134 })
2135 ] // FIXME?
2136 }).then(result => {
2137 if (result.button.accept) {
2138 if (notebook.model) {
2139 for (const cell of notebook.model.cells) {
2140 cell.trusted = true;
2141 }
2142 }
2143 }
2144 });
2145 }
2146
2147 /**
2148 * If the notebook has an active cell, focus it.
2149 *
2150 * @param notebook The target notebook widget
2151 * @param options Optional options to change the behavior of this function
2152 * @param options.waitUntilReady If true, do not call focus until activeCell.ready is resolved
2153 * @param options.preventScroll If true, do not scroll the active cell into view
2154 *
2155 * @returns a promise that resolves when focus has been called on the active
2156 * cell's node.
2157 *
2158 * #### Notes
2159 * By default, waits until after the active cell has been attached unless
2160 * called with { waitUntilReady: false }
2161 */
2162 export async function focusActiveCell(
2163 notebook: Notebook,
2164 options: {
2165 waitUntilReady?: boolean;
2166 preventScroll?: boolean;
2167 } = { waitUntilReady: true, preventScroll: false }
2168 ): Promise<void> {
2169 const { activeCell } = notebook;
2170 const { waitUntilReady, preventScroll } = options;
2171 if (!activeCell) {
2172 return;
2173 }
2174 if (waitUntilReady) {
2175 await activeCell.ready;
2176 }
2177 if (notebook.isDisposed || activeCell.isDisposed) {
2178 return;
2179 }
2180 activeCell.node.focus({
2181 preventScroll
2182 });
2183 }
2184
2185 /*
2186 * Access last notebook history.
2187 *
2188 * @param notebook - The target notebook widget.
2189 */
2190 export async function accessPreviousHistory(
2191 notebook: Notebook
2192 ): Promise<void> {
2193 if (!notebook.notebookConfig.accessKernelHistory) {
2194 return;
2195 }
2196 const activeCell = notebook.activeCell;
2197 if (activeCell) {
2198 if (notebook.kernelHistory) {
2199 const previousHistory = await notebook.kernelHistory.back(activeCell);
2200 notebook.kernelHistory.updateEditor(activeCell, previousHistory);
2201 }
2202 }
2203 }
2204
2205 /**
2206 * Access next notebook history.
2207 *
2208 * @param notebook - The target notebook widget.
2209 */
2210 export async function accessNextHistory(notebook: Notebook): Promise<void> {
2211 if (!notebook.notebookConfig.accessKernelHistory) {
2212 return;
2213 }
2214 const activeCell = notebook.activeCell;
2215 if (activeCell) {
2216 if (notebook.kernelHistory) {
2217 const nextHistory = await notebook.kernelHistory.forward(activeCell);
2218 notebook.kernelHistory.updateEditor(activeCell, nextHistory);
2219 }
2220 }
2221 }
2222}
2223
2224/**
2225 * A namespace for private data.
2226 */
2227namespace Private {
2228 /**
2229 * A signal that emits whenever a cell completes execution.
2230 */
2231 export const executed = new Signal<
2232 any,
2233 {
2234 notebook: Notebook;
2235 cell: Cell;
2236 success: boolean;
2237 error?: KernelError | null;
2238 }
2239 >({});
2240
2241 /**
2242 * A signal that emits whenever a cell execution is scheduled.
2243 */
2244 export const executionScheduled = new Signal<
2245 any,
2246 { notebook: Notebook; cell: Cell }
2247 >({});
2248
2249 /**
2250 * A signal that emits when one notebook's cells are all executed.
2251 */
2252 export const selectionExecuted = new Signal<
2253 any,
2254 { notebook: Notebook; lastCell: Cell }
2255 >({});
2256
2257 /**
2258 * A signal that emits when one notebook's cells are all executed.
2259 */
2260 export const outputCleared = new Signal<
2261 any,
2262 { notebook: Notebook; cell: Cell }
2263 >({});
2264
2265 /**
2266 * The interface for a widget state.
2267 */
2268 export interface IState {
2269 /**
2270 * Whether the widget had focus.
2271 */
2272 wasFocused: boolean;
2273
2274 /**
2275 * The active cell id before the action.
2276 *
2277 * We cannot rely on the Cell widget or model as it may be
2278 * discarded by action such as move.
2279 */
2280 activeCellId: string | null;
2281 }
2282
2283 /**
2284 * Get the state of a widget before running an action.
2285 */
2286 export function getState(notebook: Notebook): IState {
2287 return {
2288 wasFocused: notebook.node.contains(document.activeElement),
2289 activeCellId: notebook.activeCell?.model.id ?? null
2290 };
2291 }
2292
2293 /**
2294 * Handle the state of a widget after running an action.
2295 */
2296 export async function handleState(
2297 notebook: Notebook,
2298 state: IState,
2299 scrollIfNeeded = false
2300 ): Promise<void> {
2301 const { activeCell, activeCellIndex } = notebook;
2302 if (scrollIfNeeded && activeCell) {
2303 await notebook.scrollToItem(activeCellIndex, 'auto', 0).catch(reason => {
2304 // no-op
2305 });
2306 }
2307 if (state.wasFocused || notebook.mode === 'edit') {
2308 notebook.activate();
2309 }
2310 }
2311
2312 /**
2313 * Handle the state of a widget after running a run action.
2314 */
2315 export async function handleRunState(
2316 notebook: Notebook,
2317 state: IState,
2318 alignPreference?: 'start' | 'end' | 'center' | 'top-center'
2319 ): Promise<void> {
2320 const { activeCell, activeCellIndex } = notebook;
2321
2322 if (activeCell) {
2323 await notebook
2324 .scrollToItem(activeCellIndex, 'smart', 0, alignPreference)
2325 .catch(reason => {
2326 // no-op
2327 });
2328 }
2329 if (state.wasFocused || notebook.mode === 'edit') {
2330 notebook.activate();
2331 }
2332 }
2333
2334 /**
2335 * Run the selected cells.
2336 *
2337 * @param notebook Notebook
2338 * @param cells Cells to run
2339 * @param sessionContext Notebook session context
2340 * @param sessionDialogs Session dialogs
2341 * @param translator Application translator
2342 */
2343 export function runCells(
2344 notebook: Notebook,
2345 cells: readonly Cell[],
2346 sessionContext?: ISessionContext,
2347 sessionDialogs?: ISessionContextDialogs,
2348 translator?: ITranslator
2349 ): Promise<boolean> {
2350 const lastCell = cells[-1];
2351 notebook.mode = 'command';
2352
2353 let initializingDialogShown = false;
2354 return Promise.all(
2355 cells.map(cell => {
2356 if (
2357 cell.model.type === 'code' &&
2358 notebook.notebookConfig.enableKernelInitNotification &&
2359 sessionContext &&
2360 sessionContext.kernelDisplayStatus === 'initializing' &&
2361 !initializingDialogShown
2362 ) {
2363 initializingDialogShown = true;
2364 translator = translator || nullTranslator;
2365 const trans = translator.load('jupyterlab');
2366 Notification.emit(
2367 trans.__(
2368 `Kernel '${sessionContext.kernelDisplayName}' for '${sessionContext.path}' is still initializing. You can run code cells when the kernel has initialized.`
2369 ),
2370 'warning',
2371 {
2372 autoClose: false
2373 }
2374 );
2375 return Promise.resolve(false);
2376 }
2377 if (
2378 cell.model.type === 'code' &&
2379 notebook.notebookConfig.enableKernelInitNotification &&
2380 initializingDialogShown
2381 ) {
2382 return Promise.resolve(false);
2383 }
2384 return runCell(
2385 notebook,
2386 cell,
2387 sessionContext,
2388 sessionDialogs,
2389 translator
2390 );
2391 })
2392 )
2393 .then(results => {
2394 if (notebook.isDisposed) {
2395 return false;
2396 }
2397 selectionExecuted.emit({
2398 notebook,
2399 lastCell
2400 });
2401 // Post an update request.
2402 notebook.update();
2403
2404 return results.every(result => result);
2405 })
2406 .catch(reason => {
2407 if (reason.message.startsWith('KernelReplyNotOK')) {
2408 cells.map(cell => {
2409 // Remove '*' prompt from cells that didn't execute
2410 if (
2411 cell.model.type === 'code' &&
2412 (cell as CodeCell).model.executionCount == null
2413 ) {
2414 cell.setPrompt('');
2415 }
2416 });
2417 } else {
2418 throw reason;
2419 }
2420
2421 selectionExecuted.emit({
2422 notebook,
2423 lastCell
2424 });
2425
2426 notebook.update();
2427
2428 return false;
2429 });
2430 }
2431
2432 /**
2433 * Run the selected cells.
2434 *
2435 * @param notebook Notebook
2436 * @param sessionContext Notebook session context
2437 * @param sessionDialogs Session dialogs
2438 * @param translator Application translator
2439 */
2440 export function runSelected(
2441 notebook: Notebook,
2442 sessionContext?: ISessionContext,
2443 sessionDialogs?: ISessionContextDialogs,
2444 translator?: ITranslator
2445 ): Promise<boolean> {
2446 notebook.mode = 'command';
2447
2448 let lastIndex = notebook.activeCellIndex;
2449 const selected = notebook.widgets.filter((child, index) => {
2450 const active = notebook.isSelectedOrActive(child);
2451
2452 if (active) {
2453 lastIndex = index;
2454 }
2455
2456 return active;
2457 });
2458
2459 notebook.activeCellIndex = lastIndex;
2460 notebook.deselectAll();
2461
2462 return runCells(
2463 notebook,
2464 selected,
2465 sessionContext,
2466 sessionDialogs,
2467 translator
2468 );
2469 }
2470
2471 /**
2472 * Run a cell.
2473 */
2474 async function runCell(
2475 notebook: Notebook,
2476 cell: Cell,
2477 sessionContext?: ISessionContext,
2478 sessionDialogs?: ISessionContextDialogs,
2479 translator?: ITranslator
2480 ): Promise<boolean> {
2481 translator = translator || nullTranslator;
2482 const trans = translator.load('jupyterlab');
2483 switch (cell.model.type) {
2484 case 'markdown':
2485 (cell as MarkdownCell).rendered = true;
2486 cell.inputHidden = false;
2487 executed.emit({ notebook, cell, success: true });
2488 break;
2489 case 'code':
2490 if (sessionContext) {
2491 if (sessionContext.isTerminating) {
2492 await showDialog({
2493 title: trans.__('Kernel Terminating'),
2494 body: trans.__(
2495 'The kernel for %1 appears to be terminating. You can not run any cell for now.',
2496 sessionContext.session?.path
2497 ),
2498 buttons: [Dialog.okButton()]
2499 });
2500 break;
2501 }
2502 if (sessionContext.pendingInput) {
2503 await showDialog({
2504 title: trans.__('Cell not executed due to pending input'),
2505 body: trans.__(
2506 'The cell has not been executed to avoid kernel deadlock as there is another pending input! Submit your pending input and try again.'
2507 ),
2508 buttons: [Dialog.okButton()]
2509 });
2510 return false;
2511 }
2512 if (sessionContext.hasNoKernel) {
2513 const shouldSelect = await sessionContext.startKernel();
2514 if (shouldSelect && sessionDialogs) {
2515 await sessionDialogs.selectKernel(sessionContext);
2516 }
2517 }
2518
2519 if (sessionContext.hasNoKernel) {
2520 cell.model.sharedModel.transact(() => {
2521 (cell.model as ICodeCellModel).clearExecution();
2522 });
2523 return true;
2524 }
2525
2526 const deletedCells = notebook.model?.deletedCells ?? [];
2527 executionScheduled.emit({ notebook, cell });
2528
2529 let ran = false;
2530 try {
2531 const reply = await CodeCell.execute(
2532 cell as CodeCell,
2533 sessionContext,
2534 {
2535 deletedCells,
2536 recordTiming: notebook.notebookConfig.recordTiming
2537 }
2538 );
2539 deletedCells.splice(0, deletedCells.length);
2540
2541 ran = (() => {
2542 if (cell.isDisposed) {
2543 return false;
2544 }
2545
2546 if (!reply) {
2547 return true;
2548 }
2549 if (reply.content.status === 'ok') {
2550 const content = reply.content;
2551
2552 if (content.payload && content.payload.length) {
2553 handlePayload(content, notebook, cell);
2554 }
2555
2556 return true;
2557 } else {
2558 throw new KernelError(reply.content);
2559 }
2560 })();
2561 } catch (reason) {
2562 if (cell.isDisposed || reason.message.startsWith('Canceled')) {
2563 ran = false;
2564 } else {
2565 executed.emit({
2566 notebook,
2567 cell,
2568 success: false,
2569 error: reason
2570 });
2571 throw reason;
2572 }
2573 }
2574
2575 if (ran) {
2576 executed.emit({ notebook, cell, success: true });
2577 }
2578
2579 return ran;
2580 }
2581 cell.model.sharedModel.transact(() => {
2582 (cell.model as ICodeCellModel).clearExecution();
2583 }, false);
2584 break;
2585 default:
2586 break;
2587 }
2588
2589 return Promise.resolve(true);
2590 }
2591
2592 /**
2593 * Handle payloads from an execute reply.
2594 *
2595 * #### Notes
2596 * Payloads are deprecated and there are no official interfaces for them in
2597 * the kernel type definitions.
2598 * See [Payloads (DEPRECATED)](https://jupyter-client.readthedocs.io/en/latest/messaging.html#payloads-deprecated).
2599 */
2600 function handlePayload(
2601 content: KernelMessage.IExecuteReply,
2602 notebook: Notebook,
2603 cell: Cell
2604 ) {
2605 const setNextInput = content.payload?.filter(i => {
2606 return (i as any).source === 'set_next_input';
2607 })[0];
2608
2609 if (!setNextInput) {
2610 return;
2611 }
2612
2613 const text = setNextInput.text as string;
2614 const replace = setNextInput.replace;
2615
2616 if (replace) {
2617 cell.model.sharedModel.setSource(text);
2618 return;
2619 }
2620
2621 // Create a new code cell and add as the next cell.
2622 const notebookModel = notebook.model!.sharedModel;
2623 const cells = notebook.model!.cells;
2624 const index = findIndex(cells, model => model === cell.model);
2625
2626 // While this cell has no outputs and could be trusted following the letter
2627 // of Jupyter trust model, its content comes from kernel and hence is not
2628 // necessarily controlled by the user; if we set it as trusted, a user
2629 // executing cells in succession could end up with unwanted trusted output.
2630 if (index === -1) {
2631 notebookModel.insertCell(notebookModel.cells.length, {
2632 cell_type: 'code',
2633 source: text,
2634 metadata: {
2635 trusted: false
2636 }
2637 });
2638 } else {
2639 notebookModel.insertCell(index + 1, {
2640 cell_type: 'code',
2641 source: text,
2642 metadata: {
2643 trusted: false
2644 }
2645 });
2646 }
2647 }
2648
2649 /**
2650 * Get the selected cell(s) without affecting the clipboard.
2651 *
2652 * @param notebook - The target notebook widget.
2653 *
2654 * @returns A list of 0 or more selected cells
2655 */
2656 export function selectedCells(notebook: Notebook): nbformat.ICell[] {
2657 return notebook.widgets
2658 .filter(cell => notebook.isSelectedOrActive(cell))
2659 .map(cell => cell.model.toJSON())
2660 .map(cellJSON => {
2661 if ((cellJSON.metadata as JSONObject).deletable !== undefined) {
2662 delete (cellJSON.metadata as JSONObject).deletable;
2663 }
2664 return cellJSON;
2665 });
2666 }
2667
2668 /**
2669 * Copy or cut the selected cell data to the application clipboard.
2670 *
2671 * @param notebook - The target notebook widget.
2672 *
2673 * @param cut - True if the cells should be cut, false if they should be copied.
2674 */
2675 export function copyOrCut(notebook: Notebook, cut: boolean): void {
2676 if (!notebook.model || !notebook.activeCell) {
2677 return;
2678 }
2679
2680 const state = getState(notebook);
2681 const clipboard = Clipboard.getInstance();
2682
2683 notebook.mode = 'command';
2684 clipboard.clear();
2685
2686 const data = Private.selectedCells(notebook);
2687
2688 clipboard.setData(JUPYTER_CELL_MIME, data);
2689 if (cut) {
2690 deleteCells(notebook);
2691 } else {
2692 notebook.deselectAll();
2693 }
2694 if (cut) {
2695 notebook.lastClipboardInteraction = 'cut';
2696 } else {
2697 notebook.lastClipboardInteraction = 'copy';
2698 }
2699 void handleState(notebook, state);
2700 }
2701
2702 /**
2703 * Change the selected cell type(s).
2704 *
2705 * @param notebook - The target notebook widget.
2706 *
2707 * @param value - The target cell type.
2708 *
2709 * #### Notes
2710 * It should preserve the widget mode.
2711 * This action can be undone.
2712 * The existing selection will be cleared.
2713 * Any cells converted to markdown will be unrendered.
2714 */
2715 export function changeCellType(
2716 notebook: Notebook,
2717 value: nbformat.CellType
2718 ): void {
2719 const notebookSharedModel = notebook.model!.sharedModel;
2720 notebook.widgets.forEach((child, index) => {
2721 if (!notebook.isSelectedOrActive(child)) {
2722 return;
2723 }
2724 if (child.model.type !== value) {
2725 const raw = child.model.toJSON();
2726 notebookSharedModel.transact(() => {
2727 notebookSharedModel.deleteCell(index);
2728 if (value === 'code') {
2729 // After change of type outputs are deleted so cell can be trusted.
2730 raw.metadata.trusted = true;
2731 } else {
2732 // Otherwise clear the metadata as trusted is only "valid" on code
2733 // cells (since other cell types cannot have outputs).
2734 raw.metadata.trusted = undefined;
2735 }
2736 const newCell = notebookSharedModel.insertCell(index, {
2737 cell_type: value,
2738 source: raw.source,
2739 metadata: raw.metadata
2740 });
2741 if (raw.attachments && ['markdown', 'raw'].includes(value)) {
2742 (newCell as ISharedAttachmentsCell).attachments =
2743 raw.attachments as nbformat.IAttachments;
2744 }
2745 });
2746 }
2747 if (value === 'markdown') {
2748 // Fetch the new widget and unrender it.
2749 child = notebook.widgets[index];
2750 (child as MarkdownCell).rendered = false;
2751 }
2752 });
2753 notebook.deselectAll();
2754 }
2755
2756 /**
2757 * Delete the selected cells.
2758 *
2759 * @param notebook - The target notebook widget.
2760 *
2761 * #### Notes
2762 * The cell after the last selected cell will be activated.
2763 * If the last cell is deleted, then the previous one will be activated.
2764 * It will add a code cell if all cells are deleted.
2765 * This action can be undone.
2766 */
2767 export function deleteCells(notebook: Notebook): void {
2768 const model = notebook.model!;
2769 const sharedModel = model.sharedModel;
2770 const toDelete: number[] = [];
2771
2772 notebook.mode = 'command';
2773
2774 // Find the cells to delete.
2775 notebook.widgets.forEach((child, index) => {
2776 const deletable = child.model.getMetadata('deletable') !== false;
2777
2778 if (notebook.isSelectedOrActive(child) && deletable) {
2779 toDelete.push(index);
2780 notebook.model?.deletedCells.push(child.model.id);
2781 }
2782 });
2783
2784 // If cells are not deletable, we may not have anything to delete.
2785 if (toDelete.length > 0) {
2786 // Delete the cells as one undo event.
2787 sharedModel.transact(() => {
2788 // Delete cells in reverse order to maintain the correct indices.
2789 toDelete.reverse().forEach(index => {
2790 sharedModel.deleteCell(index);
2791 });
2792
2793 // Add a new cell if the notebook is empty. This is done
2794 // within the compound operation to make the deletion of
2795 // a notebook's last cell undoable.
2796 if (sharedModel.cells.length == toDelete.length) {
2797 sharedModel.insertCell(0, {
2798 cell_type: notebook.notebookConfig.defaultCell,
2799 metadata:
2800 notebook.notebookConfig.defaultCell === 'code'
2801 ? {
2802 // This is an empty cell created in empty notebook, thus is trusted
2803 trusted: true
2804 }
2805 : {}
2806 });
2807 }
2808 });
2809 // Select the *first* interior cell not deleted or the cell
2810 // *after* the last selected cell.
2811 // Note: The activeCellIndex is clamped to the available cells,
2812 // so if the last cell is deleted the previous cell will be activated.
2813 // The *first* index is the index of the last cell in the initial
2814 // toDelete list due to the `reverse` operation above.
2815 notebook.activeCellIndex = toDelete[0] - toDelete.length + 1;
2816 }
2817
2818 // Deselect any remaining, undeletable cells. Do this even if we don't
2819 // delete anything so that users are aware *something* happened.
2820 notebook.deselectAll();
2821 }
2822
2823 /**
2824 * Set the markdown header level of a cell.
2825 */
2826 export function setMarkdownHeader(cell: ICellModel, level: number): void {
2827 // Remove existing header or leading white space.
2828 let source = cell.sharedModel.getSource();
2829 const regex = /^(#+\s*)|^(\s*)/;
2830 const newHeader = Array(level + 1).join('#') + ' ';
2831 const matches = regex.exec(source);
2832
2833 if (matches) {
2834 source = source.slice(matches[0].length);
2835 }
2836 cell.sharedModel.setSource(newHeader + source);
2837 }
2838
2839 /** Functionality related to collapsible headings */
2840 export namespace Headings {
2841 /** Find the heading that is parent to cell.
2842 *
2843 * @param childCell - The cell that is child to the sought heading
2844 * @param notebook - The target notebook widget
2845 * @param includeChildCell [default=false] - if set to true and childCell is a heading itself, the childCell will be returned
2846 * @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
2847 *
2848 * @returns the (index | Cell object) of the parent heading or (-1 | null) if there is no parent heading.
2849 */
2850 export function findParentHeading(
2851 childCell: Cell,
2852 notebook: Notebook,
2853 includeChildCell = false,
2854 returnIndex = false
2855 ): number | Cell | null {
2856 let cellIdx =
2857 notebook.widgets.indexOf(childCell) - (includeChildCell ? 1 : 0);
2858 while (cellIdx >= 0) {
2859 let headingInfo = NotebookActions.getHeadingInfo(
2860 notebook.widgets[cellIdx]
2861 );
2862 if (headingInfo.isHeading) {
2863 return returnIndex ? cellIdx : notebook.widgets[cellIdx];
2864 }
2865 cellIdx--;
2866 }
2867 return returnIndex ? -1 : null;
2868 }
2869
2870 /** Find heading above with leq level than baseCell heading level.
2871 *
2872 * @param baseCell - cell relative to which so search
2873 * @param notebook - target notebook widget
2874 * @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
2875 *
2876 * @returns the (index | Cell object) of the found heading or (-1 | null) if no heading found.
2877 */
2878 export function findLowerEqualLevelParentHeadingAbove(
2879 baseCell: Cell,
2880 notebook: Notebook,
2881 returnIndex = false
2882 ): number | Cell | null {
2883 let baseHeadingLevel = Private.Headings.determineHeadingLevel(
2884 baseCell,
2885 notebook
2886 );
2887 if (baseHeadingLevel == -1) {
2888 baseHeadingLevel = 1; // if no heading level can be determined, assume we're on level 1
2889 }
2890
2891 // find the heading above with heading level <= baseHeadingLevel and return its index
2892 let cellIdx = notebook.widgets.indexOf(baseCell) - 1;
2893 while (cellIdx >= 0) {
2894 let cell = notebook.widgets[cellIdx];
2895 let headingInfo = NotebookActions.getHeadingInfo(cell);
2896 if (
2897 headingInfo.isHeading &&
2898 headingInfo.headingLevel <= baseHeadingLevel
2899 ) {
2900 return returnIndex ? cellIdx : cell;
2901 }
2902 cellIdx--;
2903 }
2904 return returnIndex ? -1 : null; // no heading found
2905 }
2906
2907 /** Find next heading with equal or lower level.
2908 *
2909 * @param baseCell - cell relative to which so search
2910 * @param notebook - target notebook widget
2911 * @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
2912 *
2913 * @returns the (index | Cell object) of the found heading or (-1 | null) if no heading found.
2914 */
2915 export function findLowerEqualLevelHeadingBelow(
2916 baseCell: Cell,
2917 notebook: Notebook,
2918 returnIndex = false
2919 ): number | Cell | null {
2920 let baseHeadingLevel = Private.Headings.determineHeadingLevel(
2921 baseCell,
2922 notebook
2923 );
2924 if (baseHeadingLevel == -1) {
2925 baseHeadingLevel = 1; // if no heading level can be determined, assume we're on level 1
2926 }
2927 let cellIdx = notebook.widgets.indexOf(baseCell) + 1;
2928 while (cellIdx < notebook.widgets.length) {
2929 let cell = notebook.widgets[cellIdx];
2930 let headingInfo = NotebookActions.getHeadingInfo(cell);
2931 if (
2932 headingInfo.isHeading &&
2933 headingInfo.headingLevel <= baseHeadingLevel
2934 ) {
2935 return returnIndex ? cellIdx : cell;
2936 }
2937 cellIdx++;
2938 }
2939 return returnIndex ? -1 : null;
2940 }
2941
2942 /** Find next heading.
2943 *
2944 * @param baseCell - cell relative to which so search
2945 * @param notebook - target notebook widget
2946 * @param returnIndex [default=false] - if set to true, the cell index is returned rather than the cell object.
2947 *
2948 * @returns the (index | Cell object) of the found heading or (-1 | null) if no heading found.
2949 */
2950 export function findHeadingBelow(
2951 baseCell: Cell,
2952 notebook: Notebook,
2953 returnIndex = false
2954 ): number | Cell | null {
2955 let cellIdx = notebook.widgets.indexOf(baseCell) + 1;
2956 while (cellIdx < notebook.widgets.length) {
2957 let cell = notebook.widgets[cellIdx];
2958 let headingInfo = NotebookActions.getHeadingInfo(cell);
2959 if (headingInfo.isHeading) {
2960 return returnIndex ? cellIdx : cell;
2961 }
2962 cellIdx++;
2963 }
2964 return returnIndex ? -1 : null;
2965 }
2966
2967 /** Determine the heading level of a cell.
2968 *
2969 * @param baseCell - The cell of which the heading level shall be determined
2970 * @param notebook - The target notebook widget
2971 *
2972 * @returns the heading level or -1 if there is no parent heading
2973 *
2974 * #### Notes
2975 * If the baseCell is a heading itself, the heading level of baseCell is returned.
2976 * If the baseCell is not a heading itself, the level of the parent heading is returned.
2977 * If there is no parent heading, -1 is returned.
2978 */
2979 export function determineHeadingLevel(
2980 baseCell: Cell,
2981 notebook: Notebook
2982 ): number {
2983 let headingInfoBaseCell = NotebookActions.getHeadingInfo(baseCell);
2984 // fill baseHeadingLevel or return null if there is no heading at or above baseCell
2985 if (headingInfoBaseCell.isHeading) {
2986 return headingInfoBaseCell.headingLevel;
2987 } else {
2988 let parentHeading = findParentHeading(
2989 baseCell,
2990 notebook,
2991 true
2992 ) as Cell | null;
2993 if (parentHeading == null) {
2994 return -1;
2995 }
2996 return NotebookActions.getHeadingInfo(parentHeading).headingLevel;
2997 }
2998 }
2999
3000 /** Insert a new heading cell at given position.
3001 *
3002 * @param cellIndex - where to insert
3003 * @param headingLevel - level of the new heading
3004 * @param notebook - target notebook
3005 *
3006 * #### Notes
3007 * Enters edit mode after insert.
3008 */
3009 export async function insertHeadingAboveCellIndex(
3010 cellIndex: number,
3011 headingLevel: number,
3012 notebook: Notebook
3013 ): Promise<void> {
3014 headingLevel = Math.min(Math.max(headingLevel, 1), 6);
3015 const state = Private.getState(notebook);
3016 const model = notebook.model!;
3017 const sharedModel = model!.sharedModel;
3018 sharedModel.insertCell(cellIndex, {
3019 cell_type: 'markdown',
3020 source: '#'.repeat(headingLevel) + ' '
3021 });
3022 notebook.activeCellIndex = cellIndex;
3023 if (notebook.activeCell?.inViewport === false) {
3024 await signalToPromise(notebook.activeCell.inViewportChanged, 200).catch(
3025 () => {
3026 // no-op
3027 }
3028 );
3029 }
3030 notebook.deselectAll();
3031
3032 void Private.handleState(notebook, state, true);
3033 notebook.mode = 'edit';
3034 notebook.widgets[cellIndex].setHidden(false);
3035 }
3036 }
3037}
3038
\No newline at end of file