Ver código fonte

Backport PR #12538: Fix file browser search highlighting bug (#12578)

Frédéric Collonval 3 anos atrás
pai
commit
e908232988

+ 13 - 12
packages/apputils/src/search.tsx

@@ -13,7 +13,10 @@ export interface IFilterBoxProps {
   /**
    * A function to callback when filter is updated.
    */
-  updateFilter: (filterFn: (item: string) => boolean, query?: string) => void;
+  updateFilter: (
+    filterFn: (item: string) => boolean | Partial<IScore> | null,
+    query?: string
+  ) => void;
 
   /**
    * Whether to use the fuzzy filter.
@@ -44,7 +47,7 @@ export interface IFilterBoxProps {
 /**
  * A text match score with associated content item.
  */
-interface IScore {
+export interface IScore {
   /**
    * The numerical score for the text match.
    */
@@ -59,7 +62,7 @@ interface IScore {
 /**
  * Perform a fuzzy search on a single item.
  */
-function fuzzySearch(source: string, query: string): IScore | null {
+export function fuzzySearch(source: string, query: string): IScore | null {
   // Set up the match score and indices array.
   let score = Infinity;
   let indices: number[] | null = null;
@@ -111,16 +114,12 @@ export const updateFilterFunction = (
   useFuzzyFilter: boolean,
   caseSensitive?: boolean
 ) => {
-  return (item: string) => {
+  return (item: string): Partial<IScore> | null => {
     if (useFuzzyFilter) {
       // Run the fuzzy search for the item and query.
       const query = value.toLowerCase();
-      let score = fuzzySearch(item, query);
       // Ignore the item if it is not a match.
-      if (!score) {
-        return false;
-      }
-      return true;
+      return fuzzySearch(item, query);
     }
     if (!caseSensitive) {
       item = item.toLocaleLowerCase();
@@ -128,9 +127,11 @@ export const updateFilterFunction = (
     }
     const i = item.indexOf(value);
     if (i === -1) {
-      return false;
+      return null;
     }
-    return true;
+    return {
+      indices: [...Array(item.length).keys()].map(x => x + 1)
+    };
   };
 };
 
@@ -140,7 +141,7 @@ export const FilterBox = (props: IFilterBoxProps) => {
   if (props.forceRefresh) {
     useEffect(() => {
       props.updateFilter((item: string) => {
-        return true;
+        return {};
       });
     }, []);
   }

+ 9 - 2
packages/filebrowser/src/browser.ts

@@ -3,6 +3,7 @@
 
 import {
   FilenameSearcher,
+  IScore,
   ReactWidget,
   showErrorMessage,
   Toolbar
@@ -88,7 +89,10 @@ export class FileBrowser extends Widget {
     });
 
     this._filenameSearcher = FilenameSearcher({
-      updateFilter: (filterFn: (item: string) => boolean) => {
+      updateFilter: (
+        filterFn: (item: string) => boolean | Partial<IScore> | null,
+        query?: string
+      ) => {
         this.listing.model.setFilter(value => {
           return filterFn(value.name.toLowerCase());
         });
@@ -162,7 +166,10 @@ export class FileBrowser extends Widget {
     this._useFuzzyFilter = value;
 
     this._filenameSearcher = FilenameSearcher({
-      updateFilter: (filterFn: (item: string) => boolean) => {
+      updateFilter: (
+        filterFn: (item: string) => boolean | Partial<IScore> | null,
+        query?: string
+      ) => {
         this.listing.model.setFilter(value => {
           return filterFn(value.name.toLowerCase());
         });

+ 12 - 6
packages/filebrowser/src/model.ts

@@ -1,7 +1,7 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { Dialog, showDialog } from '@jupyterlab/apputils';
+import { Dialog, IScore, showDialog } from '@jupyterlab/apputils';
 import { IChangedArgs, PageConfig, PathExt } from '@jupyterlab/coreutils';
 import { IDocumentManager, shouldOverwrite } from '@jupyterlab/docmanager';
 import { Contents, KernelSpec, Session } from '@jupyterlab/services';
@@ -768,7 +768,7 @@ export namespace TogglableHiddenFileBrowserModel {
 export class FilterFileBrowserModel extends TogglableHiddenFileBrowserModel {
   constructor(options: FilterFileBrowserModel.IOptions) {
     super(options);
-    this._filter = options.filter ? options.filter : model => true;
+    this._filter = options.filter ? options.filter : model => Object.freeze({});
   }
 
   /**
@@ -781,17 +781,23 @@ export class FilterFileBrowserModel extends TogglableHiddenFileBrowserModel {
       if (value.type === 'directory') {
         return true;
       } else {
-        return this._filter(value);
+        const filtered = this._filter(value);
+        if (typeof filtered !== 'boolean') {
+          value.indices = filtered?.indices;
+        }
+        return !!filtered;
       }
     });
   }
 
-  setFilter(filter: (value: Contents.IModel) => boolean) {
+  setFilter(
+    filter: (value: Contents.IModel) => boolean | Partial<IScore> | null
+  ): void {
     this._filter = filter;
     void this.refresh();
   }
 
-  private _filter: (value: Contents.IModel) => boolean;
+  private _filter: (value: Contents.IModel) => boolean | Partial<IScore> | null;
 }
 
 /**
@@ -805,6 +811,6 @@ export namespace FilterFileBrowserModel {
     /**
      * Filter function on file browser item model
      */
-    filter?: (value: Contents.IModel) => boolean;
+    filter?: (value: Contents.IModel) => boolean | Partial<IScore> | null;
   }
 }

+ 10 - 5
packages/filebrowser/src/opendialog.ts

@@ -1,7 +1,12 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { Dialog, setToolbar, ToolbarButton } from '@jupyterlab/apputils';
+import {
+  Dialog,
+  IScore,
+  setToolbar,
+  ToolbarButton
+} from '@jupyterlab/apputils';
 import { PathExt } from '@jupyterlab/coreutils';
 import { IDocumentManager } from '@jupyterlab/docmanager';
 import { Contents } from '@jupyterlab/services';
@@ -53,7 +58,7 @@ export namespace FileDialog {
     /**
      * Filter function on file browser item model
      */
-    filter?: (value: Contents.IModel) => boolean;
+    filter?: (value: Contents.IModel) => boolean | Partial<IScore> | null;
 
     /**
      * The application language translator.
@@ -108,7 +113,7 @@ export namespace FileDialog {
   ): Promise<Dialog.IResult<Contents.IModel[]>> {
     return getOpenFiles({
       ...options,
-      filter: model => false
+      filter: model => null
     });
   }
 }
@@ -121,7 +126,7 @@ class OpenDialog
   implements Dialog.IBodyWidget<Contents.IModel[]> {
   constructor(
     manager: IDocumentManager,
-    filter?: (value: Contents.IModel) => boolean,
+    filter?: (value: Contents.IModel) => boolean | Partial<IScore> | null,
     translator?: ITranslator
   ) {
     super();
@@ -228,7 +233,7 @@ namespace Private {
   export const createFilteredFileBrowser = (
     id: string,
     manager: IDocumentManager,
-    filter?: (value: Contents.IModel) => boolean,
+    filter?: (value: Contents.IModel) => boolean | Partial<IScore> | null,
     options: IFileBrowserFactory.IOptions = {},
     translator?: ITranslator
   ) => {

+ 8 - 5
packages/filebrowser/test/openfiledialog.spec.ts

@@ -55,7 +55,7 @@ describe('@jupyterlab/filebrowser', () => {
       it('should accept filter option', () => {
         const model = new FilterFileBrowserModel({
           manager,
-          filter: (model: Contents.IModel) => false
+          filter: (model: Contents.IModel) => null
         });
         expect(model).toBeInstanceOf(FilterFileBrowserModel);
       });
@@ -78,7 +78,7 @@ describe('@jupyterlab/filebrowser', () => {
       it('should list all directories whatever the filter', async () => {
         const filteredModel = new FilterFileBrowserModel({
           manager,
-          filter: (model: Contents.IModel) => false
+          filter: (model: Contents.IModel) => null
         });
         await filteredModel.cd();
         const model = new FileBrowserModel({ manager });
@@ -93,7 +93,8 @@ describe('@jupyterlab/filebrowser', () => {
       it('should respect the filter', async () => {
         const filteredModel = new FilterFileBrowserModel({
           manager,
-          filter: (model: Contents.IModel) => model.type === 'notebook'
+          filter: (model: Contents.IModel) =>
+            model.type === 'notebook' ? {} : null
         });
         await filteredModel.cd();
         const model = new FileBrowserModel({ manager });
@@ -138,7 +139,8 @@ describe('@jupyterlab/filebrowser', () => {
         manager,
         title: 'Select a notebook',
         host: node,
-        filter: (value: Contents.IModel) => value.type === 'notebook'
+        filter: (value: Contents.IModel) =>
+          value.type === 'notebook' ? {} : null
       });
 
       await acceptDialog();
@@ -161,7 +163,8 @@ describe('@jupyterlab/filebrowser', () => {
         manager,
         title: 'Select a notebook',
         host: node,
-        filter: (value: Contents.IModel) => value.type === 'notebook'
+        filter: (value: Contents.IModel) =>
+          value.type === 'notebook' ? {} : null
       });
 
       await waitForDialog();

+ 6 - 2
packages/settingeditor/src/pluginlist.tsx

@@ -5,6 +5,7 @@
 
 import {
   FilterBox,
+  IScore,
   ReactWidget,
   updateFilterFunction
 } from '@jupyterlab/apputils';
@@ -239,7 +240,7 @@ export class PluginList extends ReactWidget {
    * @returns - String array of properties that match the search results.
    */
   getFilterString(
-    filter: (item: string) => boolean,
+    filter: (item: string) => boolean | Partial<IScore> | null,
     props: ISettingRegistry.IProperty,
     definitions?: any,
     ref?: string
@@ -317,7 +318,10 @@ export class PluginList extends ReactWidget {
    * Updates the filter when the search bar value changes.
    * @param filter Filter function passed by search bar based on search value.
    */
-  setFilter(filter: (item: string) => boolean, query?: string): void {
+  setFilter(
+    filter: (item: string) => boolean | Partial<IScore> | null,
+    query?: string
+  ): void {
     this._filter = (plugin: ISettingRegistry.IPlugin): string[] | null => {
       if (filter(plugin.schema.title ?? '')) {
         return null;