浏览代码

[work in progress]

A. Darian 9 年之前
父节点
当前提交
1c4f9d8ec2
共有 4 个文件被更改,包括 107 次插入39 次删除
  1. 30 12
      src/notebook/completion/model.ts
  2. 43 7
      src/notebook/completion/widget.ts
  3. 33 20
      src/notebook/console/model.ts
  4. 1 0
      src/notebook/theme.css

+ 30 - 12
src/notebook/completion/model.ts

@@ -6,6 +6,10 @@ import {
   IDisposable
 } from 'phosphor-disposable';
 
+import {
+  IChangedArgs
+} from 'phosphor-properties';
+
 import {
   ISignal, Signal, clearSignalData
 } from 'phosphor-signaling';
@@ -18,9 +22,9 @@ import {
 export
 interface ICompletionModel extends IDisposable {
   /**
-   * A signal emitted when choosable completion options change.
+   * A signal emitted when state of the completion menu changes.
    */
-  optionsChanged: ISignal<ICompletionModel, void>;
+  stateChanged: ISignal<ICompletionModel, IChangedArgs<any>>;
 
   /**
    * The list of filtered options, including any `<mark>`ed characters.
@@ -51,10 +55,10 @@ class CompletionModel implements ICompletionModel {
   }
 
   /**
-   * A signal emitted when choosable completion options change.
+   * A signal emitted when state of the completion menu changes.
    */
-  get optionsChanged(): ISignal<ICompletionModel, void> {
-    return Private.optionsChangedSignal.bind(this);
+  get stateChanged(): ISignal<ICompletionModel, IChangedArgs<any>> {
+    return Private.stateChangedSignal.bind(this);
   }
 
   /**
@@ -68,9 +72,16 @@ class CompletionModel implements ICompletionModel {
   get options(): string[] {
     return this._filter();
   }
-  set options(options: string[]) {
-    this._options = options;
-    this.optionsChanged.emit(void 0);
+  set options(newValue: string[]) {
+    let oldValue = this._options;
+    if (newValue) {
+      this._options = [];
+      this._options.push(...newValue);
+    } else {
+      this._options = null;
+    }
+    let name = 'options';
+    this.stateChanged.emit({ name, oldValue, newValue });
   }
 
   /**
@@ -90,8 +101,11 @@ class CompletionModel implements ICompletionModel {
   get current(): ITextChange {
     return this._current;
   }
-  set current(change: ITextChange) {
-    this._current = change;
+  set current(newValue: ITextChange) {
+    let oldValue = this._current;
+    this._current = newValue;
+    let name = 'current';
+    this.stateChanged.emit({ name, oldValue, newValue });
   }
 
   /**
@@ -110,6 +124,10 @@ class CompletionModel implements ICompletionModel {
    * Apply the query to the complete options list to return the matching subset.
    */
   private _filter(): string[] {
+    let original = this._original;
+    let current = this._current;
+    console.log('original', original && original.value);
+    console.log('current', current && current.newValue);
     return this._options;
   }
 
@@ -122,8 +140,8 @@ class CompletionModel implements ICompletionModel {
 
 namespace Private {
   /**
-   * A signal emitted when choosable completion options change.
+   * A signal emitted when state of the completion menu changes.
    */
   export
-  const optionsChangedSignal = new Signal<ICompletionModel, void>();
+  const stateChangedSignal = new Signal<ICompletionModel, IChangedArgs<any>>();
 }

+ 43 - 7
src/notebook/completion/widget.ts

@@ -6,6 +6,10 @@ import {
   Message
 } from 'phosphor-messaging';
 
+import {
+  IChangedArgs
+} from 'phosphor-properties';
+
 import {
   Widget
 } from 'phosphor-widget';
@@ -46,7 +50,7 @@ class CompletionWidget extends Widget {
   constructor(model: ICompletionModel) {
     super();
     this._model = model;
-    this._model.optionsChanged.connect(this.onOptionsChanged, this);
+    this._model.stateChanged.connect(this.onModelChanged, this);
     this.addClass(COMPLETION_CLASS);
     this.update();
   }
@@ -106,9 +110,11 @@ class CompletionWidget extends Widget {
    * Handle `after_attach` messages for the widget.
    *
    * #### Notes
-   * Captures document events in capture phase to dismiss completion widget.
+   * Captures document events in capture phase to dismiss or navigate the
+   * completion widget.
    */
   protected onAfterAttach(msg: Message): void {
+    document.addEventListener('keydown', this, true);
     document.addEventListener('mousedown', this, true);
   }
 
@@ -116,6 +122,7 @@ class CompletionWidget extends Widget {
    * Handle `before_detach` messages for the widget.
    */
   protected onBeforeDetach(msg: Message): void {
+    document.removeEventListener('keydown', this);
     document.removeEventListener('mousedown', this);
   }
 
@@ -144,16 +151,19 @@ class CompletionWidget extends Widget {
 
     if (this.isHidden) this.show();
 
-    let availableHeight = this._model.original.coords.top;
+    let coords = this._model.current ? this._model.current.coords
+      : this._model.original.coords;
+    let availableHeight = coords.top;
     let maxHeight = Math.min(availableHeight, MAX_HEIGHT);
     node.style.maxHeight = `${maxHeight}px`;
 
     // Account for 1px border width.
-    let left = Math.floor(this._model.original.coords.left) + 1;
+    let left = Math.floor(coords.left) + 1;
     let rect = node.getBoundingClientRect();
     let top = maxHeight - rect.height;
     node.style.left = `${left}px`;
     node.style.top = `${top}px`;
+    node.style.width = 'auto';
     // Expand the menu width by the scrollbar size, if present.
     if (node.scrollHeight > maxHeight) {
       node.style.width = `${2 * node.offsetWidth - node.clientWidth}px`;
@@ -162,10 +172,19 @@ class CompletionWidget extends Widget {
   }
 
   /**
-   * Handle option changes from the model.
+   * Handle a model state change event.
    */
-  protected onOptionsChanged(sender: ICompletionModel, args: void): void {
-    this.options = this._model.options;
+  protected onModelChanged(sender: ICompletionModel, args: IChangedArgs<any>) {
+    switch (args.name) {
+      case 'current':
+        console.log('current updated');
+        this.update();
+        return;
+      case 'options':
+        console.log('options updated');
+        this.options = args.newValue;
+        return;
+    }
   }
 
   /**
@@ -183,6 +202,23 @@ class CompletionWidget extends Widget {
     this.hide();
   }
 
+  /**
+   * Handle keydown events for the widget.
+   */
+  private _evtKeydown(event: KeyboardEvent) {
+    let target = event.target as HTMLElement;
+    while (target !== document.documentElement) {
+      if (target === this.node) {
+        console.log('boom');
+        event.preventDefault();
+        event.stopPropagation();
+        return;
+      }
+      target = target.parentElement;
+    }
+    this.hide();
+  }
+
   private _model: ICompletionModel = null;
   private _options: string[] = null;
   private _reference: Widget = null;

+ 33 - 20
src/notebook/console/model.ts

@@ -3,7 +3,7 @@
 'use strict';
 
 import {
-  INotebookSession, IInspectReply, ICompleteReply
+  INotebookSession, IInspectReply, ICompleteRequest, ICompleteReply
 } from 'jupyter-js-services';
 
 import {
@@ -480,29 +480,12 @@ class ConsoleModel implements IConsoleModel {
    * Handle a completion request in the prompt model.
    */
   protected onCompletionRequest(sender: any, args: ICompletionRequest) {
-    let pendingComplete = ++this._pendingComplete;
     let contents = { code: args.value, cursor_pos: args.ch };
-
     // If there is no session, no requests can be sent to the API.
     if (!this._session) {
       return;
     }
-
-    this._session.kernel.complete(contents).then((value: ICompleteReply) => {
-      // If model has been disposed, bail.
-      if (this.isDisposed) {
-        return;
-      }
-      // If a newer completion requesy has created a pending request, bail.
-      if (pendingComplete !== this._pendingComplete) {
-        return;
-      }
-      // Completion request failures or negative results fail silently.
-      if (value.status !== 'ok') {
-        return;
-      }
-      // Update the completion model options and request.
-      this._completion.options = value.matches;
+    this._complete(contents).then(() => {
       this._completion.original = args;
     });
   }
@@ -521,15 +504,24 @@ class ConsoleModel implements IConsoleModel {
       return;
     }
 
+    // If there is currently a completion
+    if (this._completion && this.completion.options) {
+      let contents = { code: args.newValue, cursor_pos: args.ch };
+      this._complete(contents).then(() => {
+        this._completion.current = args;
+      });
+    }
+
     // If final character of current line isn't a whitespace character, bail.
     let currentLine = args.newValue.split('\n')[args.line];
     if (!currentLine.match(/\S$/)) {
       this.tooltip = null;
+      this.completion.options = null;
       return;
     }
 
-    let pendingInspect = ++this._pendingInspect;
     let contents = { code: currentLine, cursor_pos: args.ch, detail_level: 0 };
+    let pendingInspect = ++this._pendingInspect;
 
     this._session.kernel.inspect(contents).then((value: IInspectReply) => {
       // If model has been disposed, bail.
@@ -576,6 +568,27 @@ class ConsoleModel implements IConsoleModel {
     }
   }
 
+  private _complete(contents: ICompleteRequest): Promise<void> {
+    let pendingComplete = ++this._pendingComplete;
+    return this._session.kernel.complete(contents).then((value: ICompleteReply) => {
+      // If model has been disposed, bail.
+      if (this.isDisposed) {
+        return;
+      }
+      // If a newer completion requesy has created a pending request, bail.
+      if (pendingComplete !== this._pendingComplete) {
+        return;
+      }
+      // Completion request failures or negative results fail silently.
+      if (value.status !== 'ok') {
+        return;
+      }
+      // Update the completion model options and request.
+      this._completion.options = value.matches;
+      return null;
+    });
+  }
+
   private _banner: IRawCellModel = null;
   private _bannerText: string = '...';
   private _cells: IObservableList<ICellModel> = null;

+ 1 - 0
src/notebook/theme.css

@@ -485,6 +485,7 @@
 
 .jp-Completion-item {
   margin: 0;
+  min-width: 150px;
   padding: 0;
 }