Browse Source

live update index

Andrew Schlaepfer 6 years ago
parent
commit
7d3989e299

+ 10 - 8
packages/documentsearch-extension/src/executor.ts

@@ -1,9 +1,11 @@
 import { SearchProviderRegistry } from './searchproviderregistry';
-
 import { ISearchMatch, ISearchProvider } from './index';
-import { Widget } from '@phosphor/widgets';
+
 import { ApplicationShell } from '@jupyterlab/application';
 
+import { ISignal } from '@phosphor/signaling';
+import { Widget } from '@phosphor/widgets';
+
 export class Executor {
   constructor(registry: SearchProviderRegistry, shell: ApplicationShell) {
     this._registry = registry;
@@ -17,12 +19,8 @@ export class Executor {
       cleanupPromise = this._activeProvider.endSearch();
     }
     this._currentWidget = this._shell.currentWidget;
-    // I want widget.content.editor for cmsp
-    const compatibleProviders = this._registry.providers.filter(p =>
-      p.canSearchOn(this._currentWidget)
-    );
-    // If multiple providers match, just use the first one.
-    const provider = compatibleProviders[0];
+
+    const provider = this._registry.getProviderForWidget(this._currentWidget);
     if (!provider) {
       console.warn(
         'Unable to search on current widget, no compatible search provider'
@@ -61,6 +59,10 @@ export class Executor {
     return this._activeProvider.matches;
   }
 
+  get changed(): ISignal<ISearchProvider, void> {
+    return this._activeProvider.changed;
+  }
+
   private _registry: SearchProviderRegistry;
   private _activeProvider: ISearchProvider;
   private _currentWidget: Widget;

+ 21 - 20
packages/documentsearch-extension/src/index.ts

@@ -2,20 +2,16 @@
 | Copyright (c) Jupyter Development Team.
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
-
-import { JupyterLab, JupyterLabPlugin } from '@jupyterlab/application';
-
-import { ICommandPalette } from '@jupyterlab/apputils';
-
-// import { Widget } from '@phosphor/widgets';
+import '../style/index.css';
 
 import { SearchBox } from './searchbox';
-
 import { Executor } from './executor';
-
 import { SearchProviderRegistry } from './searchproviderregistry';
 
-import '../style/index.css';
+import { JupyterLab, JupyterLabPlugin } from '@jupyterlab/application';
+import { ICommandPalette } from '@jupyterlab/apputils';
+
+import { ISignal } from '@phosphor/signaling';
 
 export interface ISearchMatch {
   /**
@@ -86,6 +82,11 @@ export interface ISearchProvider {
    */
   readonly matches: ISearchMatch[];
 
+  /**
+   * Signal indicating that something in the search has changed, so the UI should update
+   */
+  readonly changed: ISignal<ISearchProvider, void>;
+
   /**
    * The current index of the selected match.
    */
@@ -107,10 +108,15 @@ const extension: JupyterLabPlugin<void> = {
     // Create widget, attach to signals
     const widget: SearchBox = new SearchBox();
 
+    const updateWidget = () => {
+      widget.totalMatches = executor.matches.length;
+      widget.currentIndex = executor.currentMatchIndex;
+    };
+
     const startSearchFn = (_: any, searchOptions: any) => {
-      executor.startSearch(searchOptions).then((matches: ISearchMatch[]) => {
-        widget.totalMatches = executor.matches.length;
-        widget.currentIndex = executor.currentMatchIndex;
+      executor.startSearch(searchOptions).then(() => {
+        updateWidget();
+        executor.changed.connect(updateWidget);
       });
     };
 
@@ -118,21 +124,16 @@ const extension: JupyterLabPlugin<void> = {
       executor.endSearch().then(() => {
         widget.totalMatches = 0;
         widget.currentIndex = 0;
+        executor.changed.disconnect(updateWidget);
       });
     };
 
     const highlightNextFn = () => {
-      executor.highlightNext().then(() => {
-        widget.totalMatches = executor.matches.length;
-        widget.currentIndex = executor.currentMatchIndex;
-      });
+      executor.highlightNext().then(updateWidget);
     };
 
     const highlightPreviousFn = () => {
-      executor.highlightPrevious().then(() => {
-        widget.totalMatches = executor.matches.length;
-        widget.currentIndex = executor.currentMatchIndex;
-      });
+      executor.highlightPrevious().then(updateWidget);
     };
 
     // Default to just searching on the current widget, could eventually

+ 30 - 3
packages/documentsearch-extension/src/providers/codemirrorsearchprovider.ts

@@ -5,6 +5,8 @@ import { ISearchProvider, ISearchMatch } from '../index';
 import { CodeMirrorEditor } from '@jupyterlab/codemirror';
 import { CodeEditor } from '@jupyterlab/codeeditor';
 
+import { ISignal, Signal } from '@phosphor/signaling';
+
 type MatchMap = { [key: number]: { [key: number]: ISearchMatch } };
 
 export class CodeMirrorSearchProvider implements ISearchProvider {
@@ -23,7 +25,11 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
       state.query = query;
       // clear search first
       this._cm.removeOverlay(state.overlay);
-      state.overlay = Private.searchOverlay(state.query, this._matchState);
+      state.overlay = Private.searchOverlay(
+        state.query,
+        this._matchState,
+        this._changed
+      );
       this._cm.addOverlay(state.overlay);
       // skips show matches on scroll bar here
       state.posFrom = state.posTo = this._cm.getCursor();
@@ -48,6 +54,8 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
   }
 
   endSearch(): Promise<void> {
+    this._matchState = {};
+    this._matchIndex = 0;
     Private.clearSearch(this._cm);
     return Promise.resolve();
   }
@@ -90,6 +98,10 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
     return Private.parseMatchesFromState(this._matchState);
   }
 
+  get changed(): ISignal<this, void> {
+    return this._changed;
+  }
+
   get currentMatchIndex(): number {
     return this._matchIndex;
   }
@@ -107,6 +119,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
   private _matchIndex: number;
   private _matchState: MatchMap = {};
   private _shouldLoop: boolean = true;
+  private _changed = new Signal<this, void>(this);
 }
 
 export class SearchState {
@@ -164,7 +177,17 @@ namespace Private {
         }
       };
 
+      // const localCursor = cm.cursorCoords(true, 'local');
+      // const pageCursor = cm.cursorCoords(true, 'page');
+      // const windowCursor = cm.cursorCoords(true, 'window');
+      // console.log('localCursor: ', localCursor);
+      // console.log('pageCursor: ', pageCursor);
+      // console.log('windowCursor: ', windowCursor);
+      // console.log('scroller element: ', cm.getScrollerElement());
       cm.setSelection(selRange);
+      // const scrollY = reverse ? pageCursor.top : pageCursor.bottom;
+      // console.log('------- scrolling to x, y: ', pageCursor.left, ', ', scrollY);
+      // cm.scrollTo(pageCursor.left, scrollY);
       cm.scrollIntoView(
         {
           from: fromPos,
@@ -188,7 +211,11 @@ namespace Private {
     return cm.state.search;
   }
 
-  export function searchOverlay(query: RegExp, matchState: MatchMap) {
+  export function searchOverlay(
+    query: RegExp,
+    matchState: MatchMap,
+    changed: Signal<ISearchProvider, void>
+  ) {
     return {
       /**
        * Token function is called when a line needs to be processed -
@@ -230,7 +257,6 @@ namespace Private {
             matchState[line] = {};
           }
           matchState[line][currentPos] = matchObj;
-
           // move the stream along and return searching style for the token
           stream.pos += matchLength || 1;
           return 'searching';
@@ -239,6 +265,7 @@ namespace Private {
           stream.pos = match.index;
         } else {
           // no matches, consume the rest of the stream
+          changed.emit(undefined);
           stream.skipToEnd();
         }
       }

+ 42 - 17
packages/documentsearch-extension/src/providers/notebooksearchprovider.ts

@@ -1,13 +1,12 @@
-// import * as CodeMirror from 'codemirror';
-
 import { ISearchProvider, ISearchMatch } from '../index';
-
 import { CodeMirrorSearchProvider } from './codemirrorsearchprovider';
 
 import { NotebookPanel, Notebook } from '@jupyterlab/notebook';
-
 import { CodeMirrorEditor } from '@jupyterlab/codemirror';
 import { Cell, MarkdownCell } from '@jupyterlab/cells';
+
+import { Signal, ISignal } from '@phosphor/signaling';
+
 import CodeMirror from 'codemirror';
 
 interface ICellSearchPair {
@@ -15,8 +14,6 @@ interface ICellSearchPair {
   provider: ISearchProvider;
 }
 
-// TODO: re-examine indexing of cells
-
 export class NotebookSearchProvider implements ISearchProvider {
   startSearch(
     query: RegExp,
@@ -24,9 +21,18 @@ export class NotebookSearchProvider implements ISearchProvider {
   ): Promise<ISearchMatch[]> {
     this._searchTarget = searchTarget;
     const cells = this._searchTarget.content.widgets;
+
+    const cellModel = this._searchTarget.model.cells;
+    Signal.disconnectBetween(cellModel, this);
+    cellModel.changed.connect(
+      this._restartSearch.bind(this, query, searchTarget),
+      this
+    );
+
     const activeCell = this._searchTarget.content.activeCell;
-    const matchPromises: Promise<ISearchMatch[]>[] = [];
     let indexTotal = 0;
+    let matchPromise = Promise.resolve([]);
+    const allMatches: ISearchMatch[] = [];
     // For each cell, create a search provider and collect the matches
     cells.forEach((cell: Cell) => {
       const cmEditor = cell.editor as CodeMirrorEditor;
@@ -40,7 +46,8 @@ export class NotebookSearchProvider implements ISearchProvider {
       if (cell.inputHidden) {
         cell.inputHidden = false;
       }
-      matchPromises.push(
+      // chain promises to ensure indexing is sequential
+      matchPromise = matchPromise.then(() =>
         cmSearchProvider
           .startSearch(query, cmEditor)
           .then((matchesFromCell: ISearchMatch[]) => {
@@ -58,6 +65,12 @@ export class NotebookSearchProvider implements ISearchProvider {
             });
             indexTotal += matchesFromCell.length;
 
+            // search has been initialized, connect the changed signal
+            cmSearchProvider.changed.connect(
+              this._onCmSearchProviderChanged,
+              this
+            );
+
             // In the active cell, select the next match after the cursor
             if (activeCell === cell) {
               return cmSearchProvider
@@ -69,7 +82,7 @@ export class NotebookSearchProvider implements ISearchProvider {
             }
 
             indexTotal += matchesFromCell.length;
-            return matchesFromCell;
+            allMatches.concat(matchesFromCell);
           })
       );
 
@@ -79,19 +92,14 @@ export class NotebookSearchProvider implements ISearchProvider {
       });
     });
 
-    // Flatten matches into one array
-    return Promise.all(matchPromises).then(matchesFromCells => {
-      let result: ISearchMatch[] = [];
-      matchesFromCells.forEach((cellMatches: ISearchMatch[]) => {
-        result.concat(cellMatches);
-      });
-      return result;
-    });
+    // Execute cell searches sequentially to ensure indexes are correct
+    return matchPromise.then(() => allMatches);
   }
 
   endSearch(): Promise<void> {
     this._cmSearchProviders.forEach(({ provider }) => {
       provider.endSearch();
+      provider.changed.disconnect(this._onCmSearchProviderChanged, this);
     });
     this._cmSearchProviders = [];
     this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => {
@@ -136,6 +144,10 @@ export class NotebookSearchProvider implements ISearchProvider {
     return [].concat(...Private.getMatchesFromCells(this._cmSearchProviders));
   }
 
+  get changed(): ISignal<this, void> {
+    return this._changed;
+  }
+
   get currentMatchIndex(): number {
     if (!this._currentMatch) {
       return 0;
@@ -143,10 +155,23 @@ export class NotebookSearchProvider implements ISearchProvider {
     return this._currentMatch.index;
   }
 
+  private _restartSearch(query: RegExp, searchTarget: NotebookPanel) {
+    console.log('restarting search!');
+    this.endSearch();
+    this.startSearch(query, searchTarget).then(() =>
+      this._changed.emit(undefined)
+    );
+  }
+
+  private _onCmSearchProviderChanged() {
+    this._changed.emit(undefined);
+  }
+
   private _searchTarget: NotebookPanel;
   private _cmSearchProviders: ICellSearchPair[] = [];
   private _currentMatch: ISearchMatch;
   private _unRenderedMarkdownCells: MarkdownCell[] = [];
+  private _changed = new Signal<this, void>(this);
 }
 
 namespace Private {

+ 33 - 9
packages/documentsearch-extension/src/searchproviderregistry.ts

@@ -1,5 +1,4 @@
 import { ISearchProvider } from './index';
-
 import {
   CodeMirrorSearchProvider,
   NotebookSearchProvider
@@ -10,33 +9,58 @@ const DEFAULT_CODEMIRROR_SEARCH_PROVIDER = 'jl-defaultCodeMirrorSearchProvider';
 
 export class SearchProviderRegistry {
   constructor() {
-    this.registerProvider(
+    this.registerDefaultProviders(
       DEFAULT_NOTEBOOK_SEARCH_PROVIDER,
       new NotebookSearchProvider()
     );
-    this.registerProvider(
+    this.registerDefaultProviders(
       DEFAULT_CODEMIRROR_SEARCH_PROVIDER,
       new CodeMirrorSearchProvider()
     );
   }
 
   registerProvider(key: string, provider: ISearchProvider): void {
-    this._providers[key] = provider;
+    this._customProviders[key] = provider;
   }
 
   deregisterProvider(key: string): boolean {
-    if (!this._providers[key]) {
+    if (!this._customProviders[key]) {
       return false;
     }
-    this._providers[key] = undefined;
+    this._customProviders[key] = undefined;
     return true;
   }
 
-  get providers(): ISearchProvider[] {
-    return Object.keys(this._providers).map(k => this._providers[k]);
+  getProviderForWidget(widget: any): ISearchProvider {
+    return (
+      this.findMatchingProvider(this._customProviders, widget) ||
+      this.findMatchingProvider(this._defaultProviders, widget)
+    );
+  }
+
+  private registerDefaultProviders(
+    key: string,
+    provider: ISearchProvider
+  ): void {
+    this._defaultProviders[key] = provider;
+  }
+
+  private findMatchingProvider(
+    providerMap: Private.ProviderMap,
+    widget: any
+  ): ISearchProvider {
+    const compatibleProvders = Object.keys(providerMap)
+      .map(k => providerMap[k])
+      .filter((p: ISearchProvider) => p.canSearchOn(widget));
+
+    if (compatibleProvders.length !== 0) {
+      return compatibleProvders[0];
+    }
+    return null;
   }
 
-  private _providers: Private.ProviderMap = {};
+  private _defaultProviders: Private.ProviderMap = {};
+  private _customProviders: Private.ProviderMap = {};
 }
 
 namespace Private {