Eric Charles пре 4 година
родитељ
комит
65103a0900

+ 7 - 2
packages/filebrowser/src/listing.ts

@@ -32,6 +32,7 @@ import {
 import {
   ArrayExt,
   ArrayIterator,
+  StringExt,
   each,
   filter,
   find,
@@ -52,6 +53,8 @@ import { ISignal, Signal } from '@lumino/signaling';
 
 import { Widget } from '@lumino/widgets';
 
+import { VirtualDOM, h } from '@lumino/virtualdom';
+
 import { FilterFileBrowserModel } from './model';
 
 /**
@@ -1886,8 +1889,10 @@ export namespace DirListing {
         node.removeAttribute('data-is-dot');
       }
       // If an item is being edited currently, its text node is unavailable.
-      if (text && text.textContent !== model.name) {
-        text.textContent = model.name;
+      if (text) {
+        const indices = !model.indices ? [] : model.indices;
+        let highlightedName = StringExt.highlight(model.name, indices, h.mark);
+        VirtualDOM.render(h.div(highlightedName), text);
       }
 
       let modText = '';

+ 81 - 3
packages/filebrowser/src/search.tsx

@@ -3,6 +3,8 @@
 
 import React, { useState } from 'react';
 
+import { StringExt } from '@lumino/algorithm';
+
 import { InputGroup } from '@jupyterlab/ui-components';
 
 import { ReactWidget } from '@jupyterlab/apputils';
@@ -19,6 +21,74 @@ export interface IFilterBoxProps {
   placeholder?: string;
 }
 
+/**
+ * A text match score with associated content item.
+ */
+interface IScore {
+  /**
+   * The numerical score for the text match.
+   */
+  score: number;
+
+  indices: number[] | null;
+
+  /**
+   * The command item associated with the match.
+   */
+  item: Contents.IModel;
+}
+
+/**
+ * Perform a fuzzy search on a single item.
+ */
+function fuzzySearch(item: Contents.IModel, query: string): IScore | null {
+  let source = `${item.name}`;
+
+  // Set up the match score and indices array.
+  let score = Infinity;
+  let indices: number[] | null = null;
+
+  // The regex for search word boundaries
+  let rgx = /\b\w/g;
+
+  // Search the source by word boundary.
+  while (true) {
+    // Find the next word boundary in the source.
+    let rgxMatch = rgx.exec(source);
+
+    // Break if there is no more source context.
+    if (!rgxMatch) {
+      break;
+    }
+
+    // Run the string match on the relevant substring.
+    let match = StringExt.matchSumOfDeltas(source, query, rgxMatch.index);
+
+    // Break if there is no match.
+    if (!match) {
+      break;
+    }
+
+    // Update the match if the score is better.
+    if (match && match.score <= score) {
+      score = match.score;
+      indices = match.indices;
+    }
+  }
+
+  // Bail if there was no match.
+  if (!indices || score === Infinity) {
+    return null;
+  }
+
+  // Handle a split match.
+  return {
+    score,
+    indices,
+    item
+  };
+}
+
 const FilterBox = (props: IFilterBoxProps) => {
   const [filter, setFilter] = useState('');
 
@@ -28,9 +98,17 @@ const FilterBox = (props: IFilterBoxProps) => {
   const handleChange = (e: React.FormEvent<HTMLElement>) => {
     const target = e.target as HTMLInputElement;
     setFilter(target.value);
-    props.listing.model.setFilter(
-      (value: Contents.IModel) => value.name.indexOf(target.value) !== -1
-    );
+    props.listing.model.setFilter((item: Contents.IModel) => {
+      // Run the fuzzy search for the item and query.
+      let score = fuzzySearch(item, target.value);
+      // Ignore the item if it is not a match.
+      if (!score) {
+        item.indices = [];
+        return false;
+      }
+      item.indices = score.indices;
+      return true;
+    });
   };
 
   return (

+ 5 - 0
packages/services/src/contents/index.ts

@@ -101,6 +101,11 @@ export namespace Contents {
      * The size of then file in bytes.
      */
     readonly size?: number;
+
+    /**
+     * The indices of the matched characters in the name.
+     */
+    indices?: ReadonlyArray<number> | null;
   }
 
   /**