Browse Source

partially implemented search

Andrew Schlaepfer 6 years ago
parent
commit
891ae5d91a

+ 3 - 0
dev_mode/package.json

@@ -31,6 +31,7 @@
     "@jupyterlab/docmanager": "^0.19.1",
     "@jupyterlab/docmanager-extension": "^0.19.1",
     "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/documentsearch-extension": "^0.7.1",
     "@jupyterlab/extensionmanager": "^0.19.1",
     "@jupyterlab/extensionmanager-extension": "^0.19.1",
     "@jupyterlab/faq-extension": "^0.19.1",
@@ -143,6 +144,7 @@
       "@jupyterlab/console-extension": "",
       "@jupyterlab/csvviewer-extension": "",
       "@jupyterlab/docmanager-extension": "",
+      "@jupyterlab/documentsearch-extension": "",
       "@jupyterlab/extensionmanager-extension": "",
       "@jupyterlab/faq-extension": "",
       "@jupyterlab/filebrowser-extension": "",
@@ -246,6 +248,7 @@
       "@jupyterlab/docmanager": "../packages/docmanager",
       "@jupyterlab/docmanager-extension": "../packages/docmanager-extension",
       "@jupyterlab/docregistry": "../packages/docregistry",
+      "@jupyterlab/documentsearch-extension": "../packages/documentsearch-extension",
       "@jupyterlab/extensionmanager": "../packages/extensionmanager",
       "@jupyterlab/extensionmanager-extension": "../packages/extensionmanager-extension",
       "@jupyterlab/faq-extension": "../packages/faq-extension",

+ 1 - 1
examples/console/package.json

@@ -18,7 +18,7 @@
     "es6-promise": "~4.1.1"
   },
   "devDependencies": {
-    "@types/codemirror": "~0.0.46",
+    "@types/codemirror": "~0.0.70",
     "css-loader": "~0.28.7",
     "file-loader": "~1.1.11",
     "mini-css-extract-plugin": "~0.4.4",

+ 1 - 1
examples/filebrowser/package.json

@@ -22,7 +22,7 @@
     "es6-promise": "~4.1.1"
   },
   "devDependencies": {
-    "@types/codemirror": "~0.0.46",
+    "@types/codemirror": "~0.0.70",
     "css-loader": "~0.28.7",
     "file-loader": "~1.1.11",
     "mini-css-extract-plugin": "~0.4.4",

+ 1 - 1
examples/notebook/package.json

@@ -21,7 +21,7 @@
     "es6-promise": "~4.1.1"
   },
   "devDependencies": {
-    "@types/codemirror": "~0.0.46",
+    "@types/codemirror": "~0.0.70",
     "css-loader": "~0.28.7",
     "file-loader": "~1.1.11",
     "mini-css-extract-plugin": "~0.4.4",

+ 2 - 1
packages/codemirror/package.json

@@ -42,11 +42,12 @@
     "@phosphor/disposable": "^1.1.2",
     "@phosphor/signaling": "^1.2.2",
     "@phosphor/widgets": "^1.6.0",
+    "@types/codemirror": "~0.0.70",
     "codemirror": "~5.42.0",
     "react": "~16.4.2"
   },
   "devDependencies": {
-    "@types/codemirror": "~0.0.46",
+    "@types/codemirror": "~0.0.70",
     "rimraf": "~2.6.2",
     "typedoc": "~0.12.0",
     "typescript": "~3.1.1"

+ 38 - 3
packages/codemirror/src/editor.ts

@@ -1,5 +1,7 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
+/// <reference types="codemirror"/>
+/// <reference types="codemirror/searchcursor"/>
 
 import CodeMirror from 'codemirror';
 
@@ -383,13 +385,44 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
     start?: CodeMirror.Position,
     caseFold?: boolean
   ): CodeMirror.SearchCursor {
-    return this._editor.getSearchCursor(query, start, caseFold);
+    return this._editor.getDoc().getSearchCursor(query, start, caseFold);
+  }
+
+  getCursor(start?: string): CodeMirror.Position {
+    return this._editor.getDoc().getCursor(start);
   }
 
   get state(): any {
     return this._editor.state;
   }
 
+  operation<T>(fn: () => T): T {
+    return this._editor.operation(fn);
+  }
+
+  firstLine(): number {
+    return this._editor.getDoc().firstLine();
+  }
+
+  lastLine(): number {
+    return this._editor.getDoc().lastLine();
+  }
+
+  scrollIntoView(
+    pos: { from: CodeMirror.Position; to: CodeMirror.Position },
+    margin: number
+  ): void {
+    this._editor.scrollIntoView(pos, margin);
+  }
+
+  getRange(
+    from: CodeMirror.Position,
+    to: CodeMirror.Position,
+    seperator?: string
+  ): string {
+    return this._editor.getDoc().getRange(from, to, seperator);
+  }
+
   /**
    * Add a keydown handler to the editor.
    *
@@ -781,8 +814,10 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
    */
   private _toCodeMirrorRange(range: CodeEditor.IRange): CodeMirror.Range {
     return {
-      from: this._toCodeMirrorPosition(range.start),
-      to: this._toCodeMirrorPosition(range.end)
+      anchor: this._toCodeMirrorPosition(range.start),
+      head: this._toCodeMirrorPosition(range.end),
+      from: this._toCodeMirrorPosition.bind(this, range.start),
+      to: this._toCodeMirrorPosition.bind(this, range.end)
     };
   }
 

+ 1 - 0
packages/codemirror/src/typings.d.ts

@@ -2,3 +2,4 @@
 // Distributed under the terms of the Modified BSD License.
 
 /// <reference path="../typings/codemirror/codemirror.d.ts"/>
+/// <reference types="@types/codemirror/searchcursor"/>

File diff suppressed because it is too large
+ 19 - 998
packages/codemirror/typings/codemirror/codemirror.d.ts


+ 51 - 0
packages/documentsearch-extension/package.json

@@ -0,0 +1,51 @@
+{
+  "name": "@jupyterlab/documentsearch-extension",
+  "version": "0.7.1",
+  "description": "Search document types",
+  "homepage": "https://github.com/jupyterlab/jupyterlab",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "license": "BSD-3-Clause",
+  "author": "Project Jupyter",
+  "files": [
+    "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
+    "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "directories": {
+    "lib": "lib/"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "scripts": {
+    "build": "tsc",
+    "clean": "rimraf lib",
+    "prepublishOnly": "npm run build",
+    "watch": "tsc -w --listEmittedFiles"
+  },
+  "dependencies": {
+    "@jupyterlab/application": "^0.19.1",
+    "@jupyterlab/apputils": "^0.19.1",
+    "@jupyterlab/codeeditor": "^0.19.1",
+    "@jupyterlab/codemirror": "^0.19.1",
+    "@jupyterlab/notebook": "^0.19.2",
+    "@phosphor/messaging": "^1.2.2",
+    "@phosphor/signaling": "^1.2.2",
+    "@phosphor/widgets": "^1.6.0",
+    "codemirror": "~5.42.0"
+  },
+  "devDependencies": {
+    "rimraf": "~2.6.2",
+    "typescript": "~3.1.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "jupyterlab": {
+    "extension": true
+  }
+}

+ 61 - 0
packages/documentsearch-extension/src/executor.ts

@@ -0,0 +1,61 @@
+import { SearchProviderRegistry } from './searchproviderregistry';
+
+import { ISearchOptions, ISearchMatch, ISearchProvider } from './index';
+import { Widget } from '@phosphor/widgets';
+import { ApplicationShell } from '@jupyterlab/application';
+
+export class Executor {
+  constructor(registry: SearchProviderRegistry, shell: ApplicationShell) {
+    this._registry = registry;
+    this._shell = shell;
+  }
+  startSearch(options: ISearchOptions): Promise<ISearchMatch[]> {
+    // TODO: figure out where to check if the options have changed
+    // to know how to respond to an 'enter' keypress (new search or next search)
+    let cleanupPromise = Promise.resolve();
+    if (this._activeProvider) {
+      console.log('we have an active provider already, cleaning up with end');
+      cleanupPromise = this._activeProvider.endSearch();
+    }
+    this._currentWidget = this._shell.currentWidget;
+
+    const compatibleProviders = this._registry.providers.filter(p =>
+      p.canSearchOn(this._currentWidget)
+    );
+    // If multiple providers match, just use the first one.
+    const provider = compatibleProviders[0];
+    if (!provider) {
+      console.warn(
+        'Unable to search on current widget, no compatible search provider'
+      );
+      return;
+    }
+    this._activeProvider = provider;
+    return cleanupPromise.then(() =>
+      provider.startSearch(options, this._currentWidget)
+    );
+  }
+
+  endSearch(): Promise<void> {
+    if (!this._activeProvider) {
+      return Promise.resolve();
+    }
+    return this._activeProvider.endSearch().then(() => {
+      this._activeProvider = undefined;
+      this._currentWidget = undefined;
+    });
+  }
+
+  highlightNext(): Promise<ISearchMatch> {
+    return this._activeProvider.highlightNext();
+  }
+
+  highlightPrevious(): Promise<ISearchMatch> {
+    return this._activeProvider.highlightPrevious();
+  }
+
+  private _registry: SearchProviderRegistry;
+  private _activeProvider: ISearchProvider;
+  private _currentWidget: Widget;
+  private _shell: ApplicationShell;
+}

+ 159 - 0
packages/documentsearch-extension/src/index.ts

@@ -0,0 +1,159 @@
+/*-----------------------------------------------------------------------------
+| 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 { SearchBox } from './searchbox';
+
+import { Executor } from './executor';
+
+import { SearchProviderRegistry } from './searchproviderregistry';
+
+import '../style/index.css';
+
+export interface ISearchMatch {
+  /**
+   * Text of the exact match itself
+   */
+  readonly text: string;
+
+  /**
+   * Fragment containing match
+   */
+  readonly fragment: string;
+
+  /**
+   * Line number of match
+   */
+  line: number;
+
+  /**
+   * Column location of match
+   */
+  column: number;
+
+  /**
+   * Index among the other matches
+   */
+  index: number;
+}
+export interface ISearchOptions {
+  /**
+   * Actual search query entered by user
+   */
+  query: string;
+
+  /**
+   * Should the search be performed in a case-sensitive manner?
+   */
+  caseSensitive: boolean;
+
+  /**
+   * Is the search term to be treated as a regular expression?
+   */
+  regex: boolean;
+}
+
+export interface ISearchProvider {
+  /**
+   * Initialize the search using the provided options.  Should update the UI
+   * to highlight all matches and "select" whatever the first match should be.
+   *
+   * @param options All of the search parameters configured in the search panel
+   *
+   * @returns A promise that resolves with a list of all matches
+   */
+  startSearch(
+    options: ISearchOptions,
+    searchTarget: any
+  ): Promise<ISearchMatch[]>;
+
+  /**
+   * Resets UI state, removes all matches.
+   *
+   * @returns A promise that resolves when all state has been cleaned up.
+   */
+  endSearch(): Promise<void>;
+
+  /**
+   * Move the current match indicator to the next match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  highlightNext(): Promise<ISearchMatch>;
+
+  /**
+   * Move the current match indicator to the previous match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  highlightPrevious(): Promise<ISearchMatch>;
+
+  /**
+   * Report whether or not this provider has the ability to search on the given object
+   */
+  canSearchOn(domain: any): boolean;
+
+  /**
+   * The same list of matches provided by the startSearch promise resoluton
+   */
+  readonly matches: ISearchMatch[];
+
+  /**
+   * The current index of the selected match.
+   */
+  readonly currentMatchIndex: number;
+}
+
+console.log('documentsearch-extension loaded');
+
+/**
+ * Initialization data for the document-search extension.
+ */
+const extension: JupyterLabPlugin<void> = {
+  id: 'document-search',
+  autoStart: true,
+  requires: [ICommandPalette],
+  activate: (app: JupyterLab, palette: ICommandPalette) => {
+    console.log('JupyterLab extension document-search is activated!');
+
+    // Create registry, retrieve all default providers
+    const registry: SearchProviderRegistry = new SearchProviderRegistry();
+    const executor: Executor = new Executor(registry, app.shell);
+
+    // Create widget, attach to signals
+    const widget: SearchBox = new SearchBox();
+
+    // Default to just searching on the current widget, could eventually
+    // read a flag provided by the search box widget if we want to search something else
+    widget.startSearch.connect((_, searchOptions) =>
+      executor.startSearch(searchOptions)
+    );
+    widget.endSearch.connect(_ => executor.endSearch());
+    widget.highlightNext.connect(_ => executor.highlightNext());
+    widget.highlightPrevious.connect(_ => executor.highlightPrevious());
+
+    const command: string = 'document:search';
+    app.commands.addCommand(command, {
+      label: 'Search the open document',
+      execute: () => {
+        if (!widget.isAttached) {
+          // Attach the widget to the main work area if it's not there
+          app.shell.addToLeftArea(widget, { rank: 400 });
+        }
+        app.shell.activateById(widget.id);
+      }
+    });
+
+    // Add the command to the palette.
+    palette.addItem({ command, category: 'Tutorial' });
+  }
+};
+
+export default extension;

+ 157 - 0
packages/documentsearch-extension/src/old-index.ts

@@ -0,0 +1,157 @@
+/*-----------------------------------------------------------------------------
+| 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 { SearchBox } from './searchbox';
+
+import { Executor } from './executor';
+
+import { SearchProviderRegistry } from './searchproviderregistry';
+
+import '../style/index.css';
+
+export interface ISearchMatch {
+  /**
+   * Text of the exact match itself
+   */
+  readonly text: string;
+
+  /**
+   * Line number of match
+   */
+  readonly line: number;
+
+  /**
+   * Column location of match
+   */
+  readonly column: number;
+
+  /**
+   * Filepath of file containing match
+   */
+  readonly path: string;
+
+  /**
+   * Fragment containing match
+   */
+  readonly fragment: string;
+}
+export interface ISearchOptions {
+  /**
+   * Actual search query entered by user
+   */
+  query: string;
+
+  /**
+   * Should the search be performed in a case-sensitive manner?
+   */
+  caseSensitive: boolean;
+
+  /**
+   * Is the search term to be treated as a regular expression?
+   */
+  regex: boolean;
+}
+
+export interface ISearchProvider {
+  /**
+   * Initialize the search using the provided options.  Should update the UI
+   * to highlight all matches and "select" whatever the first match should be.
+   *
+   * @param options All of the search parameters configured in the search panel
+   *
+   * @returns A promise that resolves with a list of all matches
+   */
+  startSearch(
+    options: ISearchOptions,
+    searchTarget: any
+  ): Promise<ISearchMatch[]>;
+
+  /**
+   * Resets UI state, removes all matches.
+   *
+   * @returns A promise that resolves when all state has been cleaned up.
+   */
+  endSearch(): Promise<void>;
+
+  /**
+   * Move the current match indicator to the next match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  highlightNext(): Promise<void>;
+
+  /**
+   * Move the current match indicator to the previous match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  highlightPrevious(): Promise<void>;
+
+  /**
+   * Report whether or not this provider has the ability to search on the given object
+   */
+  canSearchOn(domain: any): boolean;
+
+  /**
+   * The same list of matches provided by the startSearch promise resoluton
+   */
+  readonly matches: ISearchMatch[];
+
+  /**
+   * The current index of the selected match.
+   */
+  readonly currentMatchIndex: number;
+}
+
+/**
+ * Initialization data for the document-search extension.
+ */
+const extension: JupyterLabPlugin<void> = {
+  id: 'document-search',
+  autoStart: true,
+  requires: [ICommandPalette],
+  activate: (app: JupyterLab, palette: ICommandPalette) => {
+    console.log('JupyterLab extension document-search is activated!');
+
+    // Create registry, retrieve all default providers
+    const registry: SearchProviderRegistry = new SearchProviderRegistry();
+    const executor: Executor = new Executor(registry, app.shell);
+
+    // Create widget, attach to signals
+    const widget: SearchBox = new SearchBox();
+
+    // Default to just searching on the current widget, could eventually
+    // read a flag provided by the search box widget if we want to search something else
+    widget.startSearch.connect((_, searchOptions) =>
+      executor.startSearch(searchOptions)
+    );
+    widget.endSearch.connect(_ => executor.endSearch());
+    widget.highlightNext.connect(_ => executor.highlightNext());
+    widget.highlightPrevious.connect(_ => executor.highlightPrevious());
+
+    const command: string = 'document:search';
+    app.commands.addCommand(command, {
+      label: 'Search the open document',
+      execute: () => {
+        if (!widget.isAttached) {
+          // Attach the widget to the main work area if it's not there
+          app.shell.addToLeftArea(widget, { rank: 400 });
+        }
+        app.shell.activateById(widget.id);
+      }
+    });
+
+    // Add the command to the palette.
+    palette.addItem({ command, category: 'Tutorial' });
+  }
+};
+
+export default extension;

+ 297 - 0
packages/documentsearch-extension/src/providers/codemirrorsearchprovider.ts

@@ -0,0 +1,297 @@
+import * as CodeMirror from 'codemirror';
+
+import { ISearchProvider, ISearchOptions, ISearchMatch } from '../index';
+
+import { CodeMirrorEditor } from '@jupyterlab/codemirror';
+import { CodeEditor } from '@jupyterlab/codeeditor';
+
+type MatchMap = { [key: number]: { [key: number]: ISearchMatch } };
+
+export class CodeMirrorSearchProvider implements ISearchProvider {
+  startSearch(
+    options: ISearchOptions,
+    searchTarget: CodeMirrorEditor
+  ): Promise<ISearchMatch[]> {
+    this._cm = searchTarget;
+    console.log(
+      'CodeMirror provider: startSearch on options, target: ',
+      options,
+      ', ',
+      this._cm
+    );
+    Private.clearSearch(this._cm);
+
+    const state = Private.getSearchState(this._cm);
+    this._cm.operation(() => {
+      state.queryText = options.query;
+      state.query = Private.parseQuery(options.query);
+      // clear search first?
+      this._cm.removeOverlay(state.overlay);
+      state.overlay = Private.searchOverlay(
+        state.query,
+        Private.queryCaseInsensitive(state.query),
+        this._matchState
+      );
+      this._cm.addOverlay(state.overlay);
+      // skips show matches on scroll bar here
+      // state.posFrom = state.posTo = this._cm.getCursor();
+      // Private.findNext(this._cm, false);
+    });
+    console.log('matchState: ', this._matchState);
+    const matches = Private.parseMatchesFromState(this._matchState);
+    console.log('matches: ', matches);
+    return Promise.resolve(matches);
+  }
+
+  endSearch(): Promise<void> {
+    Private.clearSearch(this._cm);
+    return Promise.resolve();
+  }
+
+  highlightNext(): Promise<ISearchMatch> {
+    console.log('codemirror search provider: highlightNext');
+    const { from } = Private.findNext(this._cm, false);
+    const match = this._matchState[from.line][from.ch];
+    console.log('next match: ', match);
+    return Promise.resolve(match);
+  }
+
+  highlightPrevious(): Promise<ISearchMatch> {
+    console.log('codemirror search provider: highlightPrevious');
+    const { from } = Private.findNext(this._cm, true);
+    const match = this._matchState[from.line][from.ch];
+    console.log('prev match: ', match);
+    return Promise.resolve(match);
+  }
+
+  canSearchOn(domain: any): boolean {
+    console.log('codemirror search provider: canSearchOn');
+    return false;
+  }
+
+  get matches(): ISearchMatch[] {
+    console.log('codemirror search provider: matches');
+    return Private.parseMatchesFromState(this._matchState);
+  }
+
+  get currentMatchIndex(): number {
+    console.log('codemirror search provider: currentMatchIndex');
+    return this._matchIndex;
+  }
+
+  clearSelection(): void {
+    return null;
+  }
+
+  private _cm: CodeMirrorEditor;
+  private _matchIndex: number;
+  private _matchState: MatchMap = {};
+}
+
+class SearchState {
+  public posFrom: number;
+  public posTo: number;
+  public lastQuery: string;
+  public query: string;
+}
+
+namespace Private {
+  interface ICodeMirrorMatch {
+    from: CodeMirror.Position;
+    to: CodeMirror.Position;
+  }
+
+  export function findNext(
+    cm: CodeMirrorEditor,
+    reverse: boolean
+  ): ICodeMirrorMatch {
+    return cm.operation(() => {
+      const state = getSearchState(cm);
+      let cursor: CodeMirror.SearchCursor = cm.getSearchCursor(
+        state.query,
+        reverse ? state.posFrom : state.posTo
+      );
+      if (!cursor.find(reverse)) {
+        cursor = cm.getSearchCursor(
+          state.query,
+          reverse
+            ? CodeMirror.Pos(cm.lastLine())
+            : CodeMirror.Pos(cm.firstLine(), 0)
+        );
+      }
+      console.log('cursor: ', cursor);
+      console.log('fromPos: ', cursor.from());
+      console.log('toPos: ', cursor.to());
+      const fromPos: CodeMirror.Position = cursor.from();
+      const toPos: CodeMirror.Position = cursor.to();
+      const selRange: CodeEditor.IRange = {
+        start: {
+          line: fromPos.line,
+          column: fromPos.ch
+        },
+        end: {
+          line: toPos.line,
+          column: toPos.ch
+        }
+      };
+
+      cm.setSelection(selRange);
+      cm.scrollIntoView(
+        {
+          from: fromPos,
+          to: toPos
+        },
+        20
+      );
+      state.posFrom = fromPos;
+      state.posTo = toPos;
+      return {
+        from: fromPos,
+        to: toPos
+      };
+    });
+  }
+
+  export function getSearchState(cm: CodeMirrorEditor) {
+    if (!cm.state.search) {
+      cm.state.search = new SearchState();
+    }
+    return cm.state.search;
+  }
+
+  export function searchOverlay(
+    queryIn: string | RegExp,
+    caseInsensitive: boolean,
+    matchState: MatchMap
+  ) {
+    let query: RegExp;
+    if (typeof queryIn === 'string') {
+      query = new RegExp(
+        (queryIn as string).replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'),
+        caseInsensitive ? 'gi' : 'g'
+      );
+    } else {
+      let queryInReg: RegExp = queryIn as RegExp;
+      if (!queryInReg.global) {
+        query = new RegExp(
+          queryInReg.source,
+          queryInReg.ignoreCase ? 'gi' : 'g'
+        );
+      }
+      query = queryInReg;
+    }
+
+    return {
+      token: (stream: CodeMirror.StringStream) => {
+        // console.log('stream: ', stream);
+        const currentPos = stream.pos;
+        query.lastIndex = currentPos;
+        const lineText = stream.string;
+        const match = query.exec(lineText);
+        const line = (stream as any).lineOracle.line;
+        // if starting at position 0, blow away everything on this line in the state
+        if (
+          stream.start === currentPos &&
+          currentPos === 0 &&
+          !!matchState[line] &&
+          Object.keys(matchState[line]).length !== 0
+        ) {
+          matchState[line] = {};
+        }
+        if (match && match.index === currentPos) {
+          // found match, add it to state
+          const matchLength = match[0].length;
+          const matchObj: ISearchMatch = {
+            text: lineText.substr(currentPos, matchLength),
+            line: line,
+            column: currentPos,
+            fragment: lineText,
+            index: 0 // fill in index when flattening, later
+          };
+          if (!matchState[line]) {
+            matchState[line] = {};
+          }
+          matchState[line][currentPos] = matchObj;
+
+          // move the stream along and return searching style for the token
+          stream.pos += matchLength || 1;
+          return 'searching';
+        } else if (match) {
+          // there's a match in the stream, advance the stream to its position
+          stream.pos = match.index;
+        } else {
+          // no matches, consume the rest of the stream
+          stream.skipToEnd();
+        }
+        console.log('matchState post token: ', matchState);
+      }
+    };
+  }
+
+  export function clearSearch(cm: CodeMirrorEditor) {
+    const state = getSearchState(cm);
+    state.lastQuery = state.query;
+    if (!state.query) {
+      return;
+    }
+    state.query = state.queryText = null;
+    cm.removeOverlay(state.overlay);
+    if (state.annotate) {
+      state.annotate.clear();
+      state.annotate = null;
+    }
+  }
+
+  export function parseString(str: string) {
+    return str.replace(/\\(.)/g, (_, ch) => {
+      if (ch === 'n') {
+        return '\n';
+      }
+      if (ch === 'r') {
+        return '\r';
+      }
+      return ch;
+    });
+  }
+
+  export function parseQuery(query: string | RegExp) {
+    const isRE = (query as string).match(/^\/(.*)\/([a-z]*)$/);
+    let ret: string | RegExp;
+    if (isRE) {
+      try {
+        ret = new RegExp(isRE[1], isRE[2].indexOf('i') === -1 ? '' : 'i');
+        // tslint:disable-next-line:no-empty
+      } catch (e) {} // Not a regular expression after all, do a string search
+    } else {
+      ret = parseString(query as string);
+    }
+    if (typeof query === 'string' ? query === '' : query.test('')) {
+      ret = /x^/;
+    }
+    return ret;
+  }
+
+  export function queryCaseInsensitive(query: string | RegExp) {
+    return typeof query === 'string' && query === query.toLowerCase();
+  }
+
+  export function parseMatchesFromState(state: MatchMap): ISearchMatch[] {
+    let index = 0;
+    const matches: ISearchMatch[] = Object.keys(state).reduce(
+      (acc: ISearchMatch[], lineNumber: string) => {
+        const lineKey: number = parseInt(lineNumber, 10); // ugh
+        const lineMatches: { [key: number]: ISearchMatch } = state[lineKey];
+        Object.keys(lineMatches).forEach((pos: string) => {
+          const posKey: number = parseInt(pos, 10);
+          const match: ISearchMatch = lineMatches[posKey];
+          match.index = index;
+          index += 1;
+          acc.push(match);
+        });
+        return acc;
+      },
+      []
+    );
+    return matches;
+  }
+}

+ 149 - 0
packages/documentsearch-extension/src/providers/notebooksearchprovider.ts

@@ -0,0 +1,149 @@
+// import * as CodeMirror from 'codemirror';
+
+import { ISearchProvider, ISearchOptions, ISearchMatch } from '../index';
+
+import { CodeMirrorSearchProvider } from './codemirrorsearchprovider';
+
+import { NotebookPanel } from '@jupyterlab/notebook';
+
+import { CodeMirrorEditor } from '@jupyterlab/codemirror';
+import { Cell } from '@jupyterlab/cells';
+
+interface ICellSearchPair {
+  cell: Cell;
+  provider: ISearchProvider;
+}
+
+export class NotebookSearchProvider implements ISearchProvider {
+  startSearch(
+    options: ISearchOptions,
+    searchTarget: NotebookPanel
+  ): Promise<ISearchMatch[]> {
+    this._searchTarget = searchTarget;
+    console.log(
+      'notebook provider: startSearch on options, target: ',
+      options,
+      ', ',
+      this._searchTarget
+    );
+    const cells = this._searchTarget.content.widgets;
+    const activeCell = this._searchTarget.content.activeCell;
+    const matchPromises: Promise<ISearchMatch[]>[] = [];
+    let indexTotal = 0;
+    cells.forEach((cell: Cell) => {
+      const cmEditor = cell.editor as CodeMirrorEditor;
+      const cmSearchProvider = new CodeMirrorSearchProvider();
+      matchPromises.push(
+        cmSearchProvider
+          .startSearch(options, cmEditor)
+          .then((matchesFromCell: ISearchMatch[]) => {
+            matchesFromCell.forEach(match => {
+              match.index = match.index + indexTotal;
+            });
+
+            if (activeCell === cell) {
+              cmSearchProvider
+                .highlightNext()
+                .then((selectedMatch: ISearchMatch) => {
+                  // this._highlightedIndex = selectedMatch.index + indexTotal;
+                });
+            }
+
+            indexTotal += matchesFromCell.length;
+            return matchesFromCell;
+          })
+      );
+
+      this._cmSearchProviders.push({
+        cell: cell,
+        provider: cmSearchProvider
+      });
+    });
+    return Promise.all(matchPromises).then(matchesFromCells => {
+      // this._matchesFromCells = matchesFromCells;
+      let result: ISearchMatch[] = [];
+      matchesFromCells.forEach((cellMatches: ISearchMatch[]) => {
+        result.concat(cellMatches);
+      });
+      return result;
+    });
+  }
+
+  endSearch(): Promise<void> {
+    console.log('notebook provider: endSearch');
+    this._cmSearchProviders.forEach(({ provider }) => {
+      provider.endSearch();
+    });
+    this._cmSearchProviders = [];
+    this._searchTarget = null;
+    return Promise.resolve();
+  }
+
+  highlightNext(): Promise<ISearchMatch> {
+    console.log('notebook provider: highlightNext');
+    const activeCell: Cell = this._searchTarget.content.activeCell;
+    const cellIndex = this._searchTarget.content.widgets.indexOf(activeCell);
+    const { provider }: { provider: ISearchProvider } = this._cmSearchProviders[
+      cellIndex
+    ];
+    return provider.highlightNext().then((match: ISearchMatch) => {
+      const allMatches = Private.getMatchesFromCells(this._cmSearchProviders);
+      console.log('new match before index set: ', match);
+      match.index = allMatches[cellIndex].find(
+        (matchTest: ISearchMatch) => match.column === matchTest.column
+      ).index;
+      console.log('matches in same line: ', allMatches[cellIndex]);
+      console.log('new match after index set: ', match);
+      return match;
+    });
+  }
+
+  highlightPrevious(): Promise<ISearchMatch> {
+    console.log('notebook provider: highlightPrevious');
+    return Promise.resolve({
+      text: '',
+      line: 0,
+      column: 0,
+      fragment: '',
+      index: 0
+    });
+  }
+
+  canSearchOn(domain: any): boolean {
+    console.log('notebook provider: canSearchOn');
+    return true;
+  }
+
+  get matches(): ISearchMatch[] {
+    return [].concat(...Private.getMatchesFromCells(this._cmSearchProviders));
+  }
+
+  get currentMatchIndex(): number {
+    console.log('notebook provider: currentMatchIndex');
+    return 0;
+  }
+
+  private _searchTarget: NotebookPanel;
+  private _cmSearchProviders: ICellSearchPair[] = [];
+  // private _matchesFromCells: ISearchMatch[][];
+  // private _highlightedIndex: number = 0;
+  // private _currentMatch: ISearchMatch;
+}
+
+namespace Private {
+  export function getMatchesFromCells(
+    cmSearchProviders: ICellSearchPair[]
+  ): ISearchMatch[][] {
+    let indexTotal = 0;
+    const result: ISearchMatch[][] = [];
+    cmSearchProviders.forEach(({ provider }) => {
+      const cellMatches = provider.matches;
+      cellMatches.forEach(match => {
+        match.index = match.index + indexTotal;
+      });
+      indexTotal += cellMatches.length;
+      result.push(cellMatches);
+    });
+    return result;
+  }
+}

+ 328 - 0
packages/documentsearch-extension/src/providers/tmpsearch.js

@@ -0,0 +1,328 @@
+function searchOverlay(query, caseInsensitive) {
+  if (typeof query == 'string') {
+    query = new RegExp(
+      query.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'),
+      caseInsensitive ? 'gi' : 'g'
+    );
+  } else if (!query.global) {
+    query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g');
+  }
+
+  return {
+    token: stream => {
+      query.lastIndex = stream.pos;
+      const match = query.exec(stream.string);
+      if (match && match.index === stream.pos) {
+        stream.pos += match[0].length || 1;
+        return 'searching';
+      } else if (match) {
+        stream.pos = match.index;
+      } else {
+        stream.skipToEnd();
+      }
+    }
+  };
+}
+
+function SearchState() {
+  this.posFrom = this.posTo = this.lastQuery = this.query = null;
+  this.overlay = null;
+}
+
+function getSearchState(cm) {
+  return cm.state.search || (cm.state.search = new SearchState());
+}
+
+function queryCaseInsensitive(query) {
+  return typeof query === 'string' && query === query.toLowerCase();
+}
+
+function getSearchCursor(cm, query, pos) {
+  // Heuristic: if the query string is all lowercase, do a case insensitive search.
+  return cm.getSearchCursor(query, pos, {
+    caseFold: queryCaseInsensitive(query),
+    multiline: true
+  });
+}
+
+function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
+  cm.openDialog(text, onEnter, {
+    value: deflt,
+    selectValueOnOpen: true,
+    closeOnEnter: false,
+    onClose: () => {
+      clearSearch(cm);
+    },
+    onKeyDown
+  });
+}
+
+function parseString(string) {
+  return string.replace(/\\(.)/g, (_, ch) => {
+    if (ch === 'n') {
+      return '\n';
+    }
+    if (ch === 'r') {
+      return '\r';
+    }
+    return ch;
+  });
+}
+
+function parseQuery(query) {
+  const isRE = query.match(/^\/(.*)\/([a-z]*)$/);
+  if (isRE) {
+    try {
+      query = new RegExp(isRE[1], isRE[2].indexOf('i') === -1 ? '' : 'i');
+    } catch (e) {} // Not a regular expression after all, do a string search
+  } else {
+    query = parseString(query);
+  }
+  if (typeof query == 'string' ? query == '' : query.test('')) {
+    query = /x^/;
+  }
+  return query;
+}
+
+function startSearch(cm, state, query) {
+  state.queryText = query;
+  state.query = parseQuery(query);
+  cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+  state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+  cm.addOverlay(state.overlay);
+  if (cm.showMatchesOnScrollbar) {
+    if (state.annotate) {
+      state.annotate.clear();
+      state.annotate = null;
+    }
+    state.annotate = cm.showMatchesOnScrollbar(
+      state.query,
+      queryCaseInsensitive(state.query)
+    );
+  }
+}
+
+function doSearch(cm, rev, persistent, immediate) {
+  const state = getSearchState(cm);
+  if (state.query) {
+    return findNext(cm, rev);
+  }
+  const q = cm.getSelection() || state.lastQuery;
+  if (q instanceof RegExp && q.source === 'x^') {
+    q = null;
+  }
+  if (persistent && cm.openDialog) {
+    let hiding = null;
+    const searchNext = (query, event) => {
+      CodeMirror.e_stop(event);
+      if (!query) return;
+      if (query != state.queryText) {
+        startSearch(cm, state, query);
+        state.posFrom = state.posTo = cm.getCursor();
+      }
+      if (hiding) {
+        hiding.style.opacity = 1;
+      }
+      findNext(cm, event.shiftKey, (_, to) => {
+        let dialog;
+        if (
+          to.line < 3 &&
+          document.querySelector &&
+          (dialog = cm.display.wrapper.querySelector('.CodeMirror-dialog')) &&
+          dialog.getBoundingClientRect().bottom - 4 >
+            cm.cursorCoords(to, 'window').top
+        ) {
+          (hiding = dialog).style.opacity = 0.4;
+        }
+      });
+    };
+    persistentDialog(cm, getQueryDialog(cm), q, searchNext, (event, query) => {
+      const keyName = CodeMirror.keyName(event);
+      let extra = cm.getOption('extraKeys'),
+        cmd =
+          (extra && extra[keyName]) ||
+          CodeMirror.keyMap[cm.getOption('keyMap')][keyName];
+      if (
+        cmd === 'findNext' ||
+        cmd === 'findPrev' ||
+        cmd === 'findPersistentNext' ||
+        cmd === 'findPersistentPrev'
+      ) {
+        CodeMirror.e_stop(event);
+        startSearch(cm, getSearchState(cm), query);
+        cm.execCommand(cmd);
+      } else if (cmd === 'find' || cmd === 'findPersistent') {
+        CodeMirror.e_stop(event);
+        searchNext(query, event);
+      }
+    });
+    if (immediate && q) {
+      startSearch(cm, state, q);
+      findNext(cm, rev);
+    }
+  } else {
+    dialog(cm, getQueryDialog(cm), 'Search for:', q, query => {
+      if (query && !state.query) {
+        cm.operation(() => {
+          startSearch(cm, state, query);
+          state.posFrom = state.posTo = cm.getCursor();
+          findNext(cm, rev);
+        });
+      }
+    });
+  }
+}
+
+function findNext(cm, rev, callback) {
+  cm.operation(() => {
+    const state = getSearchState(cm);
+    const cursor = getSearchCursor(
+      cm,
+      state.query,
+      rev ? state.posFrom : state.posTo
+    );
+    if (!cursor.find(rev)) {
+      cursor = getSearchCursor(
+        cm,
+        state.query,
+        rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)
+      );
+      if (!cursor.find(rev)) {
+        return;
+      }
+    }
+    cm.setSelection(cursor.from(), cursor.to());
+    cm.scrollIntoView({ from: cursor.from(), to: cursor.to() }, 20);
+    state.posFrom = cursor.from();
+    state.posTo = cursor.to();
+    if (callback) {
+      callback(cursor.from(), cursor.to());
+    }
+  });
+}
+
+function clearSearch(cm) {
+  cm.operation(() => {
+    const state = getSearchState(cm);
+    state.lastQuery = state.query;
+    if (!state.query) return;
+    state.query = state.queryText = null;
+    cm.removeOverlay(state.overlay);
+    if (state.annotate) {
+      state.annotate.clear();
+      state.annotate = null;
+    }
+  });
+}
+
+function replaceAll(cm, query, text) {
+  cm.operation(() => {
+    for (const cursor = getSearchCursor(cm, query); cursor.findNext(); ) {
+      if (typeof query !== 'string') {
+        const match = cm.getRange(cursor.from(), cursor.to()).match(query);
+        cursor.replace(text.replace(/\$(\d)/g, (_, i) => match[i]));
+      } else {
+        cursor.replace(text);
+      }
+    }
+  });
+}
+
+function replace(cm, all) {
+  if (cm.getOption('readOnly')) {
+    return;
+  }
+  const query = cm.getSelection() || getSearchState(cm).lastQuery;
+  const dialogText = `<span class="CodeMirror-search-label">${
+    all ? cm.phrase('Replace all:') : cm.phrase('Replace:')
+  }</span>`;
+  dialog(
+    cm,
+    dialogText + getReplaceQueryDialog(cm),
+    dialogText,
+    query,
+    query => {
+      if (!query) return;
+      query = parseQuery(query);
+      dialog(
+        cm,
+        getReplacementQueryDialog(cm),
+        cm.phrase('Replace with:'),
+        '',
+        text => {
+          text = parseString(text);
+          if (all) {
+            replaceAll(cm, query, text);
+          } else {
+            clearSearch(cm);
+            const cursor = getSearchCursor(cm, query, cm.getCursor('from'));
+            const advance = () => {
+              let start = cursor.from(),
+                match;
+              if (!(match = cursor.findNext())) {
+                cursor = getSearchCursor(cm, query);
+                if (
+                  !(match = cursor.findNext()) ||
+                  (start &&
+                    cursor.from().line === start.line &&
+                    cursor.from().ch === start.ch)
+                ) {
+                  return;
+                }
+              }
+              cm.setSelection(cursor.from(), cursor.to());
+              cm.scrollIntoView({ from: cursor.from(), to: cursor.to() });
+              confirmDialog(
+                cm,
+                getDoReplaceConfirm(cm),
+                cm.phrase('Replace?'),
+                [
+                  () => {
+                    doReplace(match);
+                  },
+                  advance,
+                  () => {
+                    replaceAll(cm, query, text);
+                  }
+                ]
+              );
+            };
+            const doReplace = match => {
+              cursor.replace(
+                typeof query === 'string'
+                  ? text
+                  : text.replace(/\$(\d)/g, (_, i) => match[i])
+              );
+              advance();
+            };
+            advance();
+          }
+        }
+      );
+    }
+  );
+}
+
+CodeMirror.commands.find = function(cm) {
+  clearSearch(cm);
+  doSearch(cm);
+};
+CodeMirror.commands.findPersistent = function(cm) {
+  clearSearch(cm);
+  doSearch(cm, false, true);
+};
+CodeMirror.commands.findPersistentNext = function(cm) {
+  doSearch(cm, false, true, true);
+};
+CodeMirror.commands.findPersistentPrev = function(cm) {
+  doSearch(cm, true, true, true);
+};
+CodeMirror.commands.findNext = doSearch;
+CodeMirror.commands.findPrev = function(cm) {
+  doSearch(cm, true);
+};
+CodeMirror.commands.clearSearch = clearSearch;
+CodeMirror.commands.replace = replace;
+CodeMirror.commands.replaceAll = function(cm) {
+  replace(cm, true);
+};

+ 126 - 0
packages/documentsearch-extension/src/searchbox.ts

@@ -0,0 +1,126 @@
+import { Widget } from '@phosphor/widgets';
+
+import { Message } from '@phosphor/messaging';
+
+import { ISignal, Signal } from '@phosphor/signaling';
+import { ISearchOptions } from '.';
+
+const DOCUMENT_SEARCH_CLASS = 'jp-DocumentSearch';
+const SEARCHBOX_CLASS = 'jp-DocumentSearch-searchbox';
+const WRAPPER_CLASS = 'jp-DocumentSearch-wrapper';
+const INPUT_CLASS = 'jp-DocumentSearch-input';
+
+export class SearchBox extends Widget {
+  constructor() {
+    super({ node: Private.createNode() });
+    this.id = 'search-box';
+    this.title.iconClass = 'jp-ExtensionIcon jp-SideBar-tabIcon';
+    this.title.caption = 'Search document';
+    this.addClass(DOCUMENT_SEARCH_CLASS);
+  }
+
+  get inputNode(): HTMLInputElement {
+    return this.node.getElementsByClassName(INPUT_CLASS)[0] as HTMLInputElement;
+  }
+
+  get startSearch(): ISignal<this, ISearchOptions> {
+    return this._startSearch;
+  }
+
+  get endSearch(): ISignal<this, void> {
+    return this._endSearch;
+  }
+
+  get highlightNext(): ISignal<this, void> {
+    return this._highlightNext;
+  }
+
+  get highlightPrevious(): ISignal<this, void> {
+    return this._highlightPrevious;
+  }
+
+  handleEvent(event: Event): void {
+    if (event.type === 'keydown') {
+      this._handleKeyDown(event as KeyboardEvent);
+    }
+  }
+
+  protected onBeforeAttach(msg: Message): void {
+    // Add event listeners
+    this.node.addEventListener('keydown', this.handleEvent.bind(this));
+  }
+
+  protected onAfterAttach(msg: Message): void {
+    // Remove event listeners
+    this.node.removeEventListener('keydown', this);
+  }
+
+  private _handleKeyDown(event: KeyboardEvent): void {
+    if (event.keyCode === 13) {
+      // execute search!
+      const searchTerm = this.inputNode.value;
+      if (searchTerm.length === 0) {
+        return;
+      }
+      console.log(
+        'received enter keydown, execute search on searchTerm: ',
+        searchTerm
+      );
+      const options: ISearchOptions = {
+        query: searchTerm,
+        caseSensitive: false,
+        regex: false
+      };
+
+      if (this.optionsEqual(this._lastOptions, options)) {
+        this._highlightNext.emit(undefined);
+        return;
+      }
+
+      this._lastOptions = options;
+      this._startSearch.emit(options);
+    }
+  }
+
+  private optionsEqual(a: ISearchOptions, b: ISearchOptions) {
+    return (
+      !!a &&
+      !!b &&
+      a.query === b.query &&
+      a.caseSensitive === b.caseSensitive &&
+      a.regex === b.regex
+    );
+  }
+
+  private _startSearch = new Signal<this, ISearchOptions>(this);
+  private _endSearch = new Signal<this, void>(this);
+  private _highlightNext = new Signal<this, void>(this);
+  private _highlightPrevious = new Signal<this, void>(this);
+  private _lastOptions: ISearchOptions;
+}
+
+namespace Private {
+  export function createNode() {
+    const node = document.createElement('div');
+    const search = document.createElement('div');
+    const wrapper = document.createElement('div');
+    const input = document.createElement('input');
+    const results = document.createElement('div');
+    const dummyText = document.createElement('p');
+
+    input.placeholder = 'SEARCH';
+    dummyText.innerHTML = 'Dummy result';
+
+    search.className = SEARCHBOX_CLASS;
+    wrapper.className = WRAPPER_CLASS;
+    input.className = INPUT_CLASS;
+
+    search.appendChild(wrapper);
+    wrapper.appendChild(input);
+    results.appendChild(dummyText);
+    node.appendChild(search);
+    node.appendChild(results);
+
+    return node;
+  }
+}

+ 44 - 0
packages/documentsearch-extension/src/searchproviderregistry.ts

@@ -0,0 +1,44 @@
+import { ISearchProvider } from './index';
+
+import {
+  CodeMirrorSearchProvider,
+  NotebookSearchProvider
+} from './searchproviders';
+
+const DEFAULT_NOTEBOOK_SEARCH_PROVIDER = 'jl-defaultNotebookSearchProvider';
+const DEFAULT_CODEMIRROR_SEARCH_PROVIDER = 'jl-defaultCodeMirrorSearchProvider';
+
+export class SearchProviderRegistry {
+  constructor() {
+    this.registerProvider(
+      DEFAULT_NOTEBOOK_SEARCH_PROVIDER,
+      new NotebookSearchProvider()
+    );
+    this.registerProvider(
+      DEFAULT_CODEMIRROR_SEARCH_PROVIDER,
+      new CodeMirrorSearchProvider()
+    );
+  }
+
+  registerProvider(key: string, provider: ISearchProvider): void {
+    this._providers[key] = provider;
+  }
+
+  deregisterProvider(key: string): boolean {
+    if (!this._providers[key]) {
+      return false;
+    }
+    this._providers[key] = undefined;
+    return true;
+  }
+
+  get providers(): ISearchProvider[] {
+    return Object.keys(this._providers).map(k => this._providers[k]);
+  }
+
+  private _providers: Private.ProviderMap = {};
+}
+
+namespace Private {
+  export type ProviderMap = { [key: string]: ISearchProvider };
+}

+ 2 - 0
packages/documentsearch-extension/src/searchproviders.ts

@@ -0,0 +1,2 @@
+export * from './providers/notebooksearchprovider';
+export * from './providers/codemirrorsearchprovider';

+ 27 - 0
packages/documentsearch-extension/style/index.css

@@ -0,0 +1,27 @@
+.jp-DocumentSearch {
+  display: flex;
+  flex-direction: column;
+  min-width: var(--jp-sidebar-min-width);
+  color: var(--jp-ui-font-color1);
+  background: var(--jp-layout-color1);
+  /* This is needed so that all font sizing of children done in ems is
+   * relative to this base size */
+  font-size: var(--jp-ui-font-size1);
+}
+
+.jp-DocumentSearch-searchbox {
+  padding: 8px;
+}
+
+.jp-DocumentSearch-wrapper {
+  padding: 4px 6px;
+  background: white;
+  border: 1px solid #e0e0e0;
+}
+
+.jp-DocumentSearch-input {
+  width: 100%;
+  border: none;
+  outline: none;
+  font-size: 16px;
+}

+ 25 - 0
packages/documentsearch-extension/tsconfig.json

@@ -0,0 +1,25 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/**/*"],
+  "references": [
+    {
+      "path": "../application"
+    },
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../codeeditor"
+    },
+    {
+      "path": "../codemirror"
+    },
+    {
+      "path": "../notebook"
+    }
+  ]
+}

+ 1 - 0
packages/metapackage/package.json

@@ -49,6 +49,7 @@
     "@jupyterlab/docmanager": "^0.19.1",
     "@jupyterlab/docmanager-extension": "^0.19.1",
     "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/documentsearch-extension": "^0.7.1",
     "@jupyterlab/extensionmanager": "^0.19.1",
     "@jupyterlab/extensionmanager-extension": "^0.19.1",
     "@jupyterlab/faq-extension": "^0.19.1",

+ 3 - 0
packages/metapackage/tsconfig.json

@@ -63,6 +63,9 @@
     {
       "path": "../docregistry"
     },
+    {
+      "path": "../documentsearch-extension"
+    },
     {
       "path": "../extensionmanager"
     },

+ 3 - 3
yarn.lock

@@ -662,9 +662,9 @@
   version "4.0.10"
   resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.10.tgz#0eb222c7353adde8e0980bea04165d4d3b6afef3"
 
-"@types/codemirror@~0.0.46":
-  version "0.0.60"
-  resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.60.tgz#178f69f2b87253aedb03518b99c20298420a3aa3"
+"@types/codemirror@~0.0.70":
+  version "0.0.70"
+  resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.70.tgz#2d9c850d6afbc93162c1434a827f86ad5ee90e35"
 
 "@types/comment-json@^1.1.0":
   version "1.1.1"

Some files were not shown because too many files changed in this diff