فهرست منبع

Merge pull request #517 from blink1073/stdin

Implement input request handling
Afshin Darian 8 سال پیش
والد
کامیت
6cc667928e

+ 1 - 1
package.json

@@ -13,7 +13,7 @@
     "es6-promise": "^3.2.1",
     "jquery": "^2.2.0",
     "jquery-ui": "^1.10.5 <1.12",
-    "jupyter-js-services": "^0.16.0",
+    "jupyter-js-services": "^0.16.3",
     "leaflet": "^0.7.7",
     "marked": "^0.3.5",
     "moment": "^2.11.2",

+ 4 - 1
src/notebook/cells/model.ts

@@ -424,7 +424,10 @@ class CodeCellModel extends CellModel implements ICodeCellModel {
     let outputs = this.outputs;
     cell.outputs = [];
     for (let i = 0; i < outputs.length; i++) {
-      cell.outputs.push(outputs.get(i));
+      let output = outputs.get(i);
+      if (output.output_type !== 'input_request') {
+        cell.outputs.push(output as nbformat.IOutput);
+      }
     }
     return cell;
   }

+ 4 - 1
src/notebook/notebook/widget.ts

@@ -754,10 +754,13 @@ class Notebook extends StaticNotebook {
         }
       }
     } else {
+      if (!this.hasClass(COMMAND_CLASS)) {
+        this.node.focus();
+      }
       this.addClass(COMMAND_CLASS);
       this.removeClass(EDIT_CLASS);
-      this.node.focus();
     }
+
     if (activeCell) {
       activeCell.addClass(ACTIVE_CLASS);
     }

+ 81 - 20
src/notebook/output-area/model.ts

@@ -31,14 +31,14 @@ class OutputAreaModel implements IDisposable {
    * Construct a new observable outputs instance.
    */
   constructor() {
-    this._list = new ObservableList<nbformat.IOutput>();
+    this._list = new ObservableList<OutputAreaModel.Output>();
     this._list.changed.connect(this._onListChanged, this);
   }
 
   /**
    * A signal emitted when the model changes.
    */
-  get changed(): ISignal<OutputAreaModel, IListChangedArgs<nbformat.IOutput>> {
+  get changed(): ISignal<OutputAreaModel, IListChangedArgs<OutputAreaModel.Output>> {
     return Private.changedSignal.bind(this);
   }
 
@@ -85,7 +85,7 @@ class OutputAreaModel implements IDisposable {
   /**
    * Get an item at the specified index.
    */
-  get(index: number): nbformat.IOutput {
+  get(index: number): OutputAreaModel.Output {
     return this._list.get(index);
   }
 
@@ -96,43 +96,47 @@ class OutputAreaModel implements IDisposable {
    * The output bundle is copied.
    * Contiguous stream outputs of the same `name` are combined.
    */
-  add(output: nbformat.IOutput): number {
+  add(output: OutputAreaModel.Output): number {
     // If we received a delayed clear message, then clear now.
     if (this._clearNext) {
       this.clear();
       this._clearNext = false;
     }
 
+    if (output.output_type === 'input_request') {
+      this._list.add(output);
+    }
+
     // Make a copy of the output bundle.
-    output = JSON.parse(JSON.stringify(output));
+    let value = JSON.parse(JSON.stringify(output)) as nbformat.IOutput;
 
     // Join multiline text outputs.
-    if (nbformat.isStream(output)) {
-      if (Array.isArray(output.text)) {
-        output.text = (output.text as string[]).join('\n');
+    if (nbformat.isStream(value)) {
+      if (Array.isArray(value.text)) {
+        value.text = (value.text as string[]).join('\n');
       }
     }
 
     // Consolidate outputs if they are stream outputs of the same kind.
     let index = this.length - 1;
     let lastOutput = this.get(index) as nbformat.IStream;
-    if (nbformat.isStream(output)
+    if (nbformat.isStream(value)
         && lastOutput && nbformat.isStream(lastOutput)
-        && output.name === lastOutput.name) {
+        && value.name === lastOutput.name) {
       // In order to get a list change event, we add the previous
       // text to the current item and replace the previous item.
       // This also replaces the metadata of the last item.
-      let text = output.text as string;
-      output.text = lastOutput.text as string + text;
+      let text = value.text as string;
+      value.text = lastOutput.text as string + text;
       this._list.set(index, output);
       return index;
     } else {
-      switch (output.output_type) {
+      switch (value.output_type) {
       case 'stream':
       case 'execute_result':
       case 'display_data':
       case 'error':
-        return this._list.add(output);
+        return this._list.add(value);
       default:
         break;
       }
@@ -145,7 +149,7 @@ class OutputAreaModel implements IDisposable {
    *
    * @param wait Delay clearing the output until the next message is added.
    */
-  clear(wait: boolean = false): nbformat.IOutput[] {
+  clear(wait: boolean = false): OutputAreaModel.Output[] {
     if (wait) {
       this._clearNext = true;
       return [];
@@ -165,7 +169,8 @@ class OutputAreaModel implements IDisposable {
     this.clear();
     return new Promise<KernelMessage.IExecuteReplyMsg>((resolve, reject) => {
       let future = kernel.execute(content);
-      future.onIOPub = ((msg: KernelMessage.IIOPubMessage) => {
+      // Handle published messages.
+      future.onIOPub = (msg: KernelMessage.IIOPubMessage) => {
         let msgType = msg.header.msg_type as nbformat.OutputType;
         switch (msgType) {
         case 'execute_result':
@@ -179,22 +184,74 @@ class OutputAreaModel implements IDisposable {
         default:
           break;
         }
-      });
+      };
+      // Handle the execute reply.
       future.onReply = (msg: KernelMessage.IExecuteReplyMsg) => {
         resolve(msg);
       };
+      // Handle stdin.
+      future.onStdin = (msg: KernelMessage.IStdinMessage) => {
+        if (KernelMessage.isInputRequestMsg(msg)) {
+          this.add({
+            output_type: 'input_request',
+            prompt: msg.content.prompt,
+            password: msg.content.password,
+            kernel
+          });
+        }
+      };
     });
   }
 
   /**
    * Handle a change to the list.
    */
-  private _onListChanged(sender: IObservableList<nbformat.IOutput>, args: IListChangedArgs<nbformat.IOutput>) {
+  private _onListChanged(sender: IObservableList<OutputAreaModel.Output>, args: IListChangedArgs<OutputAreaModel.Output>) {
     this.changed.emit(args);
   }
 
   private _clearNext = false;
-  private _list: IObservableList<nbformat.IOutput> = null;
+  private _list: IObservableList<OutputAreaModel.Output> = null;
+}
+
+
+/**
+ * A namespace for OutputAreaModel statics.
+ */
+export
+namespace OutputAreaModel {
+  /**
+   * Output for an input request from the kernel.
+   */
+  export
+  interface IInputRequest {
+    /**
+     * Type of cell output.
+     */
+    output_type: 'input_request';
+
+    /**
+     * The text to show at the prompt.
+     */
+    prompt: string;
+
+    /**
+     * Whether the request is for a password.
+     * If so, the frontend shouldn't echo input.
+     */
+    password: boolean;
+
+    /**
+     * The kernel that made the request, used to send an input response.
+     */
+    kernel: IKernel;
+  }
+
+  /**
+   * A valid output area item.
+   */
+  export
+  type Output = nbformat.IOutput | IInputRequest;
 }
 
 
@@ -206,7 +263,11 @@ namespace Private {
    * A signal emitted when the model changes.
    */
   export
-  const changedSignal = new Signal<OutputAreaModel, IListChangedArgs<nbformat.IOutput>>();
+  const changedSignal = new Signal<OutputAreaModel, IListChangedArgs<OutputAreaModel.Output>>();
+
+  /**
+   * A signal emitted when the model is disposed.
+   */
   export
   const disposedSignal = new Signal<OutputAreaModel, void>();
 }

+ 142 - 20
src/notebook/output-area/widget.ts

@@ -2,8 +2,12 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  RenderMime, MimeMap
-} from '../../rendermime';
+  IKernel
+} from 'jupyter-js-services';
+
+import {
+  Drag, DropAction, DropActions, MimeData
+} from 'phosphor-dragdrop';
 
 import {
   IListChangedArgs, ListChangeType
@@ -26,8 +30,12 @@ import {
 } from 'phosphor-widget';
 
 import {
-  Drag, DropAction, DropActions, IDragEvent, MimeData
-} from 'phosphor-dragdrop';
+  RenderMime, MimeMap
+} from '../../rendermime';
+
+import {
+  defaultSanitizer
+} from '../../sanitizer';
 
 import {
   nbformat
@@ -37,10 +45,6 @@ import {
   OutputAreaModel
 } from './model';
 
-import {
-  defaultSanitizer
-} from '../../sanitizer';
-
 
 /**
  * The threshold in pixels to start a drag event.
@@ -92,6 +96,26 @@ const STDERR_CLASS = 'jp-Output-stderr';
  */
 const ERROR_CLASS = 'jp-Output-error';
 
+/**
+ * The class name added to stdin data.
+ */
+const STDIN_CLASS = 'jp-Output-stdin';
+
+/**
+ * The class name added to stdin data prompt nodes.
+ */
+const STDIN_PROMPT_CLASS = 'jp-Output-stdinPrompt';
+
+/**
+ * The class name added to stdin data input nodes.
+ */
+const STDIN_INPUT_CLASS = 'jp-Output-stdinInput';
+
+/**
+ * The class name added to stdin rendered text nodes.
+ */
+const STDIN_RENDERED_CLASS = 'jp-Output-stdinRendered';
+
 /**
  * The class name added to fixed height output areas.
  */
@@ -117,7 +141,8 @@ const RESULT_CLASS = 'jp-Output-result';
  * A list of outputs considered safe.
  */
 const safeOutputs = ['text/plain', 'image/png', 'image/jpeg',
-                     'application/vnd.jupyter.console-text'];
+                     'application/vnd.jupyter.console-text',
+                     'application/vnd.jupyter.console-stdin'];
 
 /**
  * A list of outputs that are sanitizable.
@@ -322,8 +347,7 @@ class OutputAreaWidget extends Widget {
       oldValue.changed.disconnect(this._onModelStateChanged, this);
       oldValue.disposed.disconnect(this._onModelDisposed, this);
     }
-    newValue.changed.connect(this._onModelStateChanged, this);
-    newValue.disposed.connect(this._onModelDisposed, this);
+
     let start = newValue ? newValue.length : 0;
     // Clear unnecessary child widgets.
     for (let i = start; i < layout.childCount(); i++) {
@@ -332,6 +356,10 @@ class OutputAreaWidget extends Widget {
     if (!newValue) {
       return;
     }
+
+    newValue.changed.connect(this._onModelStateChanged, this);
+    newValue.disposed.connect(this._onModelDisposed, this);
+
     // Reuse existing child widgets.
     for (let i = 0; i < layout.childCount(); i++) {
       this._updateChild(i);
@@ -391,12 +419,12 @@ class OutputAreaWidget extends Widget {
       break;
     case ListChangeType.Replace:
       // Only "clear" is supported by the model.
-
-      // When an output area is cleared and then quickly replaced with new content
-      // (as happens with @interact in widgets, for example), the quickly changing
-      // height can make the page jitter. We introduce a small delay in the minimum height
+      // When an output area is cleared and then quickly replaced with new
+      // content (as happens with @interact in widgets, for example), the
+      // quickly changing height can make the page jitter.
+      // We introduce a small delay in the minimum height
       // to prevent this jitter.
-      let rect = this.node.getBoundingClientRect()
+      let rect = this.node.getBoundingClientRect();
       let oldHeight = this.node.style.minHeight;
       this.node.style.minHeight = `${rect.height}px`;
       setTimeout(() => { this.node.style.minHeight = oldHeight; }, 50);
@@ -590,8 +618,8 @@ class OutputGutter extends Widget {
     });
 
     this._drag.mimeData.setData(FACTORY_MIME, () => {
-      let output_area = this.parent.parent as OutputAreaWidget;
-      return output_area.mirror();
+      let outputArea = this.parent.parent as OutputAreaWidget;
+      return outputArea.mirror();
     });
 
     // Remove mousemove and mouseup listeners and start the drag.
@@ -688,10 +716,17 @@ class OutputWidget extends Widget {
    *
    * @param trusted - Whether the output is trusted.
    */
-  render(output: nbformat.IOutput, trusted?: boolean): void {
+  render(output: OutputAreaModel.Output, trusted?: boolean): void {
+    // Handle an input request.
+    if (output.output_type === 'input_request') {
+      let child = new InputWidget(output as OutputAreaModel.IInputRequest);
+      this.setOutput(child);
+      return;
+    }
+
     // Extract the data from the output and sanitize if necessary.
     let rendermime = this._rendermime;
-    let bundle = this.getBundle(output);
+    let bundle = this.getBundle(output as nbformat.IOutput);
     let data = this.convertBundle(bundle);
     if (!trusted) {
       this.sanitize(data);
@@ -852,6 +887,89 @@ class OutputWidget extends Widget {
 }
 
 
+/**
+ * A widget that handles stdin requests from the kernel.
+ */
+ class InputWidget extends Widget {
+  /**
+   * Create the node for an InputWidget.
+   */
+  static createNode(): HTMLElement {
+    let node = document.createElement('div');
+    let prompt = document.createElement('span');
+    prompt.className = STDIN_PROMPT_CLASS;
+    let input = document.createElement('input');
+    input.className = STDIN_INPUT_CLASS;
+    node.appendChild(prompt);
+    node.appendChild(input);
+    return node;
+  }
+  /**
+   * Construct a new input widget.
+   */
+  constructor(request: OutputAreaModel.IInputRequest) {
+    super();
+    this.addClass(STDIN_CLASS);
+    let text = this.node.firstChild as HTMLElement;
+    text.textContent = request.prompt;
+    this._input = this.node.lastChild as HTMLInputElement;
+    if (request.password) {
+      this._input.type = 'password';
+    }
+    this._kernel = request.kernel;
+  }
+
+  /**
+   * Handle the DOM events for the widget.
+   *
+   * @param event - The DOM event sent to the widget.
+   *
+   * #### Notes
+   * This method implements the DOM `EventListener` interface and is
+   * called in response to events on the dock panel's node. It should
+   * not be called directly by user code.
+   */
+  handleEvent(event: Event): void {
+    let input = this._input;
+    if (event.type === 'keydown') {
+      if ((event as KeyboardEvent).keyCode === 13) {  // Enter
+        this._kernel.sendInputReply({
+          value: input.value
+        });
+        let rendered = document.createElement('span');
+        rendered.className = STDIN_RENDERED_CLASS;
+        if (input.type === 'password') {
+          rendered.textContent = Array(input.value.length + 1).join('·');
+        } else {
+          rendered.textContent = input.value;
+        }
+        this.node.replaceChild(rendered, input);
+      }
+      // Suppress keydown events from leaving the input.
+      event.stopPropagation();
+    }
+  }
+
+  /**
+   * Handle `after-attach` messages sent to the widget.
+   */
+  protected onAfterAttach(msg: Message): void {
+    this._input.focus();
+    this._input.addEventListener('keydown', this);
+  }
+
+  /**
+   * Handle `before-detach` messages sent to the widget.
+   */
+  protected onBeforeDetach(msg: Message): void {
+    this._input.removeEventListener('keydown', this);
+  }
+
+  private _kernel: IKernel = null;
+  private _input: HTMLInputElement = null;
+}
+
+
 /**
  * A namespace for OutputArea statics.
  */
@@ -879,6 +997,10 @@ namespace Private {
    */
   export
   const modelChangedSignal = new Signal<OutputAreaWidget, void>();
+
+  /**
+   * A signal emitted when the widget's model is disposed.
+   */
   export
   const modelDisposedSignal = new Signal<OutputAreaWidget, void>();
 }

+ 6 - 1
src/notebook/theme.css

@@ -116,6 +116,11 @@
 }
 
 
+.jp-Output-stdinPrompt {
+  padding-right: 8px;
+}
+
+
 .jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-active.jp-mod-selected {
   border-color: #ABABAB;
   border-left-width: 1px;
@@ -310,4 +315,4 @@
   padding: 0;
   border: 0;
   border-radius: 0;
-}
+}

+ 1 - 1
src/renderers/index.ts

@@ -41,7 +41,7 @@ class HTMLWidget extends Widget {
   constructor(html: string) {
     super();
     try {
-      var range = document.createRange();
+      let range = document.createRange();
       this.node.appendChild(range.createContextualFragment(html));
     } catch (error) {
       console.warn('Environment does not support Range ' +

+ 1 - 1
test/src/notebook/output-area/model.spec.ts

@@ -97,7 +97,7 @@ describe('notebook/output-area/model', () => {
           expect(args.oldIndex).to.be(-1);
           expect(args.newIndex).to.be(0);
           expect(args.oldValue).to.be(void 0);
-          expect(deepEqual(args.newValue, DEFAULT_OUTPUTS[0]));
+          expect(deepEqual(args.newValue as nbformat.IOutput, DEFAULT_OUTPUTS[0]));
           called = true;
         });
         model.add(DEFAULT_OUTPUTS[0]);