Browse Source

[work in progress] completion widget

A. Darian 9 years ago
parent
commit
e00ddc8f86

+ 29 - 8
src/notebook/completion/model.ts

@@ -10,6 +10,10 @@ import {
   ISignal, Signal, clearSignalData
 } from 'phosphor-signaling';
 
+import {
+  ICompletionRequest, ITextChange
+} from '../editor/model';
+
 
 export
 interface ICompletionModel extends IDisposable {
@@ -24,9 +28,14 @@ interface ICompletionModel extends IDisposable {
   options: string[];
 
   /**
-   * The query string used to filter options.
+   * The original completion request details.
+   */
+  original: ICompletionRequest;
+
+  /**
+   * The current text change details.
    */
-  query: string;
+  current: ITextChange;
 }
 
 /**
@@ -72,13 +81,24 @@ class CompletionModel implements ICompletionModel {
   }
 
   /**
-   * The query string used to filter options.
+   * The original completion request details.
+   */
+  get original(): ICompletionRequest {
+    return this._original;
+  }
+  set original(request: ICompletionRequest) {
+    this._original = request;
+    this._current = null;
+  }
+
+  /**
+   * The current text change details.
    */
-  get query(): string {
-    return this._query;
+  get current(): ITextChange {
+    return this._current;
   }
-  set query(newValue: string) {
-    this._query = newValue;
+  set current(change: ITextChange) {
+    this._current = change;
   }
 
   /**
@@ -102,7 +122,8 @@ class CompletionModel implements ICompletionModel {
 
   private _isDisposed = false;
   private _options: string[] = null;
-  private _query = '';
+  private _original: ICompletionRequest = null;
+  private _current: ITextChange = null;
 }
 
 

+ 50 - 34
src/notebook/completion/widget.ts

@@ -22,12 +22,17 @@ const COMPLETION_CLASS = 'jp-Completion';
 /**
  * The class name added to completion menu contents.
  */
-const CONTENT_CLASS = 'jp-Completion-content';
+const ITEM_CLASS = 'jp-Completion-item';
 
 /**
- * The class name added to completion menu contents.
+ * The maximum height of a completion widget.
  */
-const ITEM_CLASS = 'jp-Completion-item';
+const MAX_HEIGHT = 250;
+
+/**
+ * The offset to add to the widget width if a scrollbar exists.
+ */
+const SCROLLBAR_OFFSET = 20;
 
 
 export
@@ -36,10 +41,7 @@ class CompletionWidget extends Widget {
    * Create the DOM node for a text completion menu.
    */
   static createNode(): HTMLElement {
-    let node = document.createElement('div');
-    let ul = document.createElement('ul');
-    ul.className = CONTENT_CLASS;
-    node.appendChild(ul);
+    let node = document.createElement('ul');
     return node;
   }
 
@@ -49,19 +51,9 @@ class CompletionWidget extends Widget {
   constructor(model: ICompletionModel) {
     super();
     this._model = model;
-    this.addClass(COMPLETION_CLASS);
-    this.hide();
     this._model.optionsChanged.connect(this.onOptionsChanged, this);
-  }
-
-  /**
-   * The list element of the completion widget.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get listNode(): HTMLElement {
-    return this.node.getElementsByTagName('ul')[0];
+    this.addClass(COMPLETION_CLASS);
+    this.update();
   }
 
   /**
@@ -142,22 +134,46 @@ class CompletionWidget extends Widget {
    * Handle `update_request` messages.
    */
   protected onUpdateRequest(msg: Message): void {
-    let list = this.listNode;
-    list.textContent = '';
-    if (this._options && this._options.length) {
-      let list = this.listNode;
-      for (let i = 0, len = this._options.length; i < len; i++) {
-        let item = document.createElement('li');
-        let code = document.createElement('code');
-
-        // Use innerHTML because search results include <mark> tags.
-        code.innerHTML = this._options[i];
-        item.className = ITEM_CLASS;
-        item.appendChild(code);
-        list.appendChild(item);
-      }
+    let node = this.node;
+    node.textContent = '';
+
+    if (!this._options || !this._options.length) {
+      this.hide();
+      return;
+    }
+
+    for (let i = 0, len = this._options.length; i < len; i++) {
+      let li = document.createElement('li');
+      let code = document.createElement('code');
+
+      // Use innerHTML because search results include <mark> tags.
+      code.innerHTML = this._options[i];
+      li.className = ITEM_CLASS;
+      li.appendChild(code);
+      node.appendChild(li);
+    }
+
+    if (this.isHidden) this.show();
+
+    let availableHeight = this._model.original.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 rect = node.getBoundingClientRect();
+    let top = maxHeight - rect.height;
+    node.style.left = `${left}px`;
+    node.style.top = `${top}px`;
+
+    // If a scrollbar is necessary, add padding to prevent horizontal scrollbar.
+    let lineHeight = node.getElementsByTagName('li')[0]
+      .getBoundingClientRect().height;
+    if (lineHeight * this._options.length > maxHeight) {
+      node.style.paddingRight = `${SCROLLBAR_OFFSET}px`;
+    } else {
+      node.style.paddingRight = `0px`;
     }
-    this.show();
   }
 
   /**

+ 22 - 5
src/notebook/console/model.ts

@@ -145,6 +145,11 @@ interface IConsoleModel extends IDisposable {
    */
   history: IConsoleHistory;
 
+  /**
+   * The console's prompt, a code cell model.
+   */
+  prompt: ICodeCellModel;
+
   /**
    * The optional notebook session associated with the console model.
    */
@@ -212,14 +217,10 @@ class ConsoleModel implements IConsoleModel {
     this._cells = new ObservableList<ICellModel>();
     this._completion = new CompletionModel();
     this._history = new ConsoleHistory(this._session && this._session.kernel);
-    this._prompt = this.createPrompt();
 
     // The first cell in a console is always the banner.
     this._cells.add(this._banner);
 
-    // The last cell in a console is always the prompt.
-    this._cells.add(this._prompt);
-
     this._cells.changed.connect(this.onCellsChanged, this);
   }
 
@@ -279,6 +280,20 @@ class ConsoleModel implements IConsoleModel {
     return this._completion;
   }
 
+  /**
+   * The console's prompt, a code cell model.
+   */
+  get prompt(): ICodeCellModel {
+    return this._prompt;
+  }
+  set prompt(newValue: ICodeCellModel) {
+    if (newValue === this._prompt) {
+      return;
+    }
+    this._prompt = newValue;
+    this._cells.add(newValue);
+  }
+
   /**
    * The default mimetype for cells new code cells.
    */
@@ -486,8 +501,9 @@ class ConsoleModel implements IConsoleModel {
       if (value.status !== 'ok') {
         return;
       }
-      // Update the completion model options.
+      // Update the completion model options and request.
       this._completion.options = value.matches;
+      this._completion.original = args;
     });
   }
 
@@ -648,6 +664,7 @@ namespace Private {
       model.clear();
       // Update the console banner.
       model.banner = info.banner;
+      model.prompt = model.createPrompt();
     });
   }
 

+ 9 - 8
src/notebook/console/widget.ts

@@ -219,15 +219,12 @@ class ConsoleWidget extends Widget {
       let cell = factory(model.cells.get(i), this._rendermime);
       layout.addChild(cell);
     }
-    let banner = layout.childAt(0);
-    banner.addClass(BANNER_CLASS);
+    layout.childAt(0).addClass(BANNER_CLASS);
 
     model.cells.changed.connect(this.onCellsChanged, this);
     model.stateChanged.connect(this.onModelChanged, this);
 
-    // Hide the console until banner is set.
     this.addClass(CONSOLE_CLASS);
-    this.hide();
   }
 
   /**
@@ -293,6 +290,10 @@ class ConsoleWidget extends Widget {
   protected onModelChanged(sender: IConsoleModel, args: IChangedArgs<ITooltipModel>) {
     let constructor = this.constructor as typeof ConsoleWidget;
     switch (args.name) {
+    case 'banner':
+      let prompt = this.prompt;
+      if (prompt) prompt.input.editor.focus();
+      return;
     case 'tooltip':
       let model = args.newValue;
 
@@ -305,8 +306,8 @@ class ConsoleWidget extends Widget {
 
       // Offset the height of the tooltip by the height of cursor characters.
       top += model.change.chHeight;
-      // Offset the width of the tooltip by the width of cursor characters.
-      left -= model.change.chWidth;
+      // Account for 1px border width.
+      left += 1;
 
       // Account for 1px border on top and bottom.
       let maxHeight = window.innerHeight - top - 2;
@@ -321,8 +322,8 @@ class ConsoleWidget extends Widget {
 
       this._tooltip.rect = {top, left} as ClientRect;
       this._tooltip.content = content;
-      this._tooltip.node.style.maxHeight = maxHeight + 'px';
-      this._tooltip.node.style.maxWidth = maxWidth + 'px';
+      this._tooltip.node.style.maxHeight = `${maxHeight}px`;
+      this._tooltip.node.style.maxWidth = `${maxWidth}px`;
       if (this._tooltip.isHidden) this._tooltip.show();
       return;
     }

+ 6 - 9
src/notebook/theme.css

@@ -472,22 +472,19 @@
 
 
 .jp-Completion {
-  position: absolute;
   background: #eeeeee;
   border: 1px solid #000000;
-  /* Allow for eight lines of text + padding. */
-  max-height: 254px;
-  padding: 2px;
-  z-index: 10001;
-}
-
-
-.jp-Completion-content {
   list-style-type: none;
+  margin: 0;
+  overflow: auto;
+  padding: 0 2px;
+  position: absolute;
+  z-index: 10001;
 }
 
 
 .jp-Completion-item {
+  margin: 0;
   padding: 0;
 }