瀏覽代碼

Remove backwards compatabilities from completer API refactor

Edward Zhao 5 年之前
父節點
當前提交
db055b0229
共有 3 個文件被更改,包括 331 次插入30 次删除
  1. 100 7
      packages/completer/src/handler.ts
  2. 89 0
      packages/completer/src/model.ts
  3. 142 23
      packages/completer/src/widget.ts

+ 100 - 7
packages/completer/src/handler.ts

@@ -7,7 +7,12 @@ import { Text } from '@jupyterlab/coreutils';
 
 import { IDataConnector } from '@jupyterlab/statedb';
 
-import { ReadonlyJSONObject, JSONObject, JSONArray } from '@lumino/coreutils';
+import {
+  ReadonlyJSONObject,
+  JSONObject,
+  JSONArray,
+  PartialJSONObject
+} from '@lumino/coreutils';
 
 import { IDisposable } from '@lumino/disposable';
 
@@ -397,6 +402,16 @@ export class CompletionHandler implements IDisposable {
 
     // Update the original request.
     model.original = state;
+    // Update the cursor.
+    model.cursor = {
+      start: Text.charIndexToJsIndex(reply.start, text),
+      end: Text.charIndexToJsIndex(reply.end, text)
+    };
+
+    if (reply.items && model.setCompletionItems) {
+      model.setCompletionItems(reply.items);
+      return;
+    }
 
     // Dedupe the matches.
     const matches: string[] = [];
@@ -434,12 +449,6 @@ export class CompletionHandler implements IDisposable {
 
     // Update the options, including the type map.
     model.setOptions(matches, typeMap);
-
-    // Update the cursor.
-    model.cursor = {
-      start: Text.charIndexToJsIndex(reply.start, text),
-      end: Text.charIndexToJsIndex(reply.end, text)
-    };
   }
 
   private _connector: IDataConnector<
@@ -477,6 +486,85 @@ export namespace CompletionHandler {
     connector: IDataConnector<IReply, void, IRequest>;
   }
 
+  /**
+   * Wrapper object for ICompletionItem.
+   * Implementers of this interface should be responsible for
+   * deduping and sorting the items in the list.
+   */
+  export interface ICompletionItems {
+    /**
+     * Collection of completion items.
+     */
+    items: Array<ICompletionItem>;
+  }
+
+  /**
+   * Completion item object based off of LSP CompletionItem
+   */
+  export interface ICompletionItem extends PartialJSONObject {
+    /**
+     * User facing completion.
+     * If insertText is not set, this will be inserted.
+     */
+    label: string;
+
+    /**
+     * Completion to be inserted.
+     */
+    insertText?: string;
+
+    /**
+     * Range to be replaced by this completion.
+     */
+    range?: IRange;
+
+    /**
+     * Type of this completion item.
+     */
+    type?: string;
+
+    /**
+     * Image url for icon to be rendered with completion type.
+     */
+    icon?: string;
+
+    /**
+     * A human-readable string with additional information
+     * about this item, like type or symbol information.
+     */
+    documentation?: string;
+
+    /**
+     * A string used to help filter a set of completion items.
+     */
+    filterText?: string;
+
+    /**
+     * Indicates if the item is deprecated.
+     */
+    deprecated?: boolean;
+
+    /**
+     * Any metadata that accompanies this completion item.
+     */
+    data?: any;
+  }
+
+  /**
+   * Replacement range of completion item.
+   */
+  export interface IRange extends PartialJSONObject {
+    /**
+     * The starting index for the substring being replaced by completion.
+     */
+    start: number;
+
+    /**
+     * The end index for the substring being replaced by completion.
+     */
+    end: number;
+  }
+
   /**
    * A reply to a completion request.
    */
@@ -500,6 +588,11 @@ export namespace CompletionHandler {
      * Any metadata that accompanies the completion reply.
      */
     metadata: ReadonlyJSONObject;
+
+    /**
+     * Hook for extensions to send ICompletionItems.
+     */
+    items?: ICompletionItems;
   }
 
   /**

+ 89 - 0
packages/completer/src/model.ts

@@ -15,6 +15,7 @@ import { StringExt } from '@lumino/algorithm';
 
 import { ISignal, Signal } from '@lumino/signaling';
 
+import { CompletionHandler } from './handler';
 import { Completer } from './widget';
 
 /**
@@ -165,6 +166,36 @@ export class CompleterModel implements Completer.IModel {
     Signal.clearData(this);
   }
 
+  /**
+   * The list of visible items in the completer menu.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  completionItems?(): CompletionHandler.ICompletionItems {
+    let query = this._query;
+    if (!query) {
+      return this._completionItems;
+    }
+    this._markup(query);
+    return this._completionItems;
+  }
+
+  /**
+   * Set the list of visible items in the completer menu, and append any
+   * new types to KNOWN_TYPES.
+   */
+  setCompletionItems?(newValue: CompletionHandler.ICompletionItems): void {
+    if (JSONExt.deepEqual(newValue.items, this._completionItems.items)) {
+      return;
+    }
+    this._completionItems = newValue;
+    this._orderedTypes = Private.findOrderedCompletionItemTypes(
+      this._completionItems.items
+    );
+    this._stateChanged.emit(undefined);
+  }
+
   /**
    * The list of visible items in the completer menu.
    *
@@ -355,6 +386,33 @@ export class CompleterModel implements Completer.IModel {
     this._stateChanged.emit(undefined);
   }
 
+  /**
+   * Check if CompletionItem matches against query.
+   * Highlight matching prefix by adding <mark> tags.
+   */
+  private _markup(query: string): void {
+    let items = this._completionItems.items;
+    let results: CompletionHandler.ICompletionItem[] = [];
+    for (let item of items) {
+      // See if insert text matches query string
+      let match = StringExt.matchSumOfSquares(item.label, query);
+      if (match) {
+        // Highlight label text if there's a match
+        let marked = StringExt.highlight(
+          item.label,
+          match.indices,
+          Private.mark
+        );
+        if (!item.insertText) {
+          item.insertText = item.label;
+        }
+        item.label = marked.join('');
+        results.push(item);
+      }
+    }
+    this._completionItems.items = results;
+  }
+
   /**
    * Apply the query to the complete options list to return the matching subset.
    */
@@ -388,6 +446,7 @@ export class CompleterModel implements Completer.IModel {
   private _reset(): void {
     this._current = null;
     this._cursor = null;
+    this._completionItems = { items: [] };
     this._options = [];
     this._original = null;
     this._query = '';
@@ -399,6 +458,7 @@ export class CompleterModel implements Completer.IModel {
   private _current: Completer.ITextState | null = null;
   private _cursor: Completer.ICursorSpan | null = null;
   private _isDisposed = false;
+  private _completionItems: CompletionHandler.ICompletionItems = { items: [] };
   private _options: string[] = [];
   private _original: Completer.ITextState | null = null;
   private _query = '';
@@ -469,6 +529,35 @@ namespace Private {
     return a.raw.localeCompare(b.raw);
   }
 
+  /**
+   * Compute a reliably ordered list of types for ICompletionItems.
+   *
+   * #### Notes
+   * The resulting list always begins with the known types:
+   * ```
+   * ['function', 'instance', 'class', 'module', 'keyword']
+   * ```
+   * followed by other types in alphabetical order.
+   *
+   */
+  export function findOrderedCompletionItemTypes(
+    items: CompletionHandler.ICompletionItem[]
+  ): string[] {
+    const newTypeSet = new Set<string>();
+    items.forEach(item => {
+      if (
+        item.type &&
+        !KNOWN_TYPES.includes(item.type) &&
+        !newTypeSet.has(item.type!)
+      ) {
+        newTypeSet.add(item.type!);
+      }
+    });
+    const newTypes = Array.from(newTypeSet);
+    newTypes.sort((a, b) => a.localeCompare(b));
+    return KNOWN_TYPES.concat(newTypes);
+  }
+
   /**
    * Compute a reliably ordered list of types.
    *

+ 142 - 23
packages/completer/src/widget.ts

@@ -19,6 +19,8 @@ import { ISignal, Signal } from '@lumino/signaling';
 
 import { Widget } from '@lumino/widgets';
 
+import { CompletionHandler } from './handler';
+
 /**
  * The class name added to completer menu items.
  */
@@ -225,6 +227,85 @@ export class Completer extends Widget {
       return;
     }
 
+    let node: HTMLElement | null = null;
+
+    if (model.completionItems && model.completionItems().items.length) {
+      node = this._createCompletionItemNode(model);
+    } else {
+      node = this._createIItemNode(model);
+    }
+    if (!node) {
+      return;
+    }
+
+    let active = node.querySelectorAll(`.${ITEM_CLASS}`)[this._activeIndex];
+    active.classList.add(ACTIVE_CLASS);
+
+    // If this is the first time the current completer session has loaded,
+    // populate any initial subset match.
+    if (!model.query) {
+      const populated = this._populateSubset();
+      if (populated) {
+        this.update();
+        return;
+      }
+    }
+
+    if (this.isHidden) {
+      this.show();
+      this._setGeometry();
+      this._visibilityChanged.emit(undefined);
+    } else {
+      this._setGeometry();
+    }
+  }
+
+  private _createCompletionItemNode(
+    model: Completer.IModel
+  ): HTMLElement | null {
+    let items = model.completionItems && model.completionItems().items;
+
+    // If there are no items, reset and bail.
+    if (!items || !items.length) {
+      this._resetFlag = true;
+      this.reset();
+      if (!this.isHidden) {
+        this.hide();
+        this._visibilityChanged.emit(undefined);
+      }
+      return null;
+    }
+
+    // If there is only one option, signal and bail.
+    // We don't test the filtered `items`, as that
+    // is too aggressive of completer behavior, it can
+    // lead to double typing of an option.
+    if (items.length === 1) {
+      this._selected.emit(items[0].insertText || items[0].label);
+      this.reset();
+      return null;
+    }
+
+    // Clear the node.
+    let node = this.node;
+    node.textContent = '';
+
+    // Compute an ordered list of all the types in the typeMap, this is computed
+    // once by the model each time new data arrives for efficiency.
+    let orderedTypes = model.orderedTypes();
+
+    // Populate the completer items.
+    for (let item of items) {
+      if (!this._renderer.createCompletionItemNode) {
+        return null;
+      }
+      let li = this._renderer.createCompletionItemNode(item, orderedTypes);
+      node.appendChild(li);
+    }
+    return node;
+  }
+
+  private _createIItemNode(model: Completer.IModel): HTMLElement | null {
     let items = toArray(model.items());
 
     // If there are no items, reset and bail.
@@ -235,7 +316,7 @@ export class Completer extends Widget {
         this.hide();
         this._visibilityChanged.emit(undefined);
       }
-      return;
+      return null;
     }
 
     // If there is only one option, signal and bail.
@@ -246,7 +327,7 @@ export class Completer extends Widget {
     if (options.length === 1) {
       this._selected.emit(options[0]);
       this.reset();
-      return;
+      return null;
     }
 
     // Clear the node.
@@ -266,27 +347,7 @@ export class Completer extends Widget {
       );
       node.appendChild(li);
     }
-
-    let active = node.querySelectorAll(`.${ITEM_CLASS}`)[this._activeIndex];
-    active.classList.add(ACTIVE_CLASS);
-
-    // If this is the first time the current completer session has loaded,
-    // populate any initial subset match.
-    if (!model.query) {
-      const populated = this._populateSubset();
-      if (populated) {
-        this.update();
-        return;
-      }
-    }
-
-    if (this.isHidden) {
-      this.show();
-      this._setGeometry();
-      this._visibilityChanged.emit(undefined);
-    } else {
-      this._setGeometry();
-    }
+    return node;
   }
 
   /**
@@ -605,6 +666,16 @@ export namespace Completer {
      */
     query: string;
 
+    /**
+     * Get the list of visible CompletionItems in the completer menu.
+     */
+    completionItems?(): CompletionHandler.ICompletionItems;
+
+    /**
+     * Set the list of visible CompletionItems in the completer menu.
+     */
+    setCompletionItems?(items: CompletionHandler.ICompletionItems): void;
+
     /**
      * Get the of visible items in the completer menu.
      */
@@ -711,6 +782,15 @@ export namespace Completer {
    * A renderer for completer widget nodes.
    */
   export interface IRenderer {
+    /**
+     * Create an item node (an `li` element)  from a ICompletionItem
+     * for a text completer menu.
+     */
+    createCompletionItemNode?(
+      item: CompletionHandler.ICompletionItem,
+      orderedTypes: string[]
+    ): HTMLLIElement;
+
     /**
      * Create an item node (an `li` element) for a text completer menu.
      */
@@ -725,6 +805,45 @@ export namespace Completer {
    * The default implementation of an `IRenderer`.
    */
   export class Renderer implements IRenderer {
+    /**
+     * Create an item node from an ICompletionItem for a text completer menu.
+     */
+    createCompletionItemNode(
+      item: CompletionHandler.ICompletionItem,
+      orderedTypes: string[]
+    ): HTMLLIElement {
+      let li = document.createElement('li');
+      li.className = ITEM_CLASS;
+      // Set the raw, un-marked up value as a data attribute.
+      li.setAttribute('data-value', item.insertText || item.label);
+
+      let matchNode = document.createElement('code');
+      matchNode.className = 'jp-Completer-match';
+      // Use innerHTML because search results include <mark> tags.
+      matchNode.innerHTML = defaultSanitizer.sanitize(item.label, {
+        allowedTags: ['mark']
+      });
+
+      if (item.type) {
+        let typeNode = document.createElement('span');
+        let type = item.type;
+        typeNode.textContent = (type[0] || '').toLowerCase();
+        let colorIndex = (orderedTypes.indexOf(type) % N_COLORS) + 1;
+        typeNode.className = 'jp-Completer-type';
+        typeNode.setAttribute(`data-color-index`, colorIndex.toString());
+        li.title = type;
+        let typeExtendedNode = document.createElement('code');
+        typeExtendedNode.className = 'jp-Completer-typeExtended';
+        typeExtendedNode.textContent = type.toLocaleLowerCase();
+        li.appendChild(typeNode);
+        li.appendChild(matchNode);
+        li.appendChild(typeExtendedNode);
+      } else {
+        li.appendChild(matchNode);
+      }
+      return li;
+    }
+
     /**
      * Create an item node for a text completer menu.
      */