actions.tsx 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { KernelMessage } from '@jupyterlab/services';
  4. import {
  5. IClientSession,
  6. Clipboard,
  7. Dialog,
  8. showDialog
  9. } from '@jupyterlab/apputils';
  10. import { nbformat } from '@jupyterlab/coreutils';
  11. import {
  12. ICellModel,
  13. ICodeCellModel,
  14. CodeCell,
  15. Cell,
  16. MarkdownCell
  17. } from '@jupyterlab/cells';
  18. import { ArrayExt, each, toArray } from '@phosphor/algorithm';
  19. import { JSONObject } from '@phosphor/coreutils';
  20. import { ElementExt } from '@phosphor/domutils';
  21. import { ISignal, Signal } from '@phosphor/signaling';
  22. import * as React from 'react';
  23. import { INotebookModel } from './model';
  24. import { Notebook } from './widget';
  25. // The message to display to the user when prompting to trust the notebook.
  26. const TRUST_MESSAGE = (
  27. <p>
  28. A trusted Jupyter notebook may execute hidden malicious code when you open
  29. it.
  30. <br />
  31. Selecting trust will re-render this notebook in a trusted state.
  32. <br />
  33. For more information, see the
  34. <a href="https://jupyter-notebook.readthedocs.io/en/stable/security.html">
  35. Jupyter security documentation
  36. </a>
  37. </p>
  38. );
  39. /**
  40. * The mimetype used for Jupyter cell data.
  41. */
  42. const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
  43. /**
  44. * A collection of actions that run against notebooks.
  45. *
  46. * #### Notes
  47. * All of the actions are a no-op if there is no model on the notebook.
  48. * The actions set the widget `mode` to `'command'` unless otherwise specified.
  49. * The actions will preserve the selection on the notebook widget unless
  50. * otherwise specified.
  51. */
  52. export class NotebookActions {
  53. /**
  54. * A signal that emits whenever a cell is run.
  55. */
  56. static get executed(): ISignal<any, { notebook: Notebook; cell: Cell }> {
  57. return Private.executed;
  58. }
  59. /**
  60. * A private constructor for the `NotebookActions` class.
  61. *
  62. * #### Notes
  63. * This class can never be instantiated. Its static member `executed` will be
  64. * merged with the `NotebookActions` namespace. The reason it exists as a
  65. * standalone class is because at run time, the `Private.executed` variable
  66. * does not yet exist, so it needs to be referenced via a getter.
  67. */
  68. private constructor() {}
  69. }
  70. /**
  71. * A namespace for `NotebookActions` static methods.
  72. */
  73. export namespace NotebookActions {
  74. /**
  75. * Split the active cell into two cells.
  76. *
  77. * @param widget - The target notebook widget.
  78. *
  79. * #### Notes
  80. * It will preserve the existing mode.
  81. * The second cell will be activated.
  82. * The existing selection will be cleared.
  83. * The leading whitespace in the second cell will be removed.
  84. * If there is no content, two empty cells will be created.
  85. * Both cells will have the same type as the original cell.
  86. * This action can be undone.
  87. */
  88. export function splitCell(notebook: Notebook): void {
  89. if (!notebook.model || !notebook.activeCell) {
  90. return;
  91. }
  92. const state = Private.getState(notebook);
  93. notebook.deselectAll();
  94. const nbModel = notebook.model;
  95. const index = notebook.activeCellIndex;
  96. const child = notebook.widgets[index];
  97. const editor = child.editor;
  98. const position = editor.getCursorPosition();
  99. const offset = editor.getOffsetAt(position);
  100. const orig = child.model.value.text;
  101. // Create new models to preserve history.
  102. const clone0 = Private.cloneCell(nbModel, child.model);
  103. const clone1 = Private.cloneCell(nbModel, child.model);
  104. if (clone0.type === 'code') {
  105. (clone0 as ICodeCellModel).outputs.clear();
  106. }
  107. clone0.value.text = orig
  108. .slice(0, offset)
  109. .replace(/^\n+/, '')
  110. .replace(/\n+$/, '');
  111. clone1.value.text = orig
  112. .slice(offset)
  113. .replace(/^\n+/, '')
  114. .replace(/\n+$/, '');
  115. // Make the changes while preserving history.
  116. const cells = nbModel.cells;
  117. cells.beginCompoundOperation();
  118. cells.set(index, clone0);
  119. cells.insert(index + 1, clone1);
  120. cells.endCompoundOperation();
  121. notebook.activeCellIndex++;
  122. Private.handleState(notebook, state);
  123. }
  124. /**
  125. * Merge the selected cells.
  126. *
  127. * @param notebook - The target notebook widget.
  128. *
  129. * #### Notes
  130. * The widget mode will be preserved.
  131. * If only one cell is selected, the next cell will be selected.
  132. * If the active cell is a code cell, its outputs will be cleared.
  133. * This action can be undone.
  134. * The final cell will have the same type as the active cell.
  135. * If the active cell is a markdown cell, it will be unrendered.
  136. */
  137. export function mergeCells(notebook: Notebook): void {
  138. if (!notebook.model || !notebook.activeCell) {
  139. return;
  140. }
  141. const state = Private.getState(notebook);
  142. const toMerge: string[] = [];
  143. const toDelete: ICellModel[] = [];
  144. const model = notebook.model;
  145. const cells = model.cells;
  146. const primary = notebook.activeCell;
  147. const active = notebook.activeCellIndex;
  148. // Get the cells to merge.
  149. notebook.widgets.forEach((child, index) => {
  150. if (notebook.isSelectedOrActive(child)) {
  151. toMerge.push(child.model.value.text);
  152. if (index !== active) {
  153. toDelete.push(child.model);
  154. }
  155. }
  156. });
  157. // Check for only a single cell selected.
  158. if (toMerge.length === 1) {
  159. // Bail if it is the last cell.
  160. if (active === cells.length - 1) {
  161. return;
  162. }
  163. // Otherwise merge with the next cell.
  164. const cellModel = cells.get(active + 1);
  165. toMerge.push(cellModel.value.text);
  166. toDelete.push(cellModel);
  167. }
  168. notebook.deselectAll();
  169. // Create a new cell for the source to preserve history.
  170. const newModel = Private.cloneCell(model, primary.model);
  171. newModel.value.text = toMerge.join('\n\n');
  172. if (newModel.type === 'code') {
  173. (newModel as ICodeCellModel).outputs.clear();
  174. }
  175. // Make the changes while preserving history.
  176. cells.beginCompoundOperation();
  177. cells.set(active, newModel);
  178. toDelete.forEach(cell => {
  179. cells.removeValue(cell);
  180. });
  181. cells.endCompoundOperation();
  182. // If the original cell is a markdown cell, make sure
  183. // the new cell is unrendered.
  184. if (primary instanceof MarkdownCell) {
  185. (notebook.activeCell as MarkdownCell).rendered = false;
  186. }
  187. Private.handleState(notebook, state);
  188. }
  189. /**
  190. * Delete the selected cells.
  191. *
  192. * @param notebook - The target notebook widget.
  193. *
  194. * #### Notes
  195. * The cell after the last selected cell will be activated.
  196. * It will add a code cell if all cells are deleted.
  197. * This action can be undone.
  198. */
  199. export function deleteCells(notebook: Notebook): void {
  200. if (!notebook.model || !notebook.activeCell) {
  201. return;
  202. }
  203. const state = Private.getState(notebook);
  204. Private.deleteCells(notebook);
  205. Private.handleState(notebook, state);
  206. }
  207. /**
  208. * Insert a new code cell above the active cell.
  209. *
  210. * @param notebook - The target notebook widget.
  211. *
  212. * #### Notes
  213. * The widget mode will be preserved.
  214. * This action can be undone.
  215. * The existing selection will be cleared.
  216. * The new cell will the active cell.
  217. */
  218. export function insertAbove(notebook: Notebook): void {
  219. if (!notebook.model || !notebook.activeCell) {
  220. return;
  221. }
  222. const state = Private.getState(notebook);
  223. const model = notebook.model;
  224. const cell = model.contentFactory.createCell(
  225. notebook.notebookConfig.defaultCell,
  226. {}
  227. );
  228. const active = notebook.activeCellIndex;
  229. model.cells.insert(active, cell);
  230. // Make the newly inserted cell active.
  231. notebook.activeCellIndex = active;
  232. notebook.deselectAll();
  233. Private.handleState(notebook, state, true);
  234. }
  235. /**
  236. * Insert a new code cell below the active cell.
  237. *
  238. * @param notebook - The target notebook widget.
  239. *
  240. * #### Notes
  241. * The widget mode will be preserved.
  242. * This action can be undone.
  243. * The existing selection will be cleared.
  244. * The new cell will be the active cell.
  245. */
  246. export function insertBelow(notebook: Notebook): void {
  247. if (!notebook.model || !notebook.activeCell) {
  248. return;
  249. }
  250. const state = Private.getState(notebook);
  251. const model = notebook.model;
  252. const cell = model.contentFactory.createCell(
  253. notebook.notebookConfig.defaultCell,
  254. {}
  255. );
  256. model.cells.insert(notebook.activeCellIndex + 1, cell);
  257. // Make the newly inserted cell active.
  258. notebook.activeCellIndex++;
  259. notebook.deselectAll();
  260. Private.handleState(notebook, state, true);
  261. }
  262. /**
  263. * Move the selected cell(s) down.
  264. *
  265. * @param notebook = The target notebook widget.
  266. */
  267. export function moveDown(notebook: Notebook): void {
  268. if (!notebook.model || !notebook.activeCell) {
  269. return;
  270. }
  271. const state = Private.getState(notebook);
  272. const cells = notebook.model.cells;
  273. const widgets = notebook.widgets;
  274. cells.beginCompoundOperation();
  275. for (let i = cells.length - 2; i > -1; i--) {
  276. if (notebook.isSelectedOrActive(widgets[i])) {
  277. if (!notebook.isSelectedOrActive(widgets[i + 1])) {
  278. cells.move(i, i + 1);
  279. if (notebook.activeCellIndex === i) {
  280. notebook.activeCellIndex++;
  281. }
  282. notebook.select(widgets[i + 1]);
  283. notebook.deselect(widgets[i]);
  284. }
  285. }
  286. }
  287. cells.endCompoundOperation();
  288. Private.handleState(notebook, state, true);
  289. }
  290. /**
  291. * Move the selected cell(s) up.
  292. *
  293. * @param widget - The target notebook widget.
  294. */
  295. export function moveUp(notebook: Notebook): void {
  296. if (!notebook.model || !notebook.activeCell) {
  297. return;
  298. }
  299. const state = Private.getState(notebook);
  300. const cells = notebook.model.cells;
  301. const widgets = notebook.widgets;
  302. cells.beginCompoundOperation();
  303. for (let i = 1; i < cells.length; i++) {
  304. if (notebook.isSelectedOrActive(widgets[i])) {
  305. if (!notebook.isSelectedOrActive(widgets[i - 1])) {
  306. cells.move(i, i - 1);
  307. if (notebook.activeCellIndex === i) {
  308. notebook.activeCellIndex--;
  309. }
  310. notebook.select(widgets[i - 1]);
  311. notebook.deselect(widgets[i]);
  312. }
  313. }
  314. }
  315. cells.endCompoundOperation();
  316. Private.handleState(notebook, state, true);
  317. }
  318. /**
  319. * Change the selected cell type(s).
  320. *
  321. * @param notebook - The target notebook widget.
  322. *
  323. * @param value - The target cell type.
  324. *
  325. * #### Notes
  326. * It should preserve the widget mode.
  327. * This action can be undone.
  328. * The existing selection will be cleared.
  329. * Any cells converted to markdown will be unrendered.
  330. */
  331. export function changeCellType(
  332. notebook: Notebook,
  333. value: nbformat.CellType
  334. ): void {
  335. if (!notebook.model || !notebook.activeCell) {
  336. return;
  337. }
  338. const state = Private.getState(notebook);
  339. Private.changeCellType(notebook, value);
  340. Private.handleState(notebook, state);
  341. }
  342. /**
  343. * Run the selected cell(s).
  344. *
  345. * @param notebook - The target notebook widget.
  346. *
  347. * @param session - The optional client session object.
  348. *
  349. * #### Notes
  350. * The last selected cell will be activated, but not scrolled into view.
  351. * The existing selection will be cleared.
  352. * An execution error will prevent the remaining code cells from executing.
  353. * All markdown cells will be rendered.
  354. */
  355. export function run(
  356. notebook: Notebook,
  357. session?: IClientSession
  358. ): Promise<boolean> {
  359. if (!notebook.model || !notebook.activeCell) {
  360. return Promise.resolve(false);
  361. }
  362. const state = Private.getState(notebook);
  363. const promise = Private.runSelected(notebook, session);
  364. Private.handleRunState(notebook, state, false);
  365. return promise;
  366. }
  367. /**
  368. * Run the selected cell(s) and advance to the next cell.
  369. *
  370. * @param notebook - The target notebook widget.
  371. *
  372. * @param session - The optional client session object.
  373. *
  374. * #### Notes
  375. * The existing selection will be cleared.
  376. * The cell after the last selected cell will be activated and scrolled into view.
  377. * An execution error will prevent the remaining code cells from executing.
  378. * All markdown cells will be rendered.
  379. * If the last selected cell is the last cell, a new code cell
  380. * will be created in `'edit'` mode. The new cell creation can be undone.
  381. */
  382. export function runAndAdvance(
  383. notebook: Notebook,
  384. session?: IClientSession
  385. ): Promise<boolean> {
  386. if (!notebook.model || !notebook.activeCell) {
  387. return Promise.resolve(false);
  388. }
  389. const state = Private.getState(notebook);
  390. const promise = Private.runSelected(notebook, session);
  391. const model = notebook.model;
  392. if (notebook.activeCellIndex === notebook.widgets.length - 1) {
  393. const cell = model.contentFactory.createCell(
  394. notebook.notebookConfig.defaultCell,
  395. {}
  396. );
  397. model.cells.push(cell);
  398. notebook.activeCellIndex++;
  399. notebook.mode = 'edit';
  400. } else {
  401. notebook.activeCellIndex++;
  402. }
  403. Private.handleRunState(notebook, state, true);
  404. return promise;
  405. }
  406. /**
  407. * Run the selected cell(s) and insert a new code cell.
  408. *
  409. * @param notebook - The target notebook widget.
  410. *
  411. * @param session - The optional client session object.
  412. *
  413. * #### Notes
  414. * An execution error will prevent the remaining code cells from executing.
  415. * All markdown cells will be rendered.
  416. * The widget mode will be set to `'edit'` after running.
  417. * The existing selection will be cleared.
  418. * The cell insert can be undone.
  419. * The new cell will be scrolled into view.
  420. */
  421. export function runAndInsert(
  422. notebook: Notebook,
  423. session?: IClientSession
  424. ): Promise<boolean> {
  425. if (!notebook.model || !notebook.activeCell) {
  426. return Promise.resolve(false);
  427. }
  428. const state = Private.getState(notebook);
  429. const promise = Private.runSelected(notebook, session);
  430. const model = notebook.model;
  431. const cell = model.contentFactory.createCell(
  432. notebook.notebookConfig.defaultCell,
  433. {}
  434. );
  435. model.cells.insert(notebook.activeCellIndex + 1, cell);
  436. notebook.activeCellIndex++;
  437. notebook.mode = 'edit';
  438. Private.handleRunState(notebook, state, true);
  439. return promise;
  440. }
  441. /**
  442. * Run all of the cells in the notebook.
  443. *
  444. * @param notebook - The target notebook widget.
  445. *
  446. * @param session - The optional client session object.
  447. *
  448. * #### Notes
  449. * The existing selection will be cleared.
  450. * An execution error will prevent the remaining code cells from executing.
  451. * All markdown cells will be rendered.
  452. * The last cell in the notebook will be activated and scrolled into view.
  453. */
  454. export function runAll(
  455. notebook: Notebook,
  456. session?: IClientSession
  457. ): Promise<boolean> {
  458. if (!notebook.model || !notebook.activeCell) {
  459. return Promise.resolve(false);
  460. }
  461. const state = Private.getState(notebook);
  462. notebook.widgets.forEach(child => {
  463. notebook.select(child);
  464. });
  465. const promise = Private.runSelected(notebook, session);
  466. Private.handleRunState(notebook, state, true);
  467. return promise;
  468. }
  469. export function renderAllMarkdown(
  470. notebook: Notebook,
  471. session?: IClientSession
  472. ): Promise<boolean> {
  473. if (!notebook.model || !notebook.activeCell) {
  474. return Promise.resolve(false);
  475. }
  476. const previousIndex = notebook.activeCellIndex;
  477. const state = Private.getState(notebook);
  478. notebook.widgets.forEach((child, index) => {
  479. if (child.model.type === 'markdown') {
  480. notebook.select(child);
  481. // This is to make sure that the activeCell
  482. // does not get executed
  483. notebook.activeCellIndex = index;
  484. }
  485. });
  486. if (notebook.activeCell.model.type !== 'markdown') {
  487. return Promise.resolve(true);
  488. }
  489. const promise = Private.runSelected(notebook, session);
  490. notebook.activeCellIndex = previousIndex;
  491. Private.handleRunState(notebook, state, true);
  492. return promise;
  493. }
  494. /**
  495. * Run all of the cells before the currently active cell (exclusive).
  496. *
  497. * @param notebook - The target notebook widget.
  498. *
  499. * @param session - The optional client session object.
  500. *
  501. * #### Notes
  502. * The existing selection will be cleared.
  503. * An execution error will prevent the remaining code cells from executing.
  504. * All markdown cells will be rendered.
  505. * The currently active cell will remain selected.
  506. */
  507. export function runAllAbove(
  508. notebook: Notebook,
  509. session?: IClientSession
  510. ): Promise<boolean> {
  511. const { activeCell, activeCellIndex, model } = notebook;
  512. if (!model || !activeCell || activeCellIndex < 1) {
  513. return Promise.resolve(false);
  514. }
  515. const state = Private.getState(notebook);
  516. notebook.activeCellIndex--;
  517. notebook.deselectAll();
  518. for (let i = 0; i < notebook.activeCellIndex; ++i) {
  519. notebook.select(notebook.widgets[i]);
  520. }
  521. const promise = Private.runSelected(notebook, session);
  522. notebook.activeCellIndex++;
  523. Private.handleRunState(notebook, state, true);
  524. return promise;
  525. }
  526. /**
  527. * Run all of the cells after the currently active cell (inclusive).
  528. *
  529. * @param notebook - The target notebook widget.
  530. *
  531. * @param session - The optional client session object.
  532. *
  533. * #### Notes
  534. * The existing selection will be cleared.
  535. * An execution error will prevent the remaining code cells from executing.
  536. * All markdown cells will be rendered.
  537. * The last cell in the notebook will be activated and scrolled into view.
  538. */
  539. export function runAllBelow(
  540. notebook: Notebook,
  541. session?: IClientSession
  542. ): Promise<boolean> {
  543. if (!notebook.model || !notebook.activeCell) {
  544. return Promise.resolve(false);
  545. }
  546. const state = Private.getState(notebook);
  547. notebook.deselectAll();
  548. for (let i = notebook.activeCellIndex; i < notebook.widgets.length; ++i) {
  549. notebook.select(notebook.widgets[i]);
  550. }
  551. const promise = Private.runSelected(notebook, session);
  552. Private.handleRunState(notebook, state, true);
  553. return promise;
  554. }
  555. /**
  556. * Select the above the active cell.
  557. *
  558. * @param notebook - The target notebook widget.
  559. *
  560. * #### Notes
  561. * The widget mode will be preserved.
  562. * This is a no-op if the first cell is the active cell.
  563. * This will skip any collapsed cells.
  564. * The existing selection will be cleared.
  565. */
  566. export function selectAbove(notebook: Notebook): void {
  567. if (!notebook.model || !notebook.activeCell) {
  568. return;
  569. }
  570. if (notebook.activeCellIndex === 0) {
  571. return;
  572. }
  573. let possibleNextCell = notebook.activeCellIndex - 1;
  574. // find first non hidden cell above current cell
  575. if (notebook.mode === 'edit') {
  576. while (notebook.widgets[possibleNextCell].inputHidden) {
  577. // If we are at the top cell, we cannot change selection.
  578. if (possibleNextCell === 0) {
  579. return;
  580. }
  581. possibleNextCell -= 1;
  582. }
  583. }
  584. const state = Private.getState(notebook);
  585. notebook.activeCellIndex = possibleNextCell;
  586. notebook.deselectAll();
  587. Private.handleState(notebook, state, true);
  588. }
  589. /**
  590. * Select the cell below the active cell.
  591. *
  592. * @param notebook - The target notebook widget.
  593. *
  594. * #### Notes
  595. * The widget mode will be preserved.
  596. * This is a no-op if the last cell is the active cell.
  597. * This will skip any collapsed cells.
  598. * The existing selection will be cleared.
  599. */
  600. export function selectBelow(notebook: Notebook): void {
  601. if (!notebook.model || !notebook.activeCell) {
  602. return;
  603. }
  604. const maxCellIndex = notebook.widgets.length - 1;
  605. if (notebook.activeCellIndex === maxCellIndex) {
  606. return;
  607. }
  608. let possibleNextCell = notebook.activeCellIndex + 1;
  609. // find first non hidden cell below current cell
  610. if (notebook.mode === 'edit') {
  611. while (notebook.widgets[possibleNextCell].inputHidden) {
  612. // If we are at the bottom cell, we cannot change selection.
  613. if (possibleNextCell === maxCellIndex) {
  614. return;
  615. }
  616. possibleNextCell += 1;
  617. }
  618. }
  619. const state = Private.getState(notebook);
  620. notebook.activeCellIndex = possibleNextCell;
  621. notebook.deselectAll();
  622. Private.handleState(notebook, state, true);
  623. }
  624. /**
  625. * Extend the selection to the cell above.
  626. *
  627. * @param notebook - The target notebook widget.
  628. *
  629. * #### Notes
  630. * This is a no-op if the first cell is the active cell.
  631. * The new cell will be activated.
  632. */
  633. export function extendSelectionAbove(notebook: Notebook): void {
  634. if (!notebook.model || !notebook.activeCell) {
  635. return;
  636. }
  637. // Do not wrap around.
  638. if (notebook.activeCellIndex === 0) {
  639. return;
  640. }
  641. const state = Private.getState(notebook);
  642. notebook.mode = 'command';
  643. notebook.extendContiguousSelectionTo(notebook.activeCellIndex - 1);
  644. Private.handleState(notebook, state, true);
  645. }
  646. /**
  647. * Extend the selection to the cell below.
  648. *
  649. * @param notebook - The target notebook widget.
  650. *
  651. * #### Notes
  652. * This is a no-op if the last cell is the active cell.
  653. * The new cell will be activated.
  654. */
  655. export function extendSelectionBelow(notebook: Notebook): void {
  656. if (!notebook.model || !notebook.activeCell) {
  657. return;
  658. }
  659. // Do not wrap around.
  660. if (notebook.activeCellIndex === notebook.widgets.length - 1) {
  661. return;
  662. }
  663. const state = Private.getState(notebook);
  664. notebook.mode = 'command';
  665. notebook.extendContiguousSelectionTo(notebook.activeCellIndex + 1);
  666. Private.handleState(notebook, state, true);
  667. }
  668. /**
  669. * Select all of the cells of the notebook.
  670. *
  671. * @param notebook - the target notebook widget.
  672. */
  673. export function selectAll(notebook: Notebook): void {
  674. if (!notebook.model || !notebook.activeCell) {
  675. return;
  676. }
  677. notebook.widgets.forEach(child => {
  678. notebook.select(child);
  679. });
  680. }
  681. /**
  682. * Deselect all of the cells of the notebook.
  683. *
  684. * @param notebook - the targe notebook widget.
  685. */
  686. export function deselectAll(notebook: Notebook): void {
  687. if (!notebook.model || !notebook.activeCell) {
  688. return;
  689. }
  690. notebook.deselectAll();
  691. }
  692. /**
  693. * Copy the selected cell data to a clipboard.
  694. *
  695. * @param notebook - The target notebook widget.
  696. */
  697. export function copy(notebook: Notebook): void {
  698. Private.copyOrCut(notebook, false);
  699. }
  700. /**
  701. * Cut the selected cell data to a clipboard.
  702. *
  703. * @param notebook - The target notebook widget.
  704. *
  705. * #### Notes
  706. * This action can be undone.
  707. * A new code cell is added if all cells are cut.
  708. */
  709. export function cut(notebook: Notebook): void {
  710. Private.copyOrCut(notebook, true);
  711. }
  712. /**
  713. * Paste cells from the application clipboard.
  714. *
  715. * @param notebook - The target notebook widget.
  716. *
  717. * @param mode - the mode of the paste operation: 'below' pastes cells
  718. * below the active cell, 'above' pastes cells above the active cell,
  719. * and 'replace' removes the currently selected cells and pastes cells
  720. * in their place.
  721. *
  722. * #### Notes
  723. * The last pasted cell becomes the active cell.
  724. * This is a no-op if there is no cell data on the clipboard.
  725. * This action can be undone.
  726. */
  727. export function paste(
  728. notebook: Notebook,
  729. mode: 'below' | 'above' | 'replace' = 'below'
  730. ): void {
  731. if (!notebook.model || !notebook.activeCell) {
  732. return;
  733. }
  734. const clipboard = Clipboard.getInstance();
  735. if (!clipboard.hasData(JUPYTER_CELL_MIME)) {
  736. return;
  737. }
  738. const state = Private.getState(notebook);
  739. const values = clipboard.getData(JUPYTER_CELL_MIME) as nbformat.IBaseCell[];
  740. const model = notebook.model;
  741. notebook.mode = 'command';
  742. const newCells = values.map(cell => {
  743. switch (cell.cell_type) {
  744. case 'code':
  745. return model.contentFactory.createCodeCell({ cell });
  746. case 'markdown':
  747. return model.contentFactory.createMarkdownCell({ cell });
  748. default:
  749. return model.contentFactory.createRawCell({ cell });
  750. }
  751. });
  752. const cells = notebook.model.cells;
  753. let index: number;
  754. cells.beginCompoundOperation();
  755. // Set the starting index of the paste operation depending upon the mode.
  756. switch (mode) {
  757. case 'below':
  758. index = notebook.activeCellIndex;
  759. break;
  760. case 'above':
  761. index = notebook.activeCellIndex - 1;
  762. break;
  763. case 'replace':
  764. // Find the cells to delete.
  765. const toDelete: number[] = [];
  766. notebook.widgets.forEach((child, index) => {
  767. const deletable = child.model.metadata.get('deletable') !== false;
  768. if (notebook.isSelectedOrActive(child) && deletable) {
  769. toDelete.push(index);
  770. }
  771. });
  772. // If cells are not deletable, we may not have anything to delete.
  773. if (toDelete.length > 0) {
  774. // Delete the cells as one undo event.
  775. toDelete.reverse().forEach(i => {
  776. cells.remove(i);
  777. });
  778. }
  779. index = toDelete[0];
  780. break;
  781. default:
  782. break;
  783. }
  784. newCells.forEach(cell => {
  785. cells.insert(++index, cell);
  786. });
  787. cells.endCompoundOperation();
  788. notebook.activeCellIndex += newCells.length;
  789. notebook.deselectAll();
  790. Private.handleState(notebook, state);
  791. }
  792. /**
  793. * Undo a cell action.
  794. *
  795. * @param notebook - The target notebook widget.
  796. *
  797. * #### Notes
  798. * This is a no-op if if there are no cell actions to undo.
  799. */
  800. export function undo(notebook: Notebook): void {
  801. if (!notebook.model || !notebook.activeCell) {
  802. return;
  803. }
  804. const state = Private.getState(notebook);
  805. notebook.mode = 'command';
  806. notebook.model.cells.undo();
  807. notebook.deselectAll();
  808. Private.handleState(notebook, state);
  809. }
  810. /**
  811. * Redo a cell action.
  812. *
  813. * @param notebook - The target notebook widget.
  814. *
  815. * #### Notes
  816. * This is a no-op if there are no cell actions to redo.
  817. */
  818. export function redo(notebook: Notebook): void {
  819. if (!notebook.model || !notebook.activeCell) {
  820. return;
  821. }
  822. const state = Private.getState(notebook);
  823. notebook.mode = 'command';
  824. notebook.model.cells.redo();
  825. notebook.deselectAll();
  826. Private.handleState(notebook, state);
  827. }
  828. /**
  829. * Toggle the line number of all cells.
  830. *
  831. * @param notebook - The target notebook widget.
  832. *
  833. * #### Notes
  834. * The original state is based on the state of the active cell.
  835. * The `mode` of the widget will be preserved.
  836. */
  837. export function toggleAllLineNumbers(notebook: Notebook): void {
  838. if (!notebook.model || !notebook.activeCell) {
  839. return;
  840. }
  841. const state = Private.getState(notebook);
  842. const config = notebook.editorConfig;
  843. const lineNumbers = !(
  844. config.code.lineNumbers &&
  845. config.markdown.lineNumbers &&
  846. config.raw.lineNumbers
  847. );
  848. const newConfig = {
  849. code: { ...config.code, lineNumbers },
  850. markdown: { ...config.markdown, lineNumbers },
  851. raw: { ...config.raw, lineNumbers }
  852. };
  853. notebook.editorConfig = newConfig;
  854. Private.handleState(notebook, state);
  855. }
  856. /**
  857. * Clear the code outputs of the selected cells.
  858. *
  859. * @param notebook - The target notebook widget.
  860. *
  861. * #### Notes
  862. * The widget `mode` will be preserved.
  863. */
  864. export function clearOutputs(notebook: Notebook): void {
  865. if (!notebook.model || !notebook.activeCell) {
  866. return;
  867. }
  868. const state = Private.getState(notebook);
  869. each(notebook.model.cells, (cell: ICodeCellModel, index) => {
  870. const child = notebook.widgets[index];
  871. if (notebook.isSelectedOrActive(child) && cell.type === 'code') {
  872. cell.outputs.clear();
  873. (child as CodeCell).outputHidden = false;
  874. cell.executionCount = null;
  875. }
  876. });
  877. Private.handleState(notebook, state);
  878. }
  879. /**
  880. * Clear all the code outputs on the widget.
  881. *
  882. * @param notebook - The target notebook widget.
  883. *
  884. * #### Notes
  885. * The widget `mode` will be preserved.
  886. */
  887. export function clearAllOutputs(notebook: Notebook): void {
  888. if (!notebook.model || !notebook.activeCell) {
  889. return;
  890. }
  891. const state = Private.getState(notebook);
  892. each(notebook.model.cells, (cell: ICodeCellModel, index) => {
  893. const child = notebook.widgets[index];
  894. if (cell.type === 'code') {
  895. cell.outputs.clear();
  896. cell.executionCount = null;
  897. (child as CodeCell).outputHidden = false;
  898. }
  899. });
  900. Private.handleState(notebook, state);
  901. }
  902. /**
  903. * Hide the code on selected code cells.
  904. *
  905. * @param notebook - The target notebook widget.
  906. */
  907. export function hideCode(notebook: Notebook): void {
  908. if (!notebook.model || !notebook.activeCell) {
  909. return;
  910. }
  911. const state = Private.getState(notebook);
  912. notebook.widgets.forEach(cell => {
  913. if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
  914. cell.inputHidden = true;
  915. }
  916. });
  917. Private.handleState(notebook, state);
  918. }
  919. /**
  920. * Show the code on selected code cells.
  921. *
  922. * @param notebook - The target notebook widget.
  923. */
  924. export function showCode(notebook: Notebook): void {
  925. if (!notebook.model || !notebook.activeCell) {
  926. return;
  927. }
  928. const state = Private.getState(notebook);
  929. notebook.widgets.forEach(cell => {
  930. if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
  931. cell.inputHidden = false;
  932. }
  933. });
  934. Private.handleState(notebook, state);
  935. }
  936. /**
  937. * Hide the code on all code cells.
  938. *
  939. * @param notebook - The target notebook widget.
  940. */
  941. export function hideAllCode(notebook: Notebook): void {
  942. if (!notebook.model || !notebook.activeCell) {
  943. return;
  944. }
  945. const state = Private.getState(notebook);
  946. notebook.widgets.forEach(cell => {
  947. if (cell.model.type === 'code') {
  948. cell.inputHidden = true;
  949. }
  950. });
  951. Private.handleState(notebook, state);
  952. }
  953. /**
  954. * Show the code on all code cells.
  955. *
  956. * @param widget - The target notebook widget.
  957. */
  958. export function showAllCode(notebook: Notebook): void {
  959. if (!notebook.model || !notebook.activeCell) {
  960. return;
  961. }
  962. const state = Private.getState(notebook);
  963. notebook.widgets.forEach(cell => {
  964. if (cell.model.type === 'code') {
  965. cell.inputHidden = false;
  966. }
  967. });
  968. Private.handleState(notebook, state);
  969. }
  970. /**
  971. * Hide the output on selected code cells.
  972. *
  973. * @param notebook - The target notebook widget.
  974. */
  975. export function hideOutput(notebook: Notebook): void {
  976. if (!notebook.model || !notebook.activeCell) {
  977. return;
  978. }
  979. const state = Private.getState(notebook);
  980. notebook.widgets.forEach(cell => {
  981. if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
  982. (cell as CodeCell).outputHidden = true;
  983. }
  984. });
  985. Private.handleState(notebook, state);
  986. }
  987. /**
  988. * Show the output on selected code cells.
  989. *
  990. * @param notebook - The target notebook widget.
  991. */
  992. export function showOutput(notebook: Notebook): void {
  993. if (!notebook.model || !notebook.activeCell) {
  994. return;
  995. }
  996. const state = Private.getState(notebook);
  997. notebook.widgets.forEach(cell => {
  998. if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
  999. (cell as CodeCell).outputHidden = false;
  1000. }
  1001. });
  1002. Private.handleState(notebook, state);
  1003. }
  1004. /**
  1005. * Hide the output on all code cells.
  1006. *
  1007. * @param notebook - The target notebook widget.
  1008. */
  1009. export function hideAllOutputs(notebook: Notebook): void {
  1010. if (!notebook.model || !notebook.activeCell) {
  1011. return;
  1012. }
  1013. const state = Private.getState(notebook);
  1014. notebook.widgets.forEach(cell => {
  1015. if (cell.model.type === 'code') {
  1016. (cell as CodeCell).outputHidden = true;
  1017. }
  1018. });
  1019. Private.handleState(notebook, state);
  1020. }
  1021. /**
  1022. * Show the output on all code cells.
  1023. *
  1024. * @param notebook - The target notebook widget.
  1025. */
  1026. export function showAllOutputs(notebook: Notebook): void {
  1027. if (!notebook.model || !notebook.activeCell) {
  1028. return;
  1029. }
  1030. const state = Private.getState(notebook);
  1031. notebook.widgets.forEach(cell => {
  1032. if (cell.model.type === 'code') {
  1033. (cell as CodeCell).outputHidden = false;
  1034. }
  1035. });
  1036. Private.handleState(notebook, state);
  1037. }
  1038. /**
  1039. * Enable output scrolling for all selected cells.
  1040. *
  1041. * @param notebook - The target notebook widget.
  1042. */
  1043. export function enableOutputScrolling(notebook: Notebook): void {
  1044. if (!notebook.model || !notebook.activeCell) {
  1045. return;
  1046. }
  1047. const state = Private.getState(notebook);
  1048. notebook.widgets.forEach(cell => {
  1049. if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
  1050. (cell as CodeCell).outputsScrolled = true;
  1051. }
  1052. });
  1053. Private.handleState(notebook, state);
  1054. }
  1055. /**
  1056. * Disable output scrolling for all selected cells.
  1057. *
  1058. * @param notebook - The target notebook widget.
  1059. */
  1060. export function disableOutputScrolling(notebook: Notebook): void {
  1061. if (!notebook.model || !notebook.activeCell) {
  1062. return;
  1063. }
  1064. const state = Private.getState(notebook);
  1065. notebook.widgets.forEach(cell => {
  1066. if (notebook.isSelectedOrActive(cell) && cell.model.type === 'code') {
  1067. (cell as CodeCell).outputsScrolled = false;
  1068. }
  1069. });
  1070. Private.handleState(notebook, state);
  1071. }
  1072. /**
  1073. * Set the markdown header level.
  1074. *
  1075. * @param notebook - The target notebook widget.
  1076. *
  1077. * @param level - The header level.
  1078. *
  1079. * #### Notes
  1080. * All selected cells will be switched to markdown.
  1081. * The level will be clamped between 1 and 6.
  1082. * If there is an existing header, it will be replaced.
  1083. * There will always be one blank space after the header.
  1084. * The cells will be unrendered.
  1085. */
  1086. export function setMarkdownHeader(notebook: Notebook, level: number) {
  1087. if (!notebook.model || !notebook.activeCell) {
  1088. return;
  1089. }
  1090. const state = Private.getState(notebook);
  1091. const cells = notebook.model.cells;
  1092. level = Math.min(Math.max(level, 1), 6);
  1093. notebook.widgets.forEach((child, index) => {
  1094. if (notebook.isSelectedOrActive(child)) {
  1095. Private.setMarkdownHeader(cells.get(index), level);
  1096. }
  1097. });
  1098. Private.changeCellType(notebook, 'markdown');
  1099. Private.handleState(notebook, state);
  1100. }
  1101. /**
  1102. * Trust the notebook after prompting the user.
  1103. *
  1104. * @param notebook - The target notebook widget.
  1105. *
  1106. * @returns a promise that resolves when the transaction is finished.
  1107. *
  1108. * #### Notes
  1109. * No dialog will be presented if the notebook is already trusted.
  1110. */
  1111. export function trust(notebook: Notebook): Promise<void> {
  1112. if (!notebook.model) {
  1113. return Promise.resolve();
  1114. }
  1115. // Do nothing if already trusted.
  1116. const cells = toArray(notebook.model.cells);
  1117. const trusted = cells.every(cell => cell.trusted);
  1118. if (trusted) {
  1119. return showDialog({
  1120. body: 'Notebook is already trusted',
  1121. buttons: [Dialog.okButton()]
  1122. }).then(() => undefined);
  1123. }
  1124. return showDialog({
  1125. body: TRUST_MESSAGE,
  1126. title: 'Trust this notebook?',
  1127. buttons: [Dialog.cancelButton(), Dialog.warnButton()]
  1128. }).then(result => {
  1129. if (result.button.accept) {
  1130. cells.forEach(cell => {
  1131. cell.trusted = true;
  1132. });
  1133. }
  1134. });
  1135. }
  1136. }
  1137. /**
  1138. * A namespace for private data.
  1139. */
  1140. namespace Private {
  1141. /**
  1142. * A signal that emits whenever a cell is run.
  1143. */
  1144. export const executed = new Signal<any, { notebook: Notebook; cell: Cell }>(
  1145. {}
  1146. );
  1147. /**
  1148. * The interface for a widget state.
  1149. */
  1150. export interface IState {
  1151. /**
  1152. * Whether the widget had focus.
  1153. */
  1154. wasFocused: boolean;
  1155. /**
  1156. * The active cell before the action.
  1157. */
  1158. activeCell: Cell;
  1159. }
  1160. /**
  1161. * Get the state of a widget before running an action.
  1162. */
  1163. export function getState(notebook: Notebook): IState {
  1164. return {
  1165. wasFocused: notebook.node.contains(document.activeElement),
  1166. activeCell: notebook.activeCell
  1167. };
  1168. }
  1169. /**
  1170. * Handle the state of a widget after running an action.
  1171. */
  1172. export function handleState(
  1173. notebook: Notebook,
  1174. state: IState,
  1175. scrollIfNeeded = false
  1176. ): void {
  1177. const { activeCell, node } = notebook;
  1178. if (state.wasFocused || notebook.mode === 'edit') {
  1179. notebook.activate();
  1180. }
  1181. if (scrollIfNeeded) {
  1182. ElementExt.scrollIntoViewIfNeeded(node, activeCell.node);
  1183. }
  1184. }
  1185. /**
  1186. * Handle the state of a widget after running a run action.
  1187. */
  1188. export function handleRunState(
  1189. notebook: Notebook,
  1190. state: IState,
  1191. scroll = false
  1192. ): void {
  1193. if (state.wasFocused || notebook.mode === 'edit') {
  1194. notebook.activate();
  1195. }
  1196. if (scroll) {
  1197. // Scroll to the top of the previous active cell output.
  1198. const rect = state.activeCell.inputArea.node.getBoundingClientRect();
  1199. notebook.scrollToPosition(rect.bottom, 45);
  1200. }
  1201. }
  1202. /**
  1203. * Clone a cell model.
  1204. */
  1205. export function cloneCell(
  1206. model: INotebookModel,
  1207. cell: ICellModel
  1208. ): ICellModel {
  1209. switch (cell.type) {
  1210. case 'code':
  1211. // TODO why isn't modeldb or id passed here?
  1212. return model.contentFactory.createCodeCell({ cell: cell.toJSON() });
  1213. case 'markdown':
  1214. // TODO why isn't modeldb or id passed here?
  1215. return model.contentFactory.createMarkdownCell({ cell: cell.toJSON() });
  1216. default:
  1217. // TODO why isn't modeldb or id passed here?
  1218. return model.contentFactory.createRawCell({ cell: cell.toJSON() });
  1219. }
  1220. }
  1221. /**
  1222. * Run the selected cells.
  1223. */
  1224. export function runSelected(
  1225. notebook: Notebook,
  1226. session?: IClientSession
  1227. ): Promise<boolean> {
  1228. notebook.mode = 'command';
  1229. let lastIndex = notebook.activeCellIndex;
  1230. const selected = notebook.widgets.filter((child, index) => {
  1231. const active = notebook.isSelectedOrActive(child);
  1232. if (active) {
  1233. lastIndex = index;
  1234. }
  1235. return active;
  1236. });
  1237. notebook.activeCellIndex = lastIndex;
  1238. notebook.deselectAll();
  1239. return Promise.all(selected.map(child => runCell(notebook, child, session)))
  1240. .then(results => {
  1241. if (notebook.isDisposed) {
  1242. return false;
  1243. }
  1244. // Post an update request.
  1245. notebook.update();
  1246. return results.every(result => result);
  1247. })
  1248. .catch(reason => {
  1249. if (reason.message === 'KernelReplyNotOK') {
  1250. selected.map(cell => {
  1251. // Remove '*' prompt from cells that didn't execute
  1252. if (
  1253. cell.model.type === 'code' &&
  1254. (cell as CodeCell).model.executionCount == null
  1255. ) {
  1256. cell.setPrompt('');
  1257. }
  1258. });
  1259. } else {
  1260. throw reason;
  1261. }
  1262. notebook.update();
  1263. return false;
  1264. });
  1265. }
  1266. /**
  1267. * Run a cell.
  1268. */
  1269. function runCell(
  1270. notebook: Notebook,
  1271. cell: Cell,
  1272. session?: IClientSession
  1273. ): Promise<boolean> {
  1274. switch (cell.model.type) {
  1275. case 'markdown':
  1276. (cell as MarkdownCell).rendered = true;
  1277. cell.inputHidden = false;
  1278. executed.emit({ notebook, cell });
  1279. break;
  1280. case 'code':
  1281. if (session) {
  1282. return CodeCell.execute(cell as CodeCell, session, {
  1283. deletedCells: notebook.model.deletedCells
  1284. })
  1285. .then(reply => {
  1286. notebook.model.deletedCells.splice(
  1287. 0,
  1288. notebook.model.deletedCells.length
  1289. );
  1290. if (cell.isDisposed) {
  1291. return false;
  1292. }
  1293. if (!reply) {
  1294. return true;
  1295. }
  1296. if (reply.content.status === 'ok') {
  1297. const content = reply.content;
  1298. if (content.payload && content.payload.length) {
  1299. handlePayload(content, notebook, cell);
  1300. }
  1301. return true;
  1302. } else {
  1303. throw new Error('KernelReplyNotOK');
  1304. }
  1305. })
  1306. .catch(reason => {
  1307. if (reason.message !== 'Canceled') {
  1308. throw reason;
  1309. }
  1310. return false;
  1311. })
  1312. .then(ran => {
  1313. if (ran) {
  1314. executed.emit({ notebook, cell });
  1315. }
  1316. return ran;
  1317. });
  1318. }
  1319. (cell.model as ICodeCellModel).executionCount = null;
  1320. break;
  1321. default:
  1322. break;
  1323. }
  1324. return Promise.resolve(true);
  1325. }
  1326. /**
  1327. * Handle payloads from an execute reply.
  1328. *
  1329. * #### Notes
  1330. * Payloads are deprecated and there are no official interfaces for them in
  1331. * the kernel type definitions.
  1332. * See [Payloads (DEPRECATED)](https://jupyter-client.readthedocs.io/en/latest/messaging.html#payloads-deprecated).
  1333. */
  1334. function handlePayload(
  1335. content: KernelMessage.IExecuteReply,
  1336. notebook: Notebook,
  1337. cell: Cell
  1338. ) {
  1339. const setNextInput = content.payload.filter(i => {
  1340. return (i as any).source === 'set_next_input';
  1341. })[0];
  1342. if (!setNextInput) {
  1343. return;
  1344. }
  1345. const text = (setNextInput as any).text;
  1346. const replace = (setNextInput as any).replace;
  1347. if (replace) {
  1348. cell.model.value.text = text;
  1349. return;
  1350. }
  1351. // Create a new code cell and add as the next cell.
  1352. const newCell = notebook.model.contentFactory.createCodeCell({});
  1353. const cells = notebook.model.cells;
  1354. const index = ArrayExt.firstIndexOf(toArray(cells), cell.model);
  1355. newCell.value.text = text;
  1356. if (index === -1) {
  1357. cells.push(newCell);
  1358. } else {
  1359. cells.insert(index + 1, newCell);
  1360. }
  1361. }
  1362. /**
  1363. * Copy or cut the selected cell data to the application clipboard.
  1364. *
  1365. * @param notebook - The target notebook widget.
  1366. *
  1367. * @param cut - Whether to copy or cut.
  1368. */
  1369. export function copyOrCut(notebook: Notebook, cut: boolean): void {
  1370. if (!notebook.model || !notebook.activeCell) {
  1371. return;
  1372. }
  1373. const state = getState(notebook);
  1374. const clipboard = Clipboard.getInstance();
  1375. notebook.mode = 'command';
  1376. clipboard.clear();
  1377. let data = notebook.widgets
  1378. .filter(cell => notebook.isSelectedOrActive(cell))
  1379. .map(cell => cell.model.toJSON())
  1380. .map(cellJSON => {
  1381. if ((cellJSON.metadata as JSONObject).deletable !== undefined) {
  1382. delete (cellJSON.metadata as JSONObject).deletable;
  1383. }
  1384. return cellJSON;
  1385. });
  1386. clipboard.setData(JUPYTER_CELL_MIME, data);
  1387. if (cut) {
  1388. deleteCells(notebook);
  1389. } else {
  1390. notebook.deselectAll();
  1391. }
  1392. handleState(notebook, state);
  1393. }
  1394. /**
  1395. * Change the selected cell type(s).
  1396. *
  1397. * @param notebook - The target notebook widget.
  1398. *
  1399. * @param value - The target cell type.
  1400. *
  1401. * #### Notes
  1402. * It should preserve the widget mode.
  1403. * This action can be undone.
  1404. * The existing selection will be cleared.
  1405. * Any cells converted to markdown will be unrendered.
  1406. */
  1407. export function changeCellType(
  1408. notebook: Notebook,
  1409. value: nbformat.CellType
  1410. ): void {
  1411. const model = notebook.model;
  1412. const cells = model.cells;
  1413. cells.beginCompoundOperation();
  1414. notebook.widgets.forEach((child, index) => {
  1415. if (!notebook.isSelectedOrActive(child)) {
  1416. return;
  1417. }
  1418. if (child.model.type !== value) {
  1419. const cell = child.model.toJSON();
  1420. let newCell: ICellModel;
  1421. switch (value) {
  1422. case 'code':
  1423. newCell = model.contentFactory.createCodeCell({ cell });
  1424. break;
  1425. case 'markdown':
  1426. newCell = model.contentFactory.createMarkdownCell({ cell });
  1427. if (child.model.type === 'code') {
  1428. newCell.trusted = false;
  1429. }
  1430. break;
  1431. default:
  1432. newCell = model.contentFactory.createRawCell({ cell });
  1433. if (child.model.type === 'code') {
  1434. newCell.trusted = false;
  1435. }
  1436. }
  1437. cells.set(index, newCell);
  1438. }
  1439. if (value === 'markdown') {
  1440. // Fetch the new widget and unrender it.
  1441. child = notebook.widgets[index];
  1442. (child as MarkdownCell).rendered = false;
  1443. }
  1444. });
  1445. cells.endCompoundOperation();
  1446. notebook.deselectAll();
  1447. }
  1448. /**
  1449. * Delete the selected cells.
  1450. *
  1451. * @param notebook - The target notebook widget.
  1452. *
  1453. * #### Notes
  1454. * The cell after the last selected cell will be activated.
  1455. * If the last cell is deleted, then the previous one will be activated.
  1456. * It will add a code cell if all cells are deleted.
  1457. * This action can be undone.
  1458. */
  1459. export function deleteCells(notebook: Notebook): void {
  1460. const model = notebook.model;
  1461. const cells = model.cells;
  1462. const toDelete: number[] = [];
  1463. notebook.mode = 'command';
  1464. // Find the cells to delete.
  1465. notebook.widgets.forEach((child, index) => {
  1466. const deletable = child.model.metadata.get('deletable') !== false;
  1467. if (notebook.isSelectedOrActive(child) && deletable) {
  1468. toDelete.push(index);
  1469. notebook.model.deletedCells.push(child.model.id);
  1470. }
  1471. });
  1472. // If cells are not deletable, we may not have anything to delete.
  1473. if (toDelete.length > 0) {
  1474. // Delete the cells as one undo event.
  1475. cells.beginCompoundOperation();
  1476. // Delete cells in reverse order to maintain the correct indices.
  1477. toDelete.reverse().forEach(index => {
  1478. cells.remove(index);
  1479. });
  1480. // Add a new cell if the notebook is empty. This is done
  1481. // within the compound operation to make the deletion of
  1482. // a notebook's last cell undoable.
  1483. if (!cells.length) {
  1484. cells.push(
  1485. model.contentFactory.createCell(
  1486. notebook.notebookConfig.defaultCell,
  1487. {}
  1488. )
  1489. );
  1490. }
  1491. cells.endCompoundOperation();
  1492. // Select the *first* interior cell not deleted or the cell
  1493. // *after* the last selected cell.
  1494. // Note: The activeCellIndex is clamped to the available cells,
  1495. // so if the last cell is deleted the previous cell will be activated.
  1496. // The *first* index is the index of the last cell in the initial
  1497. // toDelete list due to the `reverse` operation above.
  1498. notebook.activeCellIndex = toDelete[0] - toDelete.length + 1;
  1499. }
  1500. // Deselect any remaining, undeletable cells. Do this even if we don't
  1501. // delete anything so that users are aware *something* happened.
  1502. notebook.deselectAll();
  1503. }
  1504. /**
  1505. * Set the markdown header level of a cell.
  1506. */
  1507. export function setMarkdownHeader(cell: ICellModel, level: number) {
  1508. // Remove existing header or leading white space.
  1509. let source = cell.value.text;
  1510. const regex = /^(#+\s*)|^(\s*)/;
  1511. const newHeader = Array(level + 1).join('#') + ' ';
  1512. const matches = regex.exec(source);
  1513. if (matches) {
  1514. source = source.slice(matches[0].length);
  1515. }
  1516. cell.value.text = newHeader + source;
  1517. }
  1518. }