UNPKG

@jupyterlab/cells

Version:
1,420 lines (1,419 loc) 72.8 kB
/* ----------------------------------------------------------------------------- | Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ import { EditorView } from '@codemirror/view'; import { ElementExt } from '@lumino/domutils'; import { AttachmentsResolver } from '@jupyterlab/attachments'; import { DOMUtils } from '@jupyterlab/apputils'; import { ActivityMonitor, URLExt } from '@jupyterlab/coreutils'; import { OutputArea, OutputPrompt, SimplifiedOutputArea, Stdin } from '@jupyterlab/outputarea'; import { imageRendererFactory, MimeModel } from '@jupyterlab/rendermime'; import { TableOfContentsUtils } from '@jupyterlab/toc'; import { nullTranslator } from '@jupyterlab/translation'; import { addIcon, collapseIcon, expandIcon } from '@jupyterlab/ui-components'; import { PromiseDelegate, UUID } from '@lumino/coreutils'; import { some } from '@lumino/algorithm'; import { MessageLoop } from '@lumino/messaging'; import { Debouncer } from '@lumino/polling'; import { Signal } from '@lumino/signaling'; import { Panel, PanelLayout, Widget } from '@lumino/widgets'; import { InputCollapser, OutputCollapser } from './collapser'; import { CellFooter, CellHeader } from './headerfooter'; import { InputArea, InputPrompt } from './inputarea'; import { InputPlaceholder, OutputPlaceholder } from './placeholder'; import { ResizeHandle } from './resizeHandle'; /** * The CSS class added to cell widgets. */ const CELL_CLASS = 'jp-Cell'; /** * The CSS class added to the cell header. */ const CELL_HEADER_CLASS = 'jp-Cell-header'; /** * The CSS class added to the cell footer. */ const CELL_FOOTER_CLASS = 'jp-Cell-footer'; /** * The CSS class added to the cell input wrapper. */ const CELL_INPUT_WRAPPER_CLASS = 'jp-Cell-inputWrapper'; /** * The CSS class added to the cell output wrapper. */ const CELL_OUTPUT_WRAPPER_CLASS = 'jp-Cell-outputWrapper'; /** * The CSS class added to the cell input area. */ const CELL_INPUT_AREA_CLASS = 'jp-Cell-inputArea'; /** * The CSS class added to the cell output area. */ const CELL_OUTPUT_AREA_CLASS = 'jp-Cell-outputArea'; /** * The CSS class added to the cell input collapser. */ const CELL_INPUT_COLLAPSER_CLASS = 'jp-Cell-inputCollapser'; /** * The CSS class added to the cell output collapser. */ const CELL_OUTPUT_COLLAPSER_CLASS = 'jp-Cell-outputCollapser'; /** * The class name added to the cell when dirty. */ const DIRTY_CLASS = 'jp-mod-dirty'; /** * The class name added to code cells. */ const CODE_CELL_CLASS = 'jp-CodeCell'; /** * The class name added to markdown cells. */ const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell'; /** * The class name added to rendered markdown output widgets. */ const MARKDOWN_OUTPUT_CLASS = 'jp-MarkdownOutput'; const MARKDOWN_HEADING_COLLAPSED = 'jp-MarkdownHeadingCollapsed'; const HEADING_COLLAPSER_CLASS = 'jp-collapseHeadingButton'; const SHOW_HIDDEN_CELLS_CLASS = 'jp-showHiddenCellsButton'; /** * The class name added to raw cells. */ const RAW_CELL_CLASS = 'jp-RawCell'; /** * The class name added to a rendered input area. */ const RENDERED_CLASS = 'jp-mod-rendered'; const NO_OUTPUTS_CLASS = 'jp-mod-noOutputs'; /** * The text applied to an empty markdown cell. */ const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $'; /** * The timeout to wait for change activity to have ceased before rendering. */ const RENDER_TIMEOUT = 1000; /** * The mime type for a rich contents drag object. */ const CONTENTS_MIME_RICH = 'application/x-jupyter-icontentsrich'; /** **************************************************************************** * Cell ******************************************************************************/ /** * A base cell widget. */ export class Cell extends Widget { /** * Construct a new base cell widget. */ constructor(options) { var _a, _b, _c, _d; super(); this.prompt = ''; this._displayChanged = new Signal(this); this._scrollRequested = new Signal(this); /** * Editor extension emitting `scrollRequested` signal on scroll. * * Scrolling within editor will be prevented when a cell is out out viewport. * Windowed containers including cells should listen to the scroll request * signal and invoke the `scrollWithinCell()` callback after scrolling the cell * back into the view (and after updating the `inViewport` property). */ this._scrollHandlerExtension = EditorView.scrollHandler.of((view, range, options) => { // When cell is in the viewport we can scroll within the editor immediately. // When cell is out of viewport, the windowed container needs to first // scroll the cell into the viewport (otherwise CodeMirror is unable to // calculate the correct scroll delta) before invoking scrolling in editor. const inWindowedContainer = this._inViewport !== null; const preventDefault = inWindowedContainer && !this._inViewport; this._scrollRequested.emit({ defaultPrevented: preventDefault, scrollWithinCell: () => { view.dispatch({ effects: EditorView.scrollIntoView(range, options) }); } }); return preventDefault; }); this._editorConfig = {}; this._editorExtensions = []; this._inputHidden = false; this._inViewportChanged = new Signal(this); this._readOnly = false; this._ready = new PromiseDelegate(); this._resizeDebouncer = new Debouncer(() => { this._displayChanged.emit(); }, 0); this._syncCollapse = false; this._syncEditable = false; this.addClass(CELL_CLASS); const model = (this._model = options.model); this.contentFactory = options.contentFactory; this.layout = (_a = options.layout) !== null && _a !== void 0 ? _a : new PanelLayout(); // Set up translator for aria labels this.translator = (_b = options.translator) !== null && _b !== void 0 ? _b : nullTranslator; // For cells disable searching with CodeMirror search panel. this._editorConfig = { searchWithCM: false, ...options.editorConfig }; this._editorExtensions = (_c = options.editorExtensions) !== null && _c !== void 0 ? _c : []; this._editorExtensions.push(this._scrollHandlerExtension); this._placeholder = true; this._inViewport = null; this.placeholder = (_d = options.placeholder) !== null && _d !== void 0 ? _d : true; model.metadataChanged.connect(this.onMetadataChanged, this); } /** * Initialize view state from model. * * #### Notes * Should be called after construction. For convenience, returns this, so it * can be chained in the construction, like `new Foo().initializeState();` */ initializeState() { this.loadCollapseState(); this.loadEditableState(); return this; } /** * Signal to indicate that widget has changed visibly (in size, in type, etc) */ get displayChanged() { return this._displayChanged; } /** * Whether the cell is in viewport or not. * * #### Notes * This property is managed by the windowed container which holds the cell. * When a cell is not in a windowed container, it always returns `false`, * but this may change in the future major version. */ get inViewport() { var _a; return (_a = this._inViewport) !== null && _a !== void 0 ? _a : false; } set inViewport(v) { if (this._inViewport !== v) { this._inViewport = v; this._inViewportChanged.emit(this._inViewport); } } /** * Will emit true just after the node is attached to the DOM * Will emit false just before the node is detached of the DOM */ get inViewportChanged() { return this._inViewportChanged; } /** * Whether the cell is a placeholder not yet fully rendered or not. */ get placeholder() { return this._placeholder; } set placeholder(v) { if (this._placeholder !== v && v === false) { this.initializeDOM(); this._placeholder = v; this._ready.resolve(); } } /** * Get the prompt node used by the cell. */ get promptNode() { if (this.placeholder) { return null; } if (!this._inputHidden) { return this._input.promptNode; } else { return this._inputPlaceholder.node .firstElementChild; } } /** * Get the CodeEditorWrapper used by the cell. */ get editorWidget() { var _a, _b; return (_b = (_a = this._input) === null || _a === void 0 ? void 0 : _a.editorWidget) !== null && _b !== void 0 ? _b : null; } /** * Get the CodeEditor used by the cell. */ get editor() { var _a, _b; return (_b = (_a = this._input) === null || _a === void 0 ? void 0 : _a.editor) !== null && _b !== void 0 ? _b : null; } /** * Editor configuration */ get editorConfig() { return this._editorConfig; } /** * Cell headings */ get headings() { return new Array(); } /** * Get the model used by the cell. */ get model() { return this._model; } /** * Get the input area for the cell. */ get inputArea() { return this._input; } /** * The read only state of the cell. */ get readOnly() { return this._readOnly; } set readOnly(value) { if (value === this._readOnly) { return; } this._readOnly = value; if (this.syncEditable) { this.saveEditableState(); } this.update(); } /** * Whether the cell is a placeholder that defer rendering * * #### Notes * You can wait for the promise `Cell.ready` to wait for the * cell to be rendered. */ isPlaceholder() { return this.placeholder; } /** * Save view editable state to model */ saveEditableState() { const { sharedModel } = this.model; const current = sharedModel.getMetadata('editable'); if ((this.readOnly && current === false) || (!this.readOnly && current === undefined)) { return; } if (this.readOnly) { sharedModel.setMetadata('editable', false); } else { sharedModel.deleteMetadata('editable'); } } /** * Load view editable state from model. */ loadEditableState() { this.readOnly = this.model.sharedModel.getMetadata('editable') === false; } /** * A promise that resolves when the widget renders for the first time. */ get ready() { return this._ready.promise; } /** * Set the prompt for the widget. * @deprecated - set the `executionState` on the model instead. */ setPrompt(value) { return this._setPrompt(value); } /** * Set the prompt for the widget. * * Note: this method is protected because it is needed in the CodeCell subclass, * but it cannot be defined there because input is private to Cell class. */ _setPrompt(value) { var _a; this.prompt = value; (_a = this._input) === null || _a === void 0 ? void 0 : _a.setPrompt(value); } /** * The view state of input being hidden. */ get inputHidden() { return this._inputHidden; } set inputHidden(value) { var _a; if (this._inputHidden === value) { return; } if (!this.placeholder) { const layout = this._inputWrapper.layout; if (value) { this._input.parent = null; if (this._inputPlaceholder) { this._inputPlaceholder.text = (_a = this.model.sharedModel .getSource() .split('\n')) === null || _a === void 0 ? void 0 : _a[0]; } layout.addWidget(this._inputPlaceholder); } else { this._inputPlaceholder.parent = null; layout.addWidget(this._input); } } this._inputHidden = value; if (this.syncCollapse) { this.saveCollapseState(); } this.handleInputHidden(value); } /** * Save view collapse state to model */ saveCollapseState() { const jupyter = { ...this.model.getMetadata('jupyter') }; if ((this.inputHidden && jupyter.source_hidden === true) || (!this.inputHidden && jupyter.source_hidden === undefined)) { return; } if (this.inputHidden) { jupyter.source_hidden = true; } else { delete jupyter.source_hidden; } if (Object.keys(jupyter).length === 0) { this.model.deleteMetadata('jupyter'); } else { this.model.setMetadata('jupyter', jupyter); } } /** * Revert view collapse state from model. */ loadCollapseState() { var _a; const jupyter = (_a = this.model.getMetadata('jupyter')) !== null && _a !== void 0 ? _a : {}; this.inputHidden = !!jupyter.source_hidden; } /** * Handle the input being hidden. * * #### Notes * This is called by the `inputHidden` setter so that subclasses * can perform actions upon the input being hidden without accessing * private state. */ handleInputHidden(value) { return; } /** * Whether to sync the collapse state to the cell model. */ get syncCollapse() { return this._syncCollapse; } set syncCollapse(value) { if (this._syncCollapse === value) { return; } this._syncCollapse = value; if (value) { this.loadCollapseState(); } } /** * Whether to sync the editable state to the cell model. */ get syncEditable() { return this._syncEditable; } set syncEditable(value) { if (this._syncEditable === value) { return; } this._syncEditable = value; if (value) { this.loadEditableState(); } } /** * Clone the cell, using the same model. */ clone() { const constructor = this.constructor; return new constructor({ model: this.model, contentFactory: this.contentFactory, placeholder: false, translator: this.translator }); } /** * Dispose of the resources held by the widget. */ dispose() { // Do nothing if already disposed. if (this.isDisposed) { return; } this._resizeDebouncer.dispose(); this._input = null; this._model = null; this._inputWrapper = null; this._inputPlaceholder = null; super.dispose(); } /** * Update the editor configuration with the partial provided dictionary. * * @param v Partial editor configuration */ updateEditorConfig(v) { this._editorConfig = { ...this._editorConfig, ...v }; if (this.editor) { this.editor.setBaseOptions(this._editorConfig); } } /** * Signal emitted when cell requests scrolling to its element. */ get scrollRequested() { return this._scrollRequested; } /** * Create children widgets. */ initializeDOM() { if (!this.placeholder) { return; } const contentFactory = this.contentFactory; const model = this._model; // Header const header = contentFactory.createCellHeader(); header.addClass(CELL_HEADER_CLASS); this.layout.addWidget(header); // Input const inputWrapper = (this._inputWrapper = new Panel()); inputWrapper.addClass(CELL_INPUT_WRAPPER_CLASS); const inputCollapser = new InputCollapser(); inputCollapser.addClass(CELL_INPUT_COLLAPSER_CLASS); const input = (this._input = new InputArea({ model, contentFactory, editorOptions: this.getEditorOptions() })); input.addClass(CELL_INPUT_AREA_CLASS); inputWrapper.addWidget(inputCollapser); inputWrapper.addWidget(input); this.layout.addWidget(inputWrapper); this._inputPlaceholder = new InputPlaceholder({ callback: () => { this.inputHidden = !this.inputHidden; }, text: input.model.sharedModel.getSource().split('\n')[0], translator: this.translator }); input.model.contentChanged.connect((sender, args) => { var _a; if (this._inputPlaceholder && this.inputHidden) { this._inputPlaceholder.text = (_a = sender.sharedModel .getSource() .split('\n')) === null || _a === void 0 ? void 0 : _a[0]; } }); if (this.inputHidden) { input.parent = null; inputWrapper.layout.addWidget(this._inputPlaceholder); } // Footer const footer = this.contentFactory.createCellFooter(); footer.addClass(CELL_FOOTER_CLASS); this.layout.addWidget(footer); } /** * Get the editor options at initialization. * * @returns Editor options */ getEditorOptions() { return { config: this.editorConfig, extensions: this._editorExtensions }; } /** * Handle `before-attach` messages. */ onBeforeAttach(msg) { if (this.placeholder) { this.placeholder = false; } } /** * Handle `after-attach` messages. */ onAfterAttach(msg) { this.update(); } /** * Handle `'activate-request'` messages. */ onActivateRequest(msg) { var _a; (_a = this.editor) === null || _a === void 0 ? void 0 : _a.focus(); } /** * Handle `resize` messages. */ onResize(msg) { void this._resizeDebouncer.invoke(); } /** * Handle `update-request` messages. */ onUpdateRequest(msg) { var _a, _b; if (!this._model) { return; } // Handle read only state. if (((_a = this.editor) === null || _a === void 0 ? void 0 : _a.getOption('readOnly')) !== this._readOnly) { (_b = this.editor) === null || _b === void 0 ? void 0 : _b.setOption('readOnly', this._readOnly); } } onContentChanged() { var _a; if (this.inputHidden && this._inputPlaceholder) { this._inputPlaceholder.text = (_a = this.model.sharedModel .getSource() .split('\n')) === null || _a === void 0 ? void 0 : _a[0]; } } /** * Handle changes in the metadata. */ onMetadataChanged(model, args) { switch (args.key) { case 'jupyter': if (this.syncCollapse) { this.loadCollapseState(); } break; case 'editable': if (this.syncEditable) { this.loadEditableState(); } break; default: break; } } } /** * The namespace for the `Cell` class statics. */ (function (Cell) { /** * Type of headings */ let HeadingType; (function (HeadingType) { /** * Heading from HTML output */ HeadingType[HeadingType["HTML"] = 0] = "HTML"; /** * Heading from Markdown cell or Markdown output */ HeadingType[HeadingType["Markdown"] = 1] = "Markdown"; })(HeadingType = Cell.HeadingType || (Cell.HeadingType = {})); /** * The default implementation of an `IContentFactory`. * * This includes a CodeMirror editor factory to make it easy to use out of the box. */ class ContentFactory { /** * Create a content factory for a cell. */ constructor(options) { this._editorFactory = options.editorFactory; } /** * The readonly editor factory that create code editors */ get editorFactory() { return this._editorFactory; } /** * Create a new cell header for the parent widget. */ createCellHeader() { return new CellHeader(); } /** * Create a new cell footer for the parent widget. */ createCellFooter() { return new CellFooter(); } /** * Create an input prompt. */ createInputPrompt() { return new InputPrompt(); } /** * Create the output prompt for the widget. */ createOutputPrompt() { return new OutputPrompt(); } /** * Create an stdin widget. */ createStdin(options) { return new Stdin(options); } } Cell.ContentFactory = ContentFactory; })(Cell || (Cell = {})); /** **************************************************************************** * CodeCell ******************************************************************************/ /** * Code cell layout * * It will not detached the output area when the cell is detached. */ export class CodeCellLayout extends PanelLayout { /** * A message handler invoked on a `'before-attach'` message. * * #### Notes * The default implementation of this method forwards the message * to all widgets. It assumes all widget nodes are attached to the * parent widget node. * * This may be reimplemented by subclasses as needed. */ onBeforeAttach(msg) { let beforeOutputArea = true; const outputAreaWrapper = this.parent.node.firstElementChild; for (const widget of this) { if (outputAreaWrapper) { if (widget.node === outputAreaWrapper) { beforeOutputArea = false; } else { MessageLoop.sendMessage(widget, msg); if (beforeOutputArea) { this.parent.node.insertBefore(widget.node, outputAreaWrapper); } else { this.parent.node.appendChild(widget.node); } // Force setting isVisible to true as it requires the parent widget to be // visible. But that flag will be set only during the `onAfterAttach` call. if (!this.parent.isHidden) { widget.setFlag(Widget.Flag.IsVisible); } // Not called in NotebookWindowedLayout to avoid outputArea // widgets unwanted update or reset. MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach); } } } } /** * A message handler invoked on an `'after-detach'` message. * * #### Notes * The default implementation of this method forwards the message * to all widgets. It assumes all widget nodes are attached to the * parent widget node. * * This may be reimplemented by subclasses as needed. */ onAfterDetach(msg) { for (const widget of this) { // TODO we could improve this further by removing outputs based // on their mime type (for example plain/text or markdown could safely be detached) // If the cell is out of the view port, its children are already detached -> skip detaching if (!widget.hasClass(CELL_OUTPUT_WRAPPER_CLASS) && widget.node.isConnected) { // Not called in NotebookWindowedLayout for windowed notebook MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach); this.parent.node.removeChild(widget.node); MessageLoop.sendMessage(widget, msg); } } } } /** * A widget for a code cell. */ export class CodeCell extends Cell { /** * Construct a code cell widget. */ constructor(options) { var _a; super({ layout: new CodeCellLayout(), ...options, placeholder: true }); /** * Detect the movement of the caret in the output area. * * Emits scroll request if the caret moved. */ this._detectCaretMovementInOuput = (e) => { const inWindowedContainer = this._inViewport !== null; const defaultPrevented = inWindowedContainer && !this._inViewport; // Because we do not want to scroll on any key, but only on keys which // move the caret (this on keys which cause input and on keys like left, // right, top, bottom arrow, home, end, page down/up - but only if the // cursor is not at the respective end of the input) we need to listen // to the `selectionchange` event on target inputs/textareas, etc. const target = e.target; if (!target || !(target instanceof HTMLElement)) { return; } // Make sure the previous listener gets disconnected if (this._lastTarget) { this._lastTarget.removeEventListener('selectionchange', this._lastOnCaretMovedHandler); document.removeEventListener('selectionchange', this._lastOnCaretMovedHandler); } const onCaretMoved = () => { this._scrollRequested.emit({ scrollWithinCell: ({ scroller }) => { ElementExt.scrollIntoViewIfNeeded(scroller, target); }, defaultPrevented }); }; // Remember the most recent target/handler to disconnect them next time. this._lastTarget = target; this._lastOnCaretMovedHandler = onCaretMoved; // Firefox only supports `selectionchange` on the actual input element, // all other browsers only support it on the top-level document. target.addEventListener('selectionchange', onCaretMoved, { once: true }); document.addEventListener('selectionchange', onCaretMoved, { once: true }); // Schedule removal of the listener. setTimeout(() => { target.removeEventListener('selectionchange', onCaretMoved); document.removeEventListener('selectionchange', onCaretMoved); }, 250); }; this._headingsCache = null; this._outputHidden = false; this._outputWrapper = null; this._outputPlaceholder = null; this._syncScrolled = false; this._lastTarget = null; this._lastOutputHeight = ''; this.addClass(CODE_CELL_CLASS); const trans = this.translator.load('jupyterlab'); // Only save options not handled by parent constructor. const rendermime = (this._rendermime = options.rendermime); const contentFactory = this.contentFactory; const model = this.model; this.maxNumberOutputs = options.maxNumberOutputs; // Note that modifying the below label warrants one to also modify // the same in this._outputLengthHandler. Ideally, this label must // have been a constant and used in both places but it is not done // so because of limitations in the translation manager. const ariaLabel = model.outputs.length === 0 ? trans.__('Code Cell Content') : trans.__('Code Cell Content with Output'); this.node.setAttribute('aria-label', ariaLabel); const output = (this._output = new OutputArea({ model: this.model.outputs, rendermime, contentFactory: contentFactory, maxNumberOutputs: this.maxNumberOutputs, translator: this.translator, promptOverlay: true, inputHistoryScope: options.inputHistoryScope, showInputPlaceholder: options.showInputPlaceholder })); output.node.addEventListener('keydown', this._detectCaretMovementInOuput); output.addClass(CELL_OUTPUT_AREA_CLASS); output.toggleScrolling.connect(() => { this.outputsScrolled = !this.outputsScrolled; }); output.initialize.connect(() => { this.updatePromptOverlayIcon(); }); // Defer setting placeholder as OutputArea must be instantiated before initializing the DOM this.placeholder = (_a = options.placeholder) !== null && _a !== void 0 ? _a : true; model.outputs.changed.connect(this.onOutputChanged, this); model.outputs.stateChanged.connect(this.onOutputChanged, this); model.stateChanged.connect(this.onStateChanged, this); } /** * Create children widgets. */ initializeDOM() { if (!this.placeholder) { return; } super.initializeDOM(); this._updatePrompt(); // Insert the output before the cell footer. const outputWrapper = (this._outputWrapper = new Panel()); outputWrapper.addClass(CELL_OUTPUT_WRAPPER_CLASS); const outputCollapser = new OutputCollapser(); outputCollapser.addClass(CELL_OUTPUT_COLLAPSER_CLASS); outputWrapper.addWidget(outputCollapser); // Set a CSS if there are no outputs, and connect a signal for future // changes to the number of outputs. This is for conditional styling // if there are no outputs. if (this.model.outputs.length === 0) { this.addClass(NO_OUTPUTS_CLASS); } this._output.outputLengthChanged.connect(this._outputLengthHandler, this); outputWrapper.addWidget(this._output); const layout = this.layout; const resizeHandle = new ResizeHandle(this.node); resizeHandle.sizeChanged.connect(this._sizeChangedHandler, this); layout.insertWidget(layout.widgets.length - 1, resizeHandle); layout.insertWidget(layout.widgets.length - 1, outputWrapper); if (this.model.isDirty) { this.addClass(DIRTY_CLASS); } this._outputPlaceholder = new OutputPlaceholder({ callback: () => { this.outputHidden = !this.outputHidden; }, text: this.getOutputPlaceholderText(), translator: this.translator }); const layoutWrapper = outputWrapper.layout; if (this.outputHidden) { layoutWrapper.removeWidget(this._output); layoutWrapper.addWidget(this._outputPlaceholder); if (this.inputHidden && !outputWrapper.isHidden) { this._outputWrapper.hide(); } } const trans = this.translator.load('jupyterlab'); const ariaLabel = this.model.outputs.length === 0 ? trans.__('Code Cell Content') : trans.__('Code Cell Content with Output'); this.node.setAttribute('aria-label', ariaLabel); } getOutputPlaceholderText() { var _a; const firstOutput = this.model.outputs.get(0); const outputData = firstOutput === null || firstOutput === void 0 ? void 0 : firstOutput.data; if (!outputData) { return undefined; } const supportedOutputTypes = [ 'text/html', 'image/svg+xml', 'application/pdf', 'text/markdown', 'text/plain', 'application/vnd.jupyter.stderr', 'application/vnd.jupyter.stdout', 'text' ]; const preferredOutput = supportedOutputTypes.find(mt => { const data = firstOutput.data[mt]; return (Array.isArray(data) ? typeof data[0] : typeof data) === 'string'; }); const dataToDisplay = firstOutput.data[preferredOutput !== null && preferredOutput !== void 0 ? preferredOutput : '']; if (dataToDisplay !== undefined) { return (_a = (Array.isArray(dataToDisplay) ? dataToDisplay : dataToDisplay === null || dataToDisplay === void 0 ? void 0 : dataToDisplay.split('\n'))) === null || _a === void 0 ? void 0 : _a.find(part => part !== ''); } return undefined; } /** * Initialize view state from model. * * #### Notes * Should be called after construction. For convenience, returns this, so it * can be chained in the construction, like `new Foo().initializeState();` */ initializeState() { super.initializeState(); this.loadScrolledState(); this._updatePrompt(); return this; } get headings() { if (!this._headingsCache) { const headings = []; // Iterate over the code cell outputs to check for Markdown or HTML from which we can generate ToC headings... const outputs = this.model.outputs; for (let j = 0; j < outputs.length; j++) { const m = outputs.get(j); let htmlType = null; let mdType = null; Object.keys(m.data).forEach(t => { if (!mdType && TableOfContentsUtils.Markdown.isMarkdown(t)) { mdType = t; } else if (!htmlType && TableOfContentsUtils.isHTML(t)) { htmlType = t; } }); // Parse HTML output if (htmlType) { let htmlData = m.data[htmlType]; if (typeof htmlData !== 'string') { htmlData = htmlData.join('\n'); } headings.push(...TableOfContentsUtils.getHTMLHeadings(this._rendermime.sanitizer.sanitize(htmlData)).map(heading => { return { ...heading, outputIndex: j, type: Cell.HeadingType.HTML }; })); } else if (mdType) { headings.push(...TableOfContentsUtils.Markdown.getHeadings(m.data[mdType]).map(heading => { return { ...heading, outputIndex: j, type: Cell.HeadingType.Markdown }; })); } } this._headingsCache = headings; } return [...this._headingsCache]; } /** * Get the output area for the cell. */ get outputArea() { return this._output; } /** * The view state of output being collapsed. */ get outputHidden() { return this._outputHidden; } set outputHidden(value) { var _a; if (this._outputHidden === value) { return; } if (!this.placeholder) { const layout = this._outputWrapper.layout; if (value) { layout.removeWidget(this._output); layout.addWidget(this._outputPlaceholder); if (this.inputHidden && !this._outputWrapper.isHidden) { this._outputWrapper.hide(); } if (this._outputPlaceholder) { this._outputPlaceholder.text = (_a = this.getOutputPlaceholderText()) !== null && _a !== void 0 ? _a : ''; } } else { if (this._outputWrapper.isHidden) { this._outputWrapper.show(); } layout.removeWidget(this._outputPlaceholder); layout.addWidget(this._output); } } this._outputHidden = value; if (this.syncCollapse) { this.saveCollapseState(); } } /** * Save view collapse state to model */ saveCollapseState() { // Because collapse state for a code cell involves two different pieces of // metadata (the `collapsed` and `jupyter` metadata keys), we block reacting // to changes in metadata until we have fully committed our changes. // Otherwise setting one key can trigger a write to the other key to // maintain the synced consistency. this.model.sharedModel.transact(() => { super.saveCollapseState(); const collapsed = this.model.getMetadata('collapsed'); if ((this.outputHidden && collapsed === true) || (!this.outputHidden && collapsed === undefined)) { return; } // Do not set jupyter.outputs_hidden since it is redundant. See // and https://github.com/jupyter/nbformat/issues/137 if (this.outputHidden) { this.model.setMetadata('collapsed', true); } else { this.model.deleteMetadata('collapsed'); } }, false, 'silent-change'); } /** * Revert view collapse state from model. * * We consider the `collapsed` metadata key as the source of truth for outputs * being hidden. */ loadCollapseState() { super.loadCollapseState(); this.outputHidden = !!this.model.getMetadata('collapsed'); } /** * Whether the output is in a scrolled state? */ get outputsScrolled() { return this._outputsScrolled; } set outputsScrolled(value) { this.toggleClass('jp-mod-outputsScrolled', value); this._outputsScrolled = value; if (this.syncScrolled) { this.saveScrolledState(); } this.updatePromptOverlayIcon(); } /** * Update the Prompt Overlay Icon */ updatePromptOverlayIcon() { var _a; const overlay = DOMUtils.findElement(this.node, 'jp-OutputArea-promptOverlay'); if (!overlay) { return; } // If you are changing this, don't forget about svg. const ICON_HEIGHT = 16 + 4 + 4; // 4px for padding if (overlay.clientHeight <= ICON_HEIGHT) { (_a = overlay.firstChild) === null || _a === void 0 ? void 0 : _a.remove(); return; } let overlayTitle; if (this._outputsScrolled) { expandIcon.element({ container: overlay }); overlayTitle = 'Expand Output'; } else { collapseIcon.element({ container: overlay }); overlayTitle = 'Collapse Output'; } const trans = this.translator.load('jupyterlab'); overlay.title = trans.__(overlayTitle); } /** * Save view collapse state to model */ saveScrolledState() { const current = this.model.getMetadata('scrolled'); if ((this.outputsScrolled && current === true) || (!this.outputsScrolled && current === undefined)) { return; } if (this.outputsScrolled) { this.model.setMetadata('scrolled', true); } else { this.outputArea.node.style.height = ''; this.model.deleteMetadata('scrolled'); } } /** * Revert view collapse state from model. */ loadScrolledState() { // We don't have the notion of 'auto' scrolled, so we make it false. if (this.model.getMetadata('scrolled') === 'auto') { this.outputsScrolled = false; } else { this.outputsScrolled = !!this.model.getMetadata('scrolled'); } } /** * Whether to sync the scrolled state to the cell model. */ get syncScrolled() { return this._syncScrolled; } set syncScrolled(value) { if (this._syncScrolled === value) { return; } this._syncScrolled = value; if (value) { this.loadScrolledState(); } } /** * Handle the input being hidden. * * #### Notes * This method is called by the case cell implementation and is * subclasses here so the code cell can watch to see when input * is hidden without accessing private state. */ handleInputHidden(value) { if (this.placeholder) { return; } if (!value && this._outputWrapper.isHidden) { this._outputWrapper.show(); } else if (value && !this._outputWrapper.isHidden && this._outputHidden) { this._outputWrapper.hide(); } } /** * Clone the cell, using the same model. */ clone() { const constructor = this.constructor; return new constructor({ model: this.model, contentFactory: this.contentFactory, rendermime: this._rendermime, placeholder: false, translator: this.translator }); } /** * Clone the OutputArea alone, returning a simplified output area, using the same model. */ cloneOutputArea() { return new SimplifiedOutputArea({ model: this.model.outputs, contentFactory: this.contentFactory, rendermime: this._rendermime }); } /** * Dispose of the resources used by the widget. */ dispose() { if (this.isDisposed) { return; } this._output.outputLengthChanged.disconnect(this._outputLengthHandler, this); this._output.node.removeEventListener('keydown', this._detectCaretMovementInOuput); this._rendermime = null; this._output = null; this._outputWrapper = null; this._outputPlaceholder = null; super.dispose(); } /** * Handle changes in the model. */ onStateChanged(model, args) { switch (args.name) { case 'executionCount': if (args.newValue !== null) { // Mark execution state if execution count was set. this.model.executionState = 'idle'; } this._updatePrompt(); break; case 'executionState': this._updatePrompt(); break; case 'isDirty': if (model.isDirty) { this.addClass(DIRTY_CLASS); } else { this.removeClass(DIRTY_CLASS); } break; default: break; } } /** * Callback on output changes */ onOutputChanged() { var _a; this._headingsCache = null; if (this._outputPlaceholder && this.outputHidden) { this._outputPlaceholder.text = (_a = this.getOutputPlaceholderText()) !== null && _a !== void 0 ? _a : ''; } // This is to hide/show icon on single line output. this.updatePromptOverlayIcon(); // Clear output area when empty const height = this.outputArea.node.style.height; if (this.model.outputs.length === 0 && height !== '') { this._lastOutputHeight = height; this.outputArea.node.style.height = ''; } else if (this.model.outputs.length > 0 && height === '') { this.outputArea.node.style.height = this._lastOutputHeight; } } /** * Handle changes in the metadata. */ onMetadataChanged(model, args) { switch (args.key) { case 'scrolled': if (this.syncScrolled) { this.loadScrolledState(); } break; case 'collapsed': if (this.syncCollapse) { this.loadCollapseState(); } break; default: break; } super.onMetadataChanged(model, args); } _updatePrompt() { let prompt; if (this.model.executionState == 'running') { prompt = '*'; } else { prompt = `${this.model.executionCount || ''}`; } this._setPrompt(prompt); } /** * Handle changes in the number of outputs in the output area. */ _outputLengthHandler(sender, args) { const force = args === 0 ? true : false; this.toggleClass(NO_OUTPUTS_CLASS, force); const trans = this.translator.load('jupyterlab'); const ariaLabel = force ? trans.__('Code Cell Content') : trans.__('Code Cell Content with Output'); this.node.setAttribute('aria-label', ariaLabel); } /** * Handle changes in input/output proportions in side-by-side mode. */ _sizeChangedHandler(sender) { this._displayChanged.emit(); } } /** * The namespace for the `CodeCell` class statics. */ (function (CodeCell) { /** * Execute a cell given a client session. */ async function execute(cell, sessionContext, metadata) { var _a; const model = cell.model; const code = model.sharedModel.getSource(); if (!code.trim() || !((_a = sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel)) { model.sharedModel.transact(() => { model.clearExecution(); }, false, 'silent-change'); return; } const cellId = { cellId: model.sharedModel.getId() }; metadata = { ...model.metadata, ...metadata, ...cellId }; const { recordTiming } = metadata; model.sharedModel.transact(() => { model.clearExecution(); cell.outputHidden = false; }, false, 'silent-change'); // note: in future we would like to distinguish running from scheduled model.executionState = 'running'; model.trusted = true; let future; try { const msgPromise = OutputArea.execute(code, cell.outputArea, sessionContext, metadata); // cell.outputArea.future assigned synchronously in `execute` if (recordTiming) { const recordTimingHook = (msg) => { let label; switch (msg.header.msg_type) { case 'status': label = `status.${msg.content.execution_state}`; break; case 'execute_input': label = 'execute_input'; break; default: return true; } // If the data is missing, estimate it to now // Date was added in 5.1: https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header const value = msg.header.date || new Date().toISOString(); const timingInfo = Object.assign({}, model.getMetadata('execution')); timingInfo[`iopub.${label}`] = value; model.setMetadata('execution', timingInfo); return true; }; cell.outputArea.future.registerMessageHook(recordTimingHook); } else { model.deleteMetadata('execution'); } // Save this execution's future so we can compare in the catch below. future = cell.outputArea.future; const msg = (await msgPromise); model.executionCount = msg.content.execution_count; if (recordTiming) { const timingInfo = Object.assign({}, model.getMetadata('execution')); const started = msg.metadata.started; // Started is not in the API, but metadata IPyKernel sends if (started) { timingInfo['shell.execute_reply.started'] = started; } // Per above, the 5.0 spec does not assume date, so we estimate is required const finished = msg.header.date; timingInfo['shell.execute_reply'] = finished || new Date().toISOString(); model.setMetadata('execution', timingInfo); } return msg; } catch (e) { // If we started executing, and the cell is still indicating this // execution, clear the prompt. if (future && !cell.isDisposed && cell.outputArea.future === future) { cell.model.executionState = 'idle'; if (recordTiming && future.isDisposed) { // Record the time when the cell execution was aborted const timingInfo = Object.assign({}, model.getMetadata('execution')); timingInfo['execution_failed'] = new Date().toISOString(); model.setMetadata('execution', timingInfo);