diff --git a/packages/outputarea/package.json b/packages/outputarea/package.json index 62a315b35cd1..d5e172e286c1 100644 --- a/packages/outputarea/package.json +++ b/packages/outputarea/package.json @@ -47,7 +47,8 @@ "@lumino/messaging": "^1.3.3", "@lumino/properties": "^1.1.6", "@lumino/signaling": "^1.3.5", - "@lumino/widgets": "^1.11.1" + "@lumino/widgets": "^1.11.1", + "rxjs": "6.5.4" }, "devDependencies": { "rimraf": "~3.0.0", diff --git a/packages/outputarea/src/OutputArea.tsx b/packages/outputarea/src/OutputArea.tsx new file mode 100644 index 000000000000..4767a1bc7321 --- /dev/null +++ b/packages/outputarea/src/OutputArea.tsx @@ -0,0 +1,242 @@ +import * as React from 'react'; +import { IOutputAreaModel } from './model'; +import { Widget, Panel } from '@lumino/widgets'; +import { Kernel, KernelMessage } from '@jupyterlab/services'; +import { OutputArea, IStdin } from './widget'; +import { dispatch } from 'rxjs/internal/observable/pairs'; + +/** + * The class name added to an output area widget. + */ +const OUTPUT_AREA_CLASS = 'jp-OutputArea'; + +/** + * The class name added to the direction children of OutputArea + */ +const OUTPUT_AREA_ITEM_CLASS = 'jp-OutputArea-child'; + +/** + * The class name added to actual outputs + */ +const OUTPUT_AREA_OUTPUT_CLASS = 'jp-OutputArea-output'; + +/** + * The class name added to prompt children of OutputArea. + */ +const OUTPUT_AREA_PROMPT_CLASS = 'jp-OutputArea-prompt'; + +/** + * The class name added to OutputPrompt. + */ +const OUTPUT_PROMPT_CLASS = 'jp-OutputPrompt'; + +/** + * The class name added to an execution result. + */ +const EXECUTE_CLASS = 'jp-OutputArea-executeResult'; + +/** + * The class name added stdin items of OutputArea + */ +const OUTPUT_AREA_STDIN_ITEM_CLASS = 'jp-OutputArea-stdin-item'; + +/** + * The class name added to stdin widgets. + */ +const STDIN_CLASS = 'jp-Stdin'; + +/** + * The class name added to stdin data prompt nodes. + */ +const STDIN_PROMPT_CLASS = 'jp-Stdin-prompt'; + +/** + * The class name added to stdin data input nodes. + */ +const STDIN_INPUT_CLASS = 'jp-Stdin-input'; + +type FUTURE = Kernel.IShellFuture< + KernelMessage.IExecuteRequestMsg, + KernelMessage.IExecuteReplyMsg +>; +type STATE = { + stdin: [Widget, IStdin] | null; + outpust: Array<{ displayID: string; widget: Widget }>; +}; +type ACTION = + | { + name: 'future.onStdin'; + msg: KernelMessage.IStdinMessage; + } + | { + name: 'future.onIOPub'; + msg: KernelMessage.IIOPubMessage; + } + | { + name: 'future.onReply'; + msg: KernelMessage.IExecuteReplyMsg; + } + | { name: 'model.clear' } + | { name: 'stdin.value'; value: string }; + +const DEFAULT_STATE: STATE = { stdin: null }; + +function makeReducer({ + future, + contentFactory, + model +}: { + future: FUTURE; + contentFactory: OutputArea.ContentFactory; + model: IOutputAreaModel; +}): (state: STATE, action: ACTION) => STATE { + return (state, action) => { + switch (action.name) { + case 'future.onIOPub': + const { msg: IOPubMsg } = action; + let output: nbformat.IOutput; + let transient = ((msg.content as any).transient || {}) as JSONObject; + let displayId = transient['display_id'] as string; + let targets: number[] | undefined; + + switch (IOPubMsg.header.msg_type) { + case 'execute_result': + case 'display_data': + case 'stream': + case 'error': + output = { + ...IOPubMsg.content, + output_type: IOPubMsg.header.msg_type + }; + model.add(output); + break; + case 'clear_output': + let wait = (msg as KernelMessage.IClearOutputMsg).content.wait; + model.clear(wait); + break; + case 'update_display_data': + output = { ...msg.content, output_type: 'display_data' }; + targets = this._displayIdMap.get(displayId); + if (targets) { + for (let index of targets) { + model.set(index, output); + } + } + break; + default: + break; + } + return state; + case 'future.onStdin': + const { msg: stdinMsg } = action; + if (KernelMessage.isInputRequestMsg(stdinMsg)) { + return { + ...state, + stdin: makeStdin({ msg: stdinMsg, contentFactory, future }) + }; + } + return state; + case 'stdin.value': + const { value } = action; + model.add({ + output_type: 'stream', + name: 'stdin', + text: value + '\n' + }); + const { stdin, ...restState } = state; + if (!stdin) { + throw new Error( + 'If we have a stdin value, we should have had a stdin widget' + ); + } + stdin[0].dispose(); + return { + ...restState, + stdin: null + }; + default: + throw new Error(`Unknown action ${action.name}`); + } + }; +} + +function makeStdin({ + msg, + contentFactory, + future +}: { + msg: KernelMessage.IInputRequestMsg; + contentFactory: OutputArea.ContentFactory; + future: FUTURE; +}): [Widget, IStdin] { + const panel = new Panel(); + panel.addClass(OUTPUT_AREA_ITEM_CLASS); + panel.addClass(OUTPUT_AREA_STDIN_ITEM_CLASS); + + const prompt = contentFactory.createOutputPrompt(); + prompt.addClass(OUTPUT_AREA_PROMPT_CLASS); + panel.addWidget(prompt); + + const input = contentFactory.createStdin({ + prompt: msg.content.prompt, + password: msg.content.password, + future + }); + input.addClass(OUTPUT_AREA_OUTPUT_CLASS); + panel.addWidget(input); + + return [panel, input]; +} + +/** + * Create an output area component based on the model. + * + * Will call `setWidgets` with a list of the lumino widgets displayed in this widget every time they change. + */ +export default function OutputArea({ + model, + future, + contentFactory, + setWidgets +}: { + model: IOutputAreaModel; + setWidgets: (widgets: Widget[]) => void; + contentFactory: OutputArea.ContentFactory; + future: FUTURE; +}) { + const [state, dispatch] = React.useReducer( + React.useCallback(makeReducer({ future, contentFactory, model }), [ + future, + contentFactory, + model + ]), + DEFAULT_STATE + ); + + React.useEffect(() => { + dispatch({ name: 'model.clear' }); + future.onIOPub = msg => dispatch({ name: 'future.onIOPub', msg }); + future.onReply = msg => dispatch({ name: 'future.onReply', msg }); + future.onStdin = msg => dispatch({ name: 'future.onStdin', msg }); + return () => future.dispose(); + }, [dispatch, future]); + + React.useEffect(() => { + if (state.stdin) { + state.stdin[1].value.then(value => + dispatch({ name: 'stdin.value', value }) + ); + } + }, [state.stdin]); + // + // + // Either we have a current input request we are dealing with + // or we dont + // Either + // Keep a list of widgets and display ids + + // Update on model change + + // mapping + return <>{state}; +} diff --git a/packages/outputarea/src/widget.ts b/packages/outputarea/src/widget.ts index 84b71dfe8595..6111b751dd8d 100644 --- a/packages/outputarea/src/widget.ts +++ b/packages/outputarea/src/widget.ts @@ -18,7 +18,7 @@ import { Panel, PanelLayout } from '@lumino/widgets'; import { Widget } from '@lumino/widgets'; -import { ISessionContext } from '@jupyterlab/apputils'; +import { ISessionContext, ReactWidget } from '@jupyterlab/apputils'; import * as nbformat from '@jupyterlab/nbformat'; @@ -30,6 +30,10 @@ import { Kernel, KernelMessage } from '@jupyterlab/services'; import { IOutputAreaModel } from './model'; +import { BehaviorSubject } from 'rxjs'; +import * as React from 'react'; +import OutputAreaComponent from './OutputArea'; + /** * The class name added to an output area widget. */ @@ -93,7 +97,7 @@ const STDIN_INPUT_CLASS = 'jp-Stdin-input'; * `null` model, and may want to listen to the `modelChanged` * signal. */ -export class OutputArea extends Widget { +export class OutputArea extends ReactWidget { /** * Construct an output area widget. */ @@ -104,11 +108,7 @@ export class OutputArea extends Widget { this.rendermime = options.rendermime; this.contentFactory = options.contentFactory || OutputArea.defaultContentFactory; - this.layout = new PanelLayout(); - for (let i = 0; i < model.length; i++) { - let output = model.get(i); - this._insertOutput(i, output); - } + model.changed.connect(this.onModelChanged, this); model.stateChanged.connect(this.onStateChanged, this); } @@ -132,7 +132,7 @@ export class OutputArea extends Widget { * A read-only sequence of the chidren widgets in the output area. */ get widgets(): ReadonlyArray { - return (this.layout as PanelLayout).widgets; + return [...this._widgets]; } /** @@ -185,13 +185,6 @@ export class OutputArea extends Widget { // Handle the execute reply. value.onReply = this._onExecuteReply; - - // Handle stdin. - value.onStdin = msg => { - if (KernelMessage.isInputRequestMsg(msg)) { - this.onInputRequest(msg, value); - } - }; } /** @@ -206,6 +199,13 @@ export class OutputArea extends Widget { super.dispose(); } + protected render() { + return React.createElement(OutputAreaComponent, { + model: this.model, + setWidgets: widgets => (this._widgets = widgets) + }); + } + /** * Follow changes on the model state. */ @@ -215,7 +215,6 @@ export class OutputArea extends Widget { ): void { switch (args.type) { case 'add': - this._insertOutput(args.newIndex, args.newValues[0]); this.outputLengthChanged.emit(this.model.length); break; case 'remove': @@ -227,21 +226,12 @@ export class OutputArea extends Widget { // range of items removed from model // remove widgets corresponding to removed model items const startIndex = args.oldIndex; - for ( - let i = 0; - i < args.oldValues.length && startIndex < this.widgets.length; - ++i - ) { - let widget = this.widgets[startIndex]; - widget.parent = null; - widget.dispose(); - } // apply item offset to target model item indices in _displayIdMap this._moveDisplayIdIndices(startIndex, args.oldValues.length); // prevent jitter caused by immediate height change - this._preventHeightChangeJitter(); + // this._preventHeightChangeJitter(); } this.outputLengthChanged.emit(this.model.length); } @@ -315,6 +305,7 @@ export class OutputArea extends Widget { this._preventHeightChangeJitter(); } + // TODO: Replicate this in react private _preventHeightChangeJitter() { // When an output area is cleared and then quickly replaced with new // content (as happens with @interact in widgets, for example), the @@ -334,53 +325,10 @@ export class OutputArea extends Widget { }, 50); } - /** - * Handle an input request from a kernel. - */ - protected onInputRequest( - msg: KernelMessage.IInputRequestMsg, - future: Kernel.IShellFuture - ): void { - // Add an output widget to the end. - let factory = this.contentFactory; - let stdinPrompt = msg.content.prompt; - let password = msg.content.password; - - let panel = new Panel(); - panel.addClass(OUTPUT_AREA_ITEM_CLASS); - panel.addClass(OUTPUT_AREA_STDIN_ITEM_CLASS); - - let prompt = factory.createOutputPrompt(); - prompt.addClass(OUTPUT_AREA_PROMPT_CLASS); - panel.addWidget(prompt); - - let input = factory.createStdin({ prompt: stdinPrompt, password, future }); - input.addClass(OUTPUT_AREA_OUTPUT_CLASS); - panel.addWidget(input); - - let layout = this.layout as PanelLayout; - layout.addWidget(panel); - - /** - * Wait for the stdin to complete, add it to the model (so it persists) - * and remove the stdin widget. - */ - void input.value.then(value => { - // Use stdin as the stream so it does not get combined with stdout. - this.model.add({ - output_type: 'stream', - name: 'stdin', - text: value + '\n' - }); - panel.dispose(); - }); - } - /** * Update an output in the layout in place. */ private _setOutput(index: number, model: IOutputModel): void { - let layout = this.layout as PanelLayout; let panel = layout.widgets[index] as Panel; let renderer = (panel.widgets ? panel.widgets[1] @@ -406,17 +354,16 @@ export class OutputArea extends Widget { } /** - * Render and insert a single output into the layout. + * Render a single output . */ - private _insertOutput(index: number, model: IOutputModel): void { + private _renderOutput(model: IOutputModel): Widget { let output = this.createOutputItem(model); if (output) { output.toggleClass(EXECUTE_CLASS, model.executionCount !== null); } else { output = new Widget(); } - let layout = this.layout as PanelLayout; - layout.insertWidget(index, output); + return output; } /** @@ -557,6 +504,7 @@ export class OutputArea extends Widget { KernelMessage.IExecuteReplyMsg >; private _displayIdMap = new Map(); + private _widgets: Widget[] = []; } export class SimplifiedOutputArea extends OutputArea { diff --git a/yarn.lock b/yarn.lock index 33e64c4238ec..7eaf0c3937fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13467,6 +13467,13 @@ rx-lite@*, rx-lite@^4.0.8: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= +rxjs@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" + integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== + dependencies: + tslib "^1.9.0" + rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3: version "6.5.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a"