widget.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IKernel, KernelMessage
  5. } from 'jupyter-js-services';
  6. import {
  7. loadModeByMIME
  8. } from '../../codemirror';
  9. import {
  10. RenderMime
  11. } from '../../rendermime';
  12. import {
  13. Message
  14. } from 'phosphor-messaging';
  15. import {
  16. PanelLayout
  17. } from 'phosphor-panel';
  18. import {
  19. IChangedArgs
  20. } from 'phosphor-properties';
  21. import {
  22. ISignal, Signal
  23. } from 'phosphor-signaling';
  24. import {
  25. Widget
  26. } from 'phosphor-widget';
  27. import {
  28. OutputAreaWidget
  29. } from '../output-area';
  30. import {
  31. IMetadataCursor
  32. } from '../common/metadata';
  33. import {
  34. CellEditorWidget
  35. } from './editor';
  36. import {
  37. ICellModel, ICodeCellModel, IMarkdownCellModel, IRawCellModel
  38. } from './model';
  39. /**
  40. * The class name added to cell widgets.
  41. */
  42. const CELL_CLASS = 'jp-Cell';
  43. /**
  44. * The class name added to input area widgets.
  45. */
  46. const INPUT_CLASS = 'jp-InputArea';
  47. /**
  48. * The class name added to the prompt area of cell.
  49. */
  50. const PROMPT_CLASS = 'jp-InputArea-prompt';
  51. /**
  52. * The class name added to the editor area of the cell.
  53. */
  54. const EDITOR_CLASS = 'jp-InputArea-editor';
  55. /**
  56. * The class name added to the cell when collapsed.
  57. */
  58. const COLLAPSED_CLASS = 'jp-mod-collapsed';
  59. /**
  60. * The class name added to the cell when readonly.
  61. */
  62. const READONLY_CLASS = 'jp-mod-readOnly';
  63. /**
  64. * The class name added to code cells.
  65. */
  66. const CODE_CELL_CLASS = 'jp-CodeCell';
  67. /**
  68. * The class name added to markdown cells.
  69. */
  70. const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell';
  71. /**
  72. * The class name added to the rendered markdown widget.
  73. */
  74. const MARKDOWN_CONTENT_CLASS = 'jp-MarkdownCell-content';
  75. /**
  76. * The class name added to raw cells.
  77. */
  78. const RAW_CELL_CLASS = 'jp-RawCell';
  79. /**
  80. * The class name added to a rendered markdown cell.
  81. */
  82. const RENDERED_CLASS = 'jp-mod-rendered';
  83. /**
  84. * The text applied to an empty markdown cell.
  85. */
  86. const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
  87. /**
  88. * A base cell widget.
  89. */
  90. export
  91. class BaseCellWidget extends Widget {
  92. /**
  93. * Construct a new base cell widget.
  94. */
  95. constructor(options: BaseCellWidget.IOptions = {}) {
  96. super();
  97. this.addClass(CELL_CLASS);
  98. this.layout = new PanelLayout();
  99. let renderer = options.renderer || BaseCellWidget.defaultRenderer;
  100. this._editor = renderer.createCellEditor({
  101. indentUnit: 4,
  102. readOnly: false,
  103. theme: 'default',
  104. extraKeys: {
  105. 'Cmd-Right': 'goLineRight',
  106. 'End': 'goLineRight',
  107. 'Cmd-Left': 'goLineLeft',
  108. 'Tab': 'indentMore',
  109. 'Shift-Tab' : 'indentLess',
  110. 'Cmd-Alt-[' : 'indentAuto',
  111. 'Ctrl-Alt-[' : 'indentAuto',
  112. 'Cmd-/' : 'toggleComment',
  113. 'Ctrl-/' : 'toggleComment',
  114. }
  115. });
  116. this._input = renderer.createInputArea(this._editor);
  117. (this.layout as PanelLayout).addChild(this._input);
  118. }
  119. /**
  120. * The model used by the widget.
  121. */
  122. get model(): ICellModel {
  123. return this._model;
  124. }
  125. set model(newValue: ICellModel) {
  126. if (!newValue && !this._model || newValue === this._model) {
  127. return;
  128. }
  129. let oldValue: ICellModel = this._model;
  130. this._model = newValue;
  131. // Trigger private, protected, and public updates.
  132. this._onModelChanged(oldValue, newValue);
  133. this.onModelChanged(oldValue, newValue);
  134. this.modelChanged.emit(void 0);
  135. }
  136. /**
  137. * A signal emitted when the model of the cell changes.
  138. */
  139. get modelChanged(): ISignal<BaseCellWidget, void> {
  140. return Private.modelChangedSignal.bind(this);
  141. }
  142. /**
  143. * Get the editor widget used by the cell.
  144. *
  145. * #### Notes
  146. * This is a ready-only property.
  147. */
  148. get editor(): CellEditorWidget {
  149. return this._editor;
  150. }
  151. /**
  152. * The mimetype used by the cell.
  153. */
  154. get mimetype(): string {
  155. return this._mimetype;
  156. }
  157. set mimetype(value: string) {
  158. if (!value) {
  159. return;
  160. }
  161. if (this._mimetype === value) {
  162. return;
  163. }
  164. this._mimetype = value;
  165. loadModeByMIME(this.editor.editor, value);
  166. }
  167. /**
  168. * The read only state of the cell.
  169. */
  170. get readOnly(): boolean {
  171. return this._readOnly;
  172. }
  173. set readOnly(value: boolean) {
  174. if (value === this._readOnly) {
  175. return;
  176. }
  177. this._readOnly = value;
  178. this.update();
  179. }
  180. /**
  181. * The trusted state of the cell.
  182. */
  183. get trusted(): boolean {
  184. return this._trusted;
  185. }
  186. set trusted(value: boolean) {
  187. if (!this._model) {
  188. return;
  189. }
  190. this._trustedCursor.setValue(value);
  191. this._trusted = value;
  192. }
  193. /**
  194. * Focus the widget.
  195. */
  196. focus(): void {
  197. this.editor.editor.focus();
  198. }
  199. /**
  200. * Set the prompt for the widget.
  201. */
  202. setPrompt(value: string): void {
  203. this._input.setPrompt(value);
  204. }
  205. /**
  206. * Toggle whether the input area is shown.
  207. */
  208. toggleInput(value: boolean): void {
  209. if (value) {
  210. this._input.show();
  211. this.focus();
  212. } else {
  213. this._input.hide();
  214. }
  215. }
  216. /**
  217. * Dispose of the resources held by the widget.
  218. */
  219. dispose() {
  220. // Do nothing if already disposed.
  221. if (this.isDisposed) {
  222. return;
  223. }
  224. this._model = null;
  225. this._input = null;
  226. this._editor = null;
  227. this._trustedCursor = null;
  228. super.dispose();
  229. }
  230. /**
  231. * Handle `after-attach` messages.
  232. */
  233. protected onAfterAttach(msg: Message): void {
  234. this.update();
  235. }
  236. /**
  237. * Handle `update_request` messages.
  238. */
  239. protected onUpdateRequest(msg: Message): void {
  240. if (!this._model) {
  241. return;
  242. }
  243. // Handle read only state.
  244. let option = this._readOnly ? 'nocursor' : false;
  245. this.editor.editor.setOption('readOnly', option);
  246. this.toggleClass(READONLY_CLASS, this._readOnly);
  247. }
  248. /**
  249. * Handle changes in the model.
  250. *
  251. * #### Notes
  252. * Subclasses may reimplement this method as needed.
  253. */
  254. protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
  255. }
  256. /**
  257. * Handle changes in the model.
  258. */
  259. protected onMetadataChanged(model: ICellModel, args: IChangedArgs<any>): void {
  260. switch (args.name) {
  261. case 'trusted':
  262. this._trusted = !!this._trustedCursor.getValue();
  263. this.update();
  264. break;
  265. default:
  266. break;
  267. }
  268. }
  269. /**
  270. * Handle a new model.
  271. *
  272. * #### Notes
  273. * This method is called after the model change has been handled
  274. * internally and before the `modelChanged` signal is emitted.
  275. * The default implementation is a no-op.
  276. */
  277. protected onModelChanged(oldValue: ICellModel, newValue: ICellModel): void { }
  278. private _onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
  279. // If the model is being replaced, disconnect the old signal handler.
  280. if (oldValue) {
  281. oldValue.stateChanged.disconnect(this.onModelStateChanged, this);
  282. oldValue.metadataChanged.disconnect(this.onMetadataChanged, this);
  283. }
  284. // Reset the editor model and set its mode to be the default MIME type.
  285. this._editor.model = this._model;
  286. loadModeByMIME(this._editor.editor, this._mimetype);
  287. // Handle trusted cursor.
  288. this._trustedCursor = this._model.getMetadata('trusted');
  289. this._trusted = !!this._trustedCursor.getValue();
  290. // Connect signal handlers.
  291. this._model.metadataChanged.connect(this.onMetadataChanged, this);
  292. this._model.stateChanged.connect(this.onModelStateChanged, this);
  293. }
  294. private _input: InputAreaWidget = null;
  295. private _editor: CellEditorWidget = null;
  296. private _model: ICellModel = null;
  297. private _mimetype = 'text/plain';
  298. private _readOnly = false;
  299. private _trustedCursor: IMetadataCursor = null;
  300. private _trusted = false;
  301. }
  302. /**
  303. * The namespace for the `BaseCellWidget` class statics.
  304. */
  305. export
  306. namespace BaseCellWidget {
  307. /**
  308. * An options object for initializing a base cell widget.
  309. */
  310. export
  311. interface IOptions {
  312. /**
  313. * A renderer for creating cell widgets.
  314. *
  315. * The default is a shared renderer instance.
  316. */
  317. renderer?: IRenderer;
  318. }
  319. /**
  320. * A renderer for creating cell widgets.
  321. */
  322. export
  323. interface IRenderer {
  324. /**
  325. * Create a new cell editor for the widget.
  326. */
  327. createCellEditor(options?: CodeMirror.EditorConfiguration): CellEditorWidget;
  328. /**
  329. * Create a new input area for the widget.
  330. */
  331. createInputArea(editor: CellEditorWidget): InputAreaWidget;
  332. }
  333. /**
  334. * The default implementation of an `IRenderer`.
  335. */
  336. export
  337. class Renderer implements IRenderer {
  338. /**
  339. * Create a new cell editor for the widget.
  340. */
  341. createCellEditor(options?: CodeMirror.EditorConfiguration): CellEditorWidget {
  342. return new CellEditorWidget(options);
  343. }
  344. /**
  345. * Create a new input area for the widget.
  346. */
  347. createInputArea(editor: CellEditorWidget): InputAreaWidget {
  348. return new InputAreaWidget(editor);
  349. }
  350. }
  351. /**
  352. * The default `IRenderer` instance.
  353. */
  354. export
  355. const defaultRenderer = new Renderer();
  356. }
  357. /**
  358. * A widget for a code cell.
  359. */
  360. export
  361. class CodeCellWidget extends BaseCellWidget {
  362. /**
  363. * Construct a code cell widget.
  364. */
  365. constructor(options: CodeCellWidget.IOptions) {
  366. super(options);
  367. this.editor.editor.setOption('matchBrackets', true);
  368. this.editor.editor.setOption('autoCloseBrackets', true);
  369. this.addClass(CODE_CELL_CLASS);
  370. this._rendermime = options.rendermime;
  371. this._renderer = options.renderer || CodeCellWidget.defaultRenderer;
  372. }
  373. /**
  374. * The model used by the widget.
  375. */
  376. model: ICodeCellModel;
  377. /**
  378. * Dispose of the resources used by the widget.
  379. */
  380. dispose(): void {
  381. if (this.isDisposed) {
  382. return;
  383. }
  384. this._collapsedCursor = null;
  385. this._scrolledCursor = null;
  386. this._output = null;
  387. super.dispose();
  388. }
  389. /**
  390. * Execute the cell given a kernel.
  391. */
  392. execute(kernel: IKernel): Promise<KernelMessage.IExecuteReplyMsg> {
  393. let model = this.model as ICodeCellModel;
  394. let code = model.source;
  395. if (!code.trim()) {
  396. model.executionCount = null;
  397. return Promise.resolve(null);
  398. }
  399. model.executionCount = null;
  400. this.setPrompt('*');
  401. this.trusted = true;
  402. let outputs = model.outputs;
  403. return outputs.execute(code, kernel).then(reply => {
  404. model.executionCount = reply.content.execution_count;
  405. return reply;
  406. });
  407. }
  408. /**
  409. * Handle `update_request` messages.
  410. */
  411. protected onUpdateRequest(msg: Message): void {
  412. if (this._collapsedCursor) {
  413. this.toggleClass(COLLAPSED_CLASS, this._collapsedCursor.getValue());
  414. }
  415. if (this._output) {
  416. // TODO: handle scrolled state.
  417. this._output.trusted = this.trusted;
  418. }
  419. super.onUpdateRequest(msg);
  420. }
  421. /**
  422. * Handle the widget receiving a new model.
  423. */
  424. protected onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
  425. let model = newValue as ICodeCellModel;
  426. let renderer = this._renderer;
  427. if (!this._output) {
  428. this._output = renderer.createOutputArea(this._rendermime);
  429. (this.layout as PanelLayout).addChild(this._output);
  430. }
  431. this._output.model = model.outputs;
  432. this._output.trusted = this.trusted;
  433. this._collapsedCursor = model.getMetadata('collapsed');
  434. this._scrolledCursor = model.getMetadata('scrolled');
  435. this.setPrompt(`${model.executionCount}`);
  436. }
  437. /**
  438. * Handle changes in the model.
  439. */
  440. protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
  441. switch (args.name) {
  442. case 'executionCount':
  443. this.setPrompt(`${(model as ICodeCellModel).executionCount}`);
  444. break;
  445. default:
  446. break;
  447. }
  448. super.onModelStateChanged(model, args);
  449. }
  450. /**
  451. * Handle changes in the metadata.
  452. */
  453. protected onMetadataChanged(model: ICellModel, args: IChangedArgs<any>): void {
  454. switch (args.name) {
  455. case 'collapsed':
  456. case 'scrolled':
  457. this.update();
  458. break;
  459. default:
  460. break;
  461. }
  462. super.onMetadataChanged(model, args);
  463. }
  464. private _renderer: CodeCellWidget.IRenderer;
  465. private _rendermime: RenderMime = null;
  466. private _output: OutputAreaWidget = null;
  467. private _collapsedCursor: IMetadataCursor = null;
  468. private _scrolledCursor: IMetadataCursor = null;
  469. }
  470. /**
  471. * The namespace for the `CodeCellWidget` class statics.
  472. */
  473. export
  474. namespace CodeCellWidget {
  475. /**
  476. * An options object for initializing a base cell widget.
  477. */
  478. export
  479. interface IOptions {
  480. /**
  481. * A renderer for creating cell widgets.
  482. *
  483. * The default is a shared renderer instance.
  484. */
  485. renderer?: IRenderer;
  486. /**
  487. * The mime renderer for the cell widget.
  488. */
  489. rendermime: RenderMime;
  490. }
  491. /**
  492. * A renderer for creating code cell widgets.
  493. */
  494. export
  495. interface IRenderer extends BaseCellWidget.IRenderer {
  496. /**
  497. * Create a new output area for the widget.
  498. */
  499. createOutputArea(rendermime: RenderMime): OutputAreaWidget;
  500. }
  501. /**
  502. * The default implementation of an `IRenderer`.
  503. */
  504. export
  505. class Renderer extends BaseCellWidget.Renderer implements IRenderer {
  506. /**
  507. * Create an output area widget.
  508. */
  509. createOutputArea(rendermime: RenderMime): OutputAreaWidget {
  510. return new OutputAreaWidget({ rendermime });
  511. }
  512. }
  513. /**
  514. * The default `IRenderer` instance.
  515. */
  516. export
  517. const defaultRenderer = new Renderer();
  518. }
  519. /**
  520. * A widget for a Markdown cell.
  521. *
  522. * #### Notes
  523. * Things get complicated if we want the rendered text to update
  524. * any time the text changes, the text editor model changes,
  525. * or the input area model changes. We don't support automatically
  526. * updating the rendered text in all of these cases.
  527. */
  528. export
  529. class MarkdownCellWidget extends BaseCellWidget {
  530. /**
  531. * Construct a Markdown cell widget.
  532. */
  533. constructor(options: MarkdownCellWidget.IOptions) {
  534. super(options);
  535. this.addClass(MARKDOWN_CELL_CLASS);
  536. // Insist on the Github-flavored markdown mode.
  537. this.mimetype = 'text/x-ipythongfm';
  538. this._rendermime = options.rendermime;
  539. this._markdownWidget = new Widget();
  540. this._markdownWidget.addClass(MARKDOWN_CONTENT_CLASS);
  541. (this.layout as PanelLayout).addChild(this._markdownWidget);
  542. // Turn on line wrapping for markdown cells.
  543. this.editor.editor.setOption('lineWrapping', true);
  544. }
  545. /**
  546. * The model used by the widget.
  547. */
  548. model: IMarkdownCellModel;
  549. /**
  550. * Whether the cell is rendered.
  551. */
  552. get rendered(): boolean {
  553. return this._rendered;
  554. }
  555. set rendered(value: boolean) {
  556. if (value === this._rendered) {
  557. return;
  558. }
  559. this._rendered = value;
  560. this.update();
  561. }
  562. /**
  563. * Dispose of the resource held by the widget.
  564. */
  565. dispose(): void {
  566. if (this.isDisposed) {
  567. return;
  568. }
  569. this._markdownWidget = null;
  570. super.dispose();
  571. }
  572. /**
  573. * Handle `update_request` messages.
  574. */
  575. protected onUpdateRequest(msg: Message): void {
  576. let model = this.model;
  577. if (this.rendered) {
  578. let text = model && model.source || DEFAULT_MARKDOWN_TEXT;
  579. // Do not re-render if the text has not changed.
  580. if (text !== this._prev) {
  581. let bundle: RenderMime.MimeMap<string> = { 'text/markdown': text };
  582. this._markdownWidget.dispose();
  583. this._rendermime.render(bundle, this.trusted).then(widget => {
  584. this._markdownWidget = widget || new Widget();
  585. this._markdownWidget.addClass(MARKDOWN_CONTENT_CLASS);
  586. (this.layout as PanelLayout).addChild(this._markdownWidget);
  587. }).catch(err => {
  588. console.error(err);
  589. });
  590. } else {
  591. this._markdownWidget.show();
  592. }
  593. this._prev = text;
  594. this.toggleInput(false);
  595. this.addClass(RENDERED_CLASS);
  596. } else {
  597. this._markdownWidget.hide();
  598. this.toggleInput(true);
  599. this.removeClass(RENDERED_CLASS);
  600. }
  601. super.onUpdateRequest(msg);
  602. }
  603. private _rendermime: RenderMime = null;
  604. private _markdownWidget: Widget = null;
  605. private _rendered = true;
  606. private _prev = '';
  607. }
  608. /**
  609. * The namespace for the `CodeCellWidget` class statics.
  610. */
  611. export
  612. namespace MarkdownCellWidget {
  613. /**
  614. * An options object for initializing a base cell widget.
  615. */
  616. export
  617. interface IOptions {
  618. /**
  619. * A renderer for creating cell widgets.
  620. *
  621. * The default is a shared renderer instance.
  622. */
  623. renderer?: BaseCellWidget.IRenderer;
  624. /**
  625. * The mime renderer for the cell widget.
  626. */
  627. rendermime: RenderMime;
  628. }
  629. }
  630. /**
  631. * A widget for a raw cell.
  632. */
  633. export
  634. class RawCellWidget extends BaseCellWidget {
  635. /**
  636. * Construct a raw cell widget.
  637. */
  638. constructor(options: BaseCellWidget.IOptions = {}) {
  639. super(options);
  640. this.addClass(RAW_CELL_CLASS);
  641. // Turn on line wrapping for raw cells.
  642. this.editor.editor.setOption('lineWrapping', true);
  643. }
  644. /**
  645. * The model used by the widget.
  646. */
  647. model: IRawCellModel;
  648. }
  649. /**
  650. * An input area widget, which hosts a prompt and an editor widget.
  651. */
  652. export
  653. class InputAreaWidget extends Widget {
  654. /**
  655. * Construct an input area widget.
  656. */
  657. constructor(editor: CellEditorWidget) {
  658. super();
  659. this.addClass(INPUT_CLASS);
  660. editor.addClass(EDITOR_CLASS);
  661. this.layout = new PanelLayout();
  662. let prompt = new Widget();
  663. prompt.addClass(PROMPT_CLASS);
  664. let layout = this.layout as PanelLayout;
  665. layout.addChild(prompt);
  666. layout.addChild(editor);
  667. }
  668. /**
  669. * Set the prompt of the input area.
  670. */
  671. setPrompt(value: string): void {
  672. let prompt = (this.layout as PanelLayout).childAt(0);
  673. if (value === 'null') {
  674. value = ' ';
  675. }
  676. let text = `In [${value || ' '}]:`;
  677. prompt.node.textContent = text;
  678. }
  679. }
  680. /**
  681. * A namespace for private data.
  682. */
  683. namespace Private {
  684. /**
  685. * A signal emitted when the model changes on a cell widget.
  686. */
  687. export
  688. const modelChangedSignal = new Signal<BaseCellWidget, void>();
  689. }