Kaynağa Gözat

Merge pull request #5795 from aschlaep/ctrl-f-init

Add ctrl-f/g support for notebooks and text files
Jason Grout 6 yıl önce
ebeveyn
işleme
937a3d8470
34 değiştirilmiş dosya ile 2061 ekleme ve 966 silme
  1. 3 0
      dev_mode/package.json
  2. 1 1
      examples/console/package.json
  3. 1 1
      examples/filebrowser/package.json
  4. 1 1
      examples/notebook/package.json
  5. 1 1
      packages/codemirror/package.json
  6. 65 11
      packages/codemirror/src/editor.ts
  7. 1 0
      packages/codemirror/src/typings.d.ts
  8. 26 948
      packages/codemirror/typings/codemirror/codemirror.d.ts
  9. 58 0
      packages/documentsearch-extension/package.json
  10. 24 0
      packages/documentsearch-extension/schema/plugin.json
  11. 285 0
      packages/documentsearch-extension/src/index.ts
  12. 410 0
      packages/documentsearch-extension/src/providers/codemirrorsearchprovider.ts
  13. 285 0
      packages/documentsearch-extension/src/providers/notebooksearchprovider.ts
  14. 191 0
      packages/documentsearch-extension/src/searchinstance.ts
  15. 309 0
      packages/documentsearch-extension/src/searchoverlay.tsx
  16. 89 0
      packages/documentsearch-extension/src/searchproviderregistry.ts
  17. 4 0
      packages/documentsearch-extension/src/searchproviders.ts
  18. 126 0
      packages/documentsearch-extension/style/index.css
  19. 34 0
      packages/documentsearch-extension/tsconfig.json
  20. 1 0
      packages/metapackage/package.json
  21. 3 0
      packages/metapackage/tsconfig.json
  22. 11 0
      packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg
  23. 11 0
      packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg
  24. 19 0
      packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg
  25. 19 0
      packages/theme-dark-extension/style/icons/jupyter/search_regex.svg
  26. 4 0
      packages/theme-dark-extension/style/urls.css
  27. 6 0
      packages/theme-dark-extension/style/variables.css
  28. 11 0
      packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg
  29. 11 0
      packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg
  30. 19 0
      packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg
  31. 19 0
      packages/theme-light-extension/style/icons/jupyter/search_regex.svg
  32. 4 0
      packages/theme-light-extension/style/urls.css
  33. 6 0
      packages/theme-light-extension/style/variables.css
  34. 3 3
      yarn.lock

+ 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.19.1",
     "@jupyterlab/extensionmanager": "^0.19.1",
     "@jupyterlab/extensionmanager-extension": "^0.19.1",
     "@jupyterlab/faq-extension": "^0.19.1",
@@ -147,6 +148,7 @@
       "@jupyterlab/console-extension": "",
       "@jupyterlab/csvviewer-extension": "",
       "@jupyterlab/docmanager-extension": "",
+      "@jupyterlab/documentsearch-extension": "",
       "@jupyterlab/extensionmanager-extension": "",
       "@jupyterlab/faq-extension": "",
       "@jupyterlab/filebrowser-extension": "",
@@ -251,6 +253,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

@@ -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
packages/codemirror/package.json

@@ -46,7 +46,7 @@
     "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"

+ 65 - 11
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';
 
@@ -377,6 +379,65 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
     this._clearHover();
   }
 
+  // todo: docs, maybe define overlay options as a type?
+  addOverlay(mode: string | object, options?: object): void {
+    this._editor.addOverlay(mode, options);
+  }
+
+  removeOverlay(mode: string | object): void {
+    this._editor.removeOverlay(mode);
+  }
+
+  getSearchCursor(
+    query: string | RegExp,
+    start?: CodeMirror.Position,
+    caseFold?: boolean
+  ): CodeMirror.SearchCursor {
+    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);
+  }
+
+  cursorCoords(
+    where: boolean,
+    mode?: 'window' | 'page' | 'local'
+  ): { left: number; top: number; bottom: number } {
+    return this._editor.cursorCoords(where, mode);
+  }
+
+  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.
    *
@@ -415,7 +476,10 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
    * Reveal the given selection in the editor.
    */
   revealSelection(selection: CodeEditor.IRange): void {
-    const range = this._toCodeMirrorRange(selection);
+    const range = {
+      from: this._toCodeMirrorPosition(selection.start),
+      to: this._toCodeMirrorPosition(selection.end)
+    };
     this._editor.scrollIntoView(range);
   }
 
@@ -763,16 +827,6 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
     };
   }
 
-  /**
-   * Converts an editor selection to a code mirror selection.
-   */
-  private _toCodeMirrorRange(range: CodeEditor.IRange): CodeMirror.Range {
-    return {
-      from: this._toCodeMirrorPosition(range.start),
-      to: this._toCodeMirrorPosition(range.end)
-    };
-  }
-
   /**
    * Convert a code mirror position to an editor position.
    */

+ 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"/>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 26 - 948
packages/codemirror/typings/codemirror/codemirror.d.ts


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

@@ -0,0 +1,58 @@
+{
+  "name": "@jupyterlab/documentsearch-extension",
+  "version": "0.19.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/cells": "^0.19.1",
+    "@jupyterlab/codeeditor": "^0.19.1",
+    "@jupyterlab/codemirror": "^0.19.1",
+    "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/mainmenu": "^0.8.1",
+    "@jupyterlab/notebook": "^0.19.2",
+    "@phosphor/disposable": "^1.1.2",
+    "@phosphor/messaging": "^1.2.2",
+    "@phosphor/signaling": "^1.2.2",
+    "@phosphor/widgets": "^1.6.0",
+    "codemirror": "~5.42.0",
+    "react": "~16.4.2",
+    "react-dom": "~16.4.2"
+  },
+  "devDependencies": {
+    "rimraf": "~2.6.2",
+    "typescript": "~3.1.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "jupyterlab": {
+    "extension": true,
+    "schemaDir": "schema"
+  }
+}

+ 24 - 0
packages/documentsearch-extension/schema/plugin.json

@@ -0,0 +1,24 @@
+{
+  "title": "Document Search",
+  "description": "Document search plugin.",
+  "jupyter.lab.shortcuts": [
+    {
+      "command": "documentsearch:start",
+      "keys": ["Accel F"],
+      "selector": "body"
+    },
+    {
+      "command": "documentsearch:highlightNext",
+      "keys": ["Accel G"],
+      "selector": "body"
+    },
+    {
+      "command": "documentsearch:highlightPrevious",
+      "keys": ["Accel Shift G"],
+      "selector": "body"
+    }
+  ],
+  "properties": {},
+  "additionalProperties": false,
+  "type": "object"
+}

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

@@ -0,0 +1,285 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+import '../style/index.css';
+
+import { SearchProviderRegistry } from './searchproviderregistry';
+import { SearchInstance } from './searchinstance';
+
+import {
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import { ICommandPalette } from '@jupyterlab/apputils';
+
+import { IMainMenu } from '@jupyterlab/mainmenu';
+
+import { ISignal } from '@phosphor/signaling';
+
+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;
+}
+
+/**
+ * This interface is meant to enforce that SearchProviders implement the static
+ * canSearchOn function.
+ */
+export interface ISearchProviderConstructor {
+  new (): ISearchProvider;
+  /**
+   * Report whether or not this provider has the ability to search on the given object
+   */
+  canSearchOn(domain: any): 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 query A RegExp to be use to perform the search
+   * @param searchTarget The widget to be searched
+   *
+   * @returns A promise that resolves with a list of all matches
+   */
+  startQuery(query: RegExp, searchTarget: any): Promise<ISearchMatch[]>;
+
+  /**
+   * Clears state of a search provider to prepare for startQuery to be called
+   * in order to start a new query or refresh an existing one.
+   *
+   * @returns A promise that resolves when the search provider is ready to
+   * begin a new search.
+   */
+  endQuery(): Promise<void>;
+
+  /**
+   * Resets UI state as it was before the search process began.  Cleans up and
+   * disposes of all internal state.
+   *
+   * @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 | undefined>;
+
+  /**
+   * Move the current match indicator to the previous match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  highlightPrevious(): Promise<ISearchMatch | undefined>;
+
+  /**
+   * The same list of matches provided by the startQuery promise resoluton
+   */
+  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.
+   */
+  readonly currentMatchIndex: number | null;
+}
+
+export interface IDisplayState {
+  /**
+   * The index of the currently selected match
+   */
+  currentIndex: number;
+
+  /**
+   * The total number of matches found in the document
+   */
+  totalMatches: number;
+
+  /**
+   * Should the search be case sensitive?
+   */
+  caseSensitive: boolean;
+
+  /**
+   * Should the search string be treated as a RegExp?
+   */
+  useRegex: boolean;
+
+  /**
+   * The text in the entry
+   */
+  inputText: string;
+
+  /**
+   * The query constructed from the text and the case/regex flags
+   */
+  query: RegExp;
+
+  /**
+   * An error message (used for bad regex syntax)
+   */
+  errorMessage: string;
+
+  /**
+   * Should the focus forced into the input on the next render?
+   */
+  forceFocus: boolean;
+}
+
+/**
+ * Initialization data for the document-search extension.
+ */
+const extension: JupyterFrontEndPlugin<void> = {
+  id: '@jupyterlab/documentsearch:plugin',
+  autoStart: true,
+  requires: [ICommandPalette],
+  optional: [IMainMenu],
+  activate: (
+    app: JupyterFrontEnd,
+    palette: ICommandPalette,
+    mainMenu: IMainMenu | null
+  ) => {
+    // Create registry, retrieve all default providers
+    const registry: SearchProviderRegistry = new SearchProviderRegistry();
+    const activeSearches: {
+      [key: string]: SearchInstance;
+    } = {};
+
+    const startCommand: string = 'documentsearch:start';
+    const nextCommand: string = 'documentsearch:highlightNext';
+    const prevCommand: string = 'documentsearch:highlightPrevious';
+    app.commands.addCommand(startCommand, {
+      label: 'Find…',
+      isEnabled: () => {
+        const currentWidget = app.shell.currentWidget;
+        if (!currentWidget) {
+          return;
+        }
+        return registry.getProviderForWidget(currentWidget) !== undefined;
+      },
+      execute: () => {
+        const currentWidget = app.shell.currentWidget;
+        if (!currentWidget) {
+          return;
+        }
+        const widgetId = currentWidget.id;
+        let searchInstance = activeSearches[widgetId];
+        if (!searchInstance) {
+          const searchProvider = registry.getProviderForWidget(currentWidget);
+          if (!searchProvider) {
+            return;
+          }
+          searchInstance = new SearchInstance(currentWidget, searchProvider);
+
+          activeSearches[widgetId] = searchInstance;
+          // find next and previous are now enabled
+          app.commands.notifyCommandChanged();
+
+          searchInstance.disposed.connect(() => {
+            delete activeSearches[widgetId];
+            // find next and previous are now not enabled
+            app.commands.notifyCommandChanged();
+          });
+        }
+        searchInstance.focusInput();
+      }
+    });
+
+    app.commands.addCommand(nextCommand, {
+      label: 'Find Next',
+      isEnabled: () => {
+        const currentWidget = app.shell.currentWidget;
+        if (!currentWidget) {
+          return;
+        }
+        return !!activeSearches[currentWidget.id];
+      },
+      execute: async () => {
+        const currentWidget = app.shell.currentWidget;
+        if (!currentWidget) {
+          return;
+        }
+        const instance = activeSearches[currentWidget.id];
+        if (!instance) {
+          return;
+        }
+
+        await instance.provider.highlightNext();
+        instance.updateIndices();
+      }
+    });
+
+    app.commands.addCommand(prevCommand, {
+      label: 'Find Previous',
+      isEnabled: () => {
+        const currentWidget = app.shell.currentWidget;
+        if (!currentWidget) {
+          return;
+        }
+        return !!activeSearches[currentWidget.id];
+      },
+      execute: async () => {
+        const currentWidget = app.shell.currentWidget;
+        if (!currentWidget) {
+          return;
+        }
+        const instance = activeSearches[currentWidget.id];
+        if (!instance) {
+          return;
+        }
+
+        await instance.provider.highlightPrevious();
+        instance.updateIndices();
+      }
+    });
+
+    // Add the command to the palette.
+    palette.addItem({ command: startCommand, category: 'Main Area' });
+    palette.addItem({ command: nextCommand, category: 'Main Area' });
+    palette.addItem({ command: prevCommand, category: 'Main Area' });
+
+    // Add main menu notebook menu.
+    if (mainMenu) {
+      mainMenu.editMenu.addGroup(
+        [
+          { command: startCommand },
+          { command: nextCommand },
+          { command: prevCommand }
+        ],
+        10
+      );
+    }
+  }
+};
+
+export default extension;

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

@@ -0,0 +1,410 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+/*
+  Parts of the implementation of the search in this file were derived from
+  CodeMirror's search at:
+  https://github.com/codemirror/CodeMirror/blob/c2676685866c571a1c9c82cb25018cc08b4d42b2/addon/search/search.js
+  which is licensed with the following license:
+
+  MIT License
+
+  Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+import * as CodeMirror from 'codemirror';
+
+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 {
+  /**
+   * Initialize the search using the provided options.  Should update the UI
+   * to highlight all matches and "select" whatever the first match should be.
+   *
+   * @param query A RegExp to be use to perform the search
+   * @param searchTarget The widget to be searched
+   *
+   * @returns A promise that resolves with a list of all matches
+   */
+  async startQuery(query: RegExp, domain: any): Promise<ISearchMatch[]> {
+    if (domain instanceof CodeMirrorEditor) {
+      this._cm = domain;
+    } else if (domain) {
+      this._cm = domain.content.editor;
+    }
+    await this.endQuery();
+
+    this._query = query;
+
+    CodeMirror.on(this._cm.doc, 'change', this._onDocChanged.bind(this));
+    this._refreshOverlay();
+    this._setInitialMatches(query);
+
+    const matches = this._parseMatchesFromState();
+    if (matches.length === 0) {
+      return [];
+    }
+    if (!this.isSubProvider) {
+      const cursorMatch = this._findNext(false);
+      const match = this._matchState[cursorMatch.from.line][
+        cursorMatch.from.ch
+      ];
+      this._matchIndex = match.index;
+    }
+    return matches;
+  }
+
+  /**
+   * Clears state of a search provider to prepare for startQuery to be called
+   * in order to start a new query or refresh an existing one.
+   *
+   * @returns A promise that resolves when the search provider is ready to
+   * begin a new search.
+   */
+  async endQuery(): Promise<void> {
+    this._matchState = {};
+    this._matchIndex = null;
+    this._cm.removeOverlay(this._overlay);
+    CodeMirror.off(this._cm.doc, 'change', this._onDocChanged.bind(this));
+  }
+
+  /**
+   * Resets UI state, removes all matches.
+   *
+   * @returns A promise that resolves when all state has been cleaned up.
+   */
+  async endSearch(): Promise<void> {
+    if (!this.isSubProvider) {
+      this._cm.focus();
+    }
+    this.endQuery();
+  }
+
+  /**
+   * Move the current match indicator to the next match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  async highlightNext(): Promise<ISearchMatch | undefined> {
+    const cursorMatch = this._findNext(false);
+    if (!cursorMatch) {
+      return;
+    }
+    const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
+    this._matchIndex = match.index;
+    return match;
+  }
+
+  /**
+   * Move the current match indicator to the previous match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  async highlightPrevious(): Promise<ISearchMatch | undefined> {
+    const cursorMatch = this._findNext(true);
+    if (!cursorMatch) {
+      return;
+    }
+    const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
+    this._matchIndex = match.index;
+    return match;
+  }
+
+  /**
+   * Report whether or not this provider has the ability to search on the given object
+   */
+  static canSearchOn(domain: any): boolean {
+    return domain.content && domain.content.editor instanceof CodeMirrorEditor;
+  }
+
+  /**
+   * The same list of matches provided by the startQuery promise resoluton
+   */
+  get matches(): ISearchMatch[] {
+    return this._parseMatchesFromState();
+  }
+
+  /**
+   * Signal indicating that something in the search has changed, so the UI should update
+   */
+  get changed(): ISignal<this, void> {
+    return this._changed;
+  }
+
+  /**
+   * The current index of the selected match.
+   */
+  get currentMatchIndex(): number {
+    return this._matchIndex;
+  }
+
+  clearSelection(): void {
+    return null;
+  }
+
+  /**
+   * Set whether or not the CodemirrorSearchProvider will wrap to the beginning
+   * or end of the document on invocations of highlightNext or highlightPrevious, respectively
+   */
+  isSubProvider = false;
+
+  private _onDocChanged(_: any, changeObj: CodeMirror.EditorChange) {
+    // If we get newlines added/removed, the line numbers across the
+    // match state are all shifted, so here we need to recalculate it
+    if (changeObj.text.length > 1 || changeObj.removed.length > 1) {
+      this._setInitialMatches(this._query);
+      this._changed.emit(undefined);
+    }
+  }
+
+  private _refreshOverlay() {
+    this._cm.operation(() => {
+      // clear search first
+      this._cm.removeOverlay(this._overlay);
+      this._overlay = this._getSearchOverlay();
+      this._cm.addOverlay(this._overlay);
+      this._changed.emit(null);
+    });
+  }
+
+  /**
+   * Do a full search on the entire document.
+   *
+   * This manually constructs the initial match state across the whole
+   * document. This must be done manually because the codemirror overlay
+   * is lazy-loaded, so it will only tokenize lines that are in or near
+   * the viewport.  This is sufficient for efficiently maintaining the
+   * state when changes are made to the document, as changes occur in or
+   * near the viewport, but to scan the whole document, a manual search
+   * across the entire content is required.
+   *
+   * @param query The search term
+   */
+  private _setInitialMatches(query: RegExp) {
+    this._matchState = {};
+
+    const start = CodeMirror.Pos(this._cm.doc.firstLine(), 0);
+    const end = CodeMirror.Pos(this._cm.doc.lastLine());
+    const content = this._cm.doc.getRange(start, end);
+    const lines = content.split('\n');
+    let totalMatchIndex = 0;
+    lines.forEach((line, lineNumber) => {
+      query.lastIndex = 0;
+      let match = query.exec(line);
+      while (match) {
+        const col = match.index;
+        const matchObj: ISearchMatch = {
+          text: match[0],
+          line: lineNumber,
+          column: col,
+          fragment: line,
+          index: totalMatchIndex
+        };
+        if (!this._matchState[lineNumber]) {
+          this._matchState[lineNumber] = {};
+        }
+        this._matchState[lineNumber][col] = matchObj;
+        match = query.exec(line);
+      }
+    });
+  }
+
+  private _getSearchOverlay() {
+    return {
+      /**
+       * Token function is called when a line needs to be processed -
+       * when the overlay is intially created, it's called on all lines;
+       * when a line is modified and needs to be re-evaluated, it's called
+       * on just that line.
+       *
+       * This implementation of the token function both constructs/maintains
+       * the overlay and keeps track of the match state as the document is
+       * updated while a search is active.
+       */
+      token: (stream: CodeMirror.StringStream) => {
+        const currentPos = stream.pos;
+        this._query.lastIndex = currentPos;
+        const lineText = stream.string;
+        const match = this._query.exec(lineText);
+        const line = (stream as any).lineOracle.line;
+
+        // If starting at position 0, the tokenization of this line has just started.
+        // Blow away everything on this line in the state so it can be updated.
+        if (
+          stream.start === currentPos &&
+          currentPos === 0 &&
+          !!this._matchState[line]
+        ) {
+          this._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 (!this._matchState[line]) {
+            this._matchState[line] = {};
+          }
+          this._matchState[line][currentPos] = matchObj;
+          // move the stream along and return searching style for the token
+          stream.pos += matchLength || 1;
+
+          // if the last thing on the line was a match, make sure we still
+          // emit the changed signal so the display can pick up the updates
+          if (stream.eol) {
+            this._changed.emit(undefined);
+          }
+          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
+          this._changed.emit(undefined);
+          stream.skipToEnd();
+        }
+      }
+    };
+  }
+
+  private _findNext(reverse: boolean): Private.ICodeMirrorMatch {
+    return this._cm.operation(() => {
+      const caseSensitive = this._query.ignoreCase;
+      const cursorToGet = reverse ? 'from' : 'to';
+      const lastPosition = this._cm.getCursor(cursorToGet);
+      const position = this._toEditorPos(lastPosition);
+      let cursor: CodeMirror.SearchCursor = this._cm.getSearchCursor(
+        this._query,
+        lastPosition,
+        !caseSensitive
+      );
+      if (!cursor.find(reverse)) {
+        // if we don't want to loop, no more matches found, reset the cursor and exit
+        if (this.isSubProvider) {
+          this._cm.setCursorPosition(position);
+          this._matchIndex = null;
+          return null;
+        }
+
+        // if we do want to loop, try searching from the bottom/top
+        const startOrEnd = reverse
+          ? CodeMirror.Pos(this._cm.lastLine())
+          : CodeMirror.Pos(this._cm.firstLine(), 0);
+        cursor = this._cm.getSearchCursor(
+          this._query,
+          startOrEnd,
+          !caseSensitive
+        );
+        if (!cursor.find(reverse)) {
+          return null;
+        }
+      }
+      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
+        }
+      };
+
+      this._cm.setSelection(selRange);
+      this._cm.scrollIntoView(
+        {
+          from: fromPos,
+          to: toPos
+        },
+        100
+      );
+      return {
+        from: fromPos,
+        to: toPos
+      };
+    });
+  }
+
+  private _parseMatchesFromState(): ISearchMatch[] {
+    let index = 0;
+    // Flatten state map and update the index of each match
+    const matches: ISearchMatch[] = Object.keys(this._matchState).reduce(
+      (result: ISearchMatch[], lineNumber: string) => {
+        const lineKey = parseInt(lineNumber, 10);
+        const lineMatches: { [key: number]: ISearchMatch } = this._matchState[
+          lineKey
+        ];
+        Object.keys(lineMatches).forEach((pos: string) => {
+          const posKey = parseInt(pos, 10);
+          const match: ISearchMatch = lineMatches[posKey];
+          match.index = index;
+          index += 1;
+          result.push(match);
+        });
+        return result;
+      },
+      []
+    );
+    return matches;
+  }
+
+  private _toEditorPos(posIn: CodeMirror.Position): CodeEditor.IPosition {
+    return {
+      line: posIn.line,
+      column: posIn.ch
+    };
+  }
+
+  private _query: RegExp;
+  private _cm: CodeMirrorEditor;
+  private _matchIndex: number;
+  private _matchState: MatchMap = {};
+  private _changed = new Signal<this, void>(this);
+  private _overlay: any;
+}
+
+export class SearchState {
+  public posFrom: CodeMirror.Position;
+  public posTo: CodeMirror.Position;
+  public lastQuery: string;
+  public query: RegExp;
+}
+
+namespace Private {
+  export interface ICodeMirrorMatch {
+    from: CodeMirror.Position;
+    to: CodeMirror.Position;
+  }
+}

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

@@ -0,0 +1,285 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+import { ISearchProvider, ISearchMatch } from '../index';
+import { CodeMirrorSearchProvider } from './codemirrorsearchprovider';
+
+import { NotebookPanel } 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 {
+  cell: Cell;
+  provider: ISearchProvider;
+}
+
+export class NotebookSearchProvider implements 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 query A RegExp to be use to perform the search
+   * @param searchTarget The widget to be searched
+   *
+   * @returns A promise that resolves with a list of all matches
+   */
+  async startQuery(
+    query: RegExp,
+    searchTarget: NotebookPanel
+  ): Promise<ISearchMatch[]> {
+    this._searchTarget = searchTarget;
+    const cells = this._searchTarget.content.widgets;
+
+    this._query = query;
+    // Listen for cell model change to redo the search in case of
+    // new/pasted/deleted cells
+    const cellList = this._searchTarget.model.cells;
+    cellList.changed.connect(
+      this._restartQuery.bind(this),
+      this
+    );
+
+    let indexTotal = 0;
+    const allMatches: ISearchMatch[] = [];
+    // For each cell, create a search provider and collect the matches
+
+    for (let cell of cells) {
+      const cmEditor = cell.editor as CodeMirrorEditor;
+      const cmSearchProvider = new CodeMirrorSearchProvider();
+      cmSearchProvider.isSubProvider = true;
+
+      // If a rendered MarkdownCell contains a match, unrender it so that
+      // CodeMirror can show the match(es).  If the MarkdownCell is not
+      // rendered, putting CodeMirror on the page, CodeMirror will not run
+      // the mode, which will prevent the search from occurring.
+      // Keep track so that the cell can be rerendered when the search is ended
+      // or if there are no matches
+      let cellShouldReRender = false;
+      if (cell instanceof MarkdownCell && cell.rendered) {
+        cell.rendered = false;
+        cellShouldReRender = true;
+      }
+
+      // Unhide hidden cells for the same reason as above
+      if (cell.inputHidden) {
+        cell.inputHidden = false;
+      }
+      // chain promises to ensure indexing is sequential
+      const matchesFromCell = await cmSearchProvider.startQuery(
+        query,
+        cmEditor
+      );
+      if (cell instanceof MarkdownCell) {
+        if (matchesFromCell.length !== 0) {
+          // un-render markdown cells with matches
+          this._unRenderedMarkdownCells.push(cell);
+        } else if (cellShouldReRender) {
+          cell.rendered = true;
+        }
+      }
+
+      // update the match indices to reflect the whole document index values
+      matchesFromCell.forEach(match => {
+        match.index = match.index + indexTotal;
+      });
+      indexTotal += matchesFromCell.length;
+
+      // search has been initialized, connect the changed signal
+      cmSearchProvider.changed.connect(
+        this._onCmSearchProviderChanged,
+        this
+      );
+
+      allMatches.concat(matchesFromCell);
+
+      this._cmSearchProviders.push({
+        cell: cell,
+        provider: cmSearchProvider
+      });
+    }
+
+    this._currentMatch = await this._stepNext();
+
+    return allMatches;
+  }
+
+  /**
+   * Clears state of a search provider to prepare for startQuery to be called
+   * in order to start a new query or refresh an existing one.
+   *
+   * @returns A promise that resolves when the search provider is ready to
+   * begin a new search.
+   */
+  async endQuery(): Promise<void> {
+    this._cmSearchProviders.forEach(({ provider }) => {
+      provider.endQuery();
+      provider.changed.disconnect(this._onCmSearchProviderChanged, this);
+    });
+    Signal.disconnectBetween(this._searchTarget.model.cells, this);
+
+    this._cmSearchProviders = [];
+    this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => {
+      // Guard against the case where markdown cells have been deleted
+      if (!cell.isDisposed) {
+        cell.rendered = true;
+      }
+    });
+    this._unRenderedMarkdownCells = [];
+  }
+
+  /**
+   * Resets UI state, removes all matches.
+   *
+   * @returns A promise that resolves when all state has been cleaned up.
+   */
+  async endSearch(): Promise<void> {
+    Signal.disconnectBetween(this._searchTarget.model.cells, this);
+
+    const index = this._searchTarget.content.activeCellIndex;
+    this._cmSearchProviders.forEach(({ provider }) => {
+      provider.endSearch();
+      provider.changed.disconnect(this._onCmSearchProviderChanged, this);
+    });
+
+    this._cmSearchProviders = [];
+    this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => {
+      cell.rendered = true;
+    });
+    this._unRenderedMarkdownCells = [];
+
+    this._searchTarget.content.activeCellIndex = index;
+    this._searchTarget.content.mode = 'edit';
+    this._searchTarget = null;
+    this._currentMatch = null;
+  }
+
+  /**
+   * Move the current match indicator to the next match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  async highlightNext(): Promise<ISearchMatch | undefined> {
+    this._currentMatch = await this._stepNext();
+    return this._currentMatch;
+  }
+
+  /**
+   * Move the current match indicator to the previous match.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  async highlightPrevious(): Promise<ISearchMatch | undefined> {
+    this._currentMatch = await this._stepNext(true);
+    return this._currentMatch;
+  }
+
+  /**
+   * Report whether or not this provider has the ability to search on the given object
+   */
+  static canSearchOn(domain: any): boolean {
+    // check to see if the CMSearchProvider can search on the
+    // first cell, false indicates another editor is present
+    return domain instanceof NotebookPanel;
+  }
+
+  /**
+   * The same list of matches provided by the startQuery promise resoluton
+   */
+  get matches(): ISearchMatch[] {
+    return [].concat(...this._getMatchesFromCells());
+  }
+
+  /**
+   * Signal indicating that something in the search has changed, so the UI should update
+   */
+  get changed(): ISignal<this, void> {
+    return this._changed;
+  }
+
+  /**
+   * The current index of the selected match.
+   */
+  get currentMatchIndex(): number {
+    if (!this._currentMatch) {
+      return 0;
+    }
+    return this._currentMatch.index;
+  }
+
+  private async _stepNext(
+    reverse = false,
+    steps = 0
+  ): Promise<ISearchMatch | undefined> {
+    const notebook = this._searchTarget.content;
+    const activeCell: Cell = notebook.activeCell;
+    const cellIndex = notebook.widgets.indexOf(activeCell);
+    const numCells = notebook.widgets.length;
+    const { provider } = this._cmSearchProviders[cellIndex];
+
+    // highlightNext/Previous will not be able to search rendered MarkdownCells or
+    // hidden code cells, but that is okay here because in startQuery, we unrendered
+    // all cells with matches and unhid all cells
+    const match = reverse
+      ? await provider.highlightPrevious()
+      : await provider.highlightNext();
+    // If there was no match in this cell, try the next cell
+    if (!match) {
+      // We have looped around the whole notebook and have searched the original
+      // cell once more and found no matches.  Do not proceed with incrementing the
+      // active cell index so that the active cell doesn't change
+      if (steps === numCells) {
+        return undefined;
+      }
+      notebook.activeCellIndex =
+        ((reverse ? cellIndex - 1 : cellIndex + 1) + numCells) % numCells;
+      const editor = notebook.activeCell.editor as CodeMirrorEditor;
+      // move the cursor of the next cell to the start/end of the cell so it can
+      // search the whole thing
+      const newPosCM = reverse
+        ? CodeMirror.Pos(editor.lastLine())
+        : CodeMirror.Pos(editor.firstLine(), 0);
+      const newPos = {
+        line: newPosCM.line,
+        column: newPosCM.ch
+      };
+      editor.setCursorPosition(newPos);
+      return this._stepNext(reverse, steps + 1);
+    }
+
+    return match;
+  }
+
+  private async _restartQuery() {
+    await this.endQuery();
+    await this.startQuery(this._query, this._searchTarget);
+    this._changed.emit(undefined);
+  }
+
+  private _getMatchesFromCells(): ISearchMatch[][] {
+    let indexTotal = 0;
+    const result: ISearchMatch[][] = [];
+    this._cmSearchProviders.forEach(({ provider }) => {
+      const cellMatches = provider.matches;
+      cellMatches.forEach(match => {
+        match.index = match.index + indexTotal;
+      });
+      indexTotal += cellMatches.length;
+      result.push(cellMatches);
+    });
+    return result;
+  }
+
+  private _onCmSearchProviderChanged() {
+    this._changed.emit(undefined);
+  }
+
+  private _searchTarget: NotebookPanel;
+  private _query: RegExp;
+  private _cmSearchProviders: ICellSearchPair[] = [];
+  private _currentMatch: ISearchMatch;
+  private _unRenderedMarkdownCells: MarkdownCell[] = [];
+  private _changed = new Signal<this, void>(this);
+}

+ 191 - 0
packages/documentsearch-extension/src/searchinstance.ts

@@ -0,0 +1,191 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { IDisplayState, ISearchProvider } from '.';
+import { createSearchOverlay } from './searchoverlay';
+
+import { MainAreaWidget } from '@jupyterlab/apputils';
+
+import { Widget } from '@phosphor/widgets';
+import { ISignal, Signal } from '@phosphor/signaling';
+import { IDisposable } from '@phosphor/disposable';
+
+/**
+ * Represents a search on a single widget.
+ */
+export class SearchInstance implements IDisposable {
+  constructor(widget: Widget, searchProvider: ISearchProvider) {
+    this._widget = widget;
+    this._activeProvider = searchProvider;
+
+    this._searchWidget = createSearchOverlay({
+      widgetChanged: this._displayUpdateSignal,
+      overlayState: this._displayState,
+      onCaseSensitiveToggled: this._onCaseSensitiveToggled.bind(this),
+      onRegexToggled: this._onRegexToggled.bind(this),
+      onHightlightNext: this._highlightNext.bind(this),
+      onHighlightPrevious: this._highlightPrevious.bind(this),
+      onStartQuery: this._startQuery.bind(this),
+      onEndSearch: this.dispose.bind(this)
+    });
+
+    this._widget.disposed.connect(() => {
+      this.dispose();
+    });
+    this._searchWidget.disposed.connect(() => {
+      this.dispose();
+    });
+
+    // TODO: this does not update if the toolbar changes height.
+    if (this._widget instanceof MainAreaWidget) {
+      // Offset the position of the search widget to not cover the toolbar.
+      this._searchWidget.node.style.top = `${
+        this._widget.toolbar.node.clientHeight
+      }px`;
+    }
+    this._displaySearchWidget();
+  }
+
+  /**
+   * The search widget.
+   */
+  get searchWidget() {
+    return this._searchWidget;
+  }
+
+  /**
+   * The search provider.
+   */
+  get provider() {
+    return this._activeProvider;
+  }
+
+  /**
+   * Focus the search widget input.
+   */
+  focusInput(): void {
+    this._displayState.forceFocus = true;
+
+    // Trigger a rerender without resetting the forceFocus.
+    this._displayUpdateSignal.emit(this._displayState);
+  }
+
+  /**
+   * Updates the match index and total display in the search widget.
+   */
+  updateIndices(): void {
+    this._displayState.totalMatches = this._activeProvider.matches.length;
+    this._displayState.currentIndex = this._activeProvider.currentMatchIndex;
+    this._updateDisplay();
+  }
+
+  private _updateDisplay() {
+    // Reset the focus attribute to make sure we don't steal focus.
+    this._displayState.forceFocus = false;
+
+    // Trigger a rerender
+    this._displayUpdateSignal.emit(this._displayState);
+  }
+
+  private async _startQuery(query: RegExp) {
+    // save the last query (or set it to the current query if this is the first)
+    if (this._activeProvider && this._displayState.query) {
+      await this._activeProvider.endQuery();
+    }
+    this._displayState.query = query;
+    await this._activeProvider.startQuery(query, this._widget);
+    this.updateIndices();
+
+    // this signal should get injected when the widget is
+    // created and hooked up to react!
+    this._activeProvider.changed.connect(
+      this.updateIndices,
+      this
+    );
+  }
+
+  /**
+   * Dispose of the resources held by the search instance.
+   */
+  dispose() {
+    if (this.isDisposed) {
+      return;
+    }
+    this._isDisposed = true;
+
+    // If a query hasn't been executed yet, no need to call endSearch
+    if (this._displayState.query) {
+      this._activeProvider.endSearch();
+    }
+
+    this._searchWidget.dispose();
+    this._disposed.emit(undefined);
+    Signal.clearData(this);
+  }
+
+  /**
+   * Test if the object has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * A signal emitted when the object is disposed.
+   */
+  get disposed(): ISignal<this, void> {
+    return this._disposed;
+  }
+
+  /**
+   * Display search widget.
+   */
+  _displaySearchWidget() {
+    if (!this._searchWidget.isAttached) {
+      Widget.attach(this._searchWidget, this._widget.node);
+    }
+  }
+
+  private async _highlightNext() {
+    if (!this._displayState.query) {
+      return;
+    }
+    await this._activeProvider.highlightNext();
+    this.updateIndices();
+  }
+
+  private async _highlightPrevious() {
+    if (!this._displayState.query) {
+      return;
+    }
+    await this._activeProvider.highlightPrevious();
+    this.updateIndices();
+  }
+
+  private _onCaseSensitiveToggled() {
+    this._displayState.caseSensitive = !this._displayState.caseSensitive;
+    this._updateDisplay();
+  }
+
+  private _onRegexToggled() {
+    this._displayState.useRegex = !this._displayState.useRegex;
+    this._updateDisplay();
+  }
+
+  private _widget: Widget;
+  private _displayState: IDisplayState = {
+    currentIndex: 0,
+    totalMatches: 0,
+    caseSensitive: false,
+    useRegex: false,
+    inputText: '',
+    query: null,
+    errorMessage: '',
+    forceFocus: true
+  };
+  private _displayUpdateSignal = new Signal<this, IDisplayState>(this);
+  private _activeProvider: ISearchProvider;
+  private _searchWidget: Widget;
+  private _isDisposed = false;
+  private _disposed = new Signal<this, void>(this);
+}

+ 309 - 0
packages/documentsearch-extension/src/searchoverlay.tsx

@@ -0,0 +1,309 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+import * as React from 'react';
+
+import '../style/index.css';
+import { ReactWidget, UseSignal } from '@jupyterlab/apputils';
+import { Signal } from '@phosphor/signaling';
+import { Widget } from '@phosphor/widgets';
+import { IDisplayState } from '.';
+import { SearchInstance } from './searchinstance';
+
+const OVERLAY_CLASS = 'jp-DocumentSearch-overlay';
+const INPUT_CLASS = 'jp-DocumentSearch-input';
+const INPUT_WRAPPER_CLASS = 'jp-DocumentSearch-input-wrapper';
+const REGEX_BUTTON_CLASS_OFF =
+  'jp-DocumentSearch-input-button-off jp-DocumentSearch-regex-button';
+const REGEX_BUTTON_CLASS_ON =
+  'jp-DocumentSearch-input-button-on jp-DocumentSearch-regex-button';
+const CASE_BUTTON_CLASS_OFF =
+  'jp-DocumentSearch-input-button-off jp-DocumentSearch-case-button';
+const CASE_BUTTON_CLASS_ON =
+  'jp-DocumentSearch-input-button-on jp-DocumentSearch-case-button';
+const INDEX_COUNTER_CLASS = 'jp-DocumentSearch-index-counter';
+const UP_DOWN_BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-up-down-wrapper';
+const UP_BUTTON_CLASS = 'jp-DocumentSearch-up-button';
+const DOWN_BUTTON_CLASS = 'jp-DocumentSearch-down-button';
+const CLOSE_BUTTON_CLASS = 'jp-DocumentSearch-close-button';
+const REGEX_ERROR_CLASS = 'jp-DocumentSearch-regex-error';
+
+interface ISearchEntryProps {
+  onCaseSensitiveToggled: Function;
+  onRegexToggled: Function;
+  onKeydown: Function;
+  onChange: Function;
+  caseSensitive: boolean;
+  useRegex: boolean;
+  inputText: string;
+  forceFocus: boolean;
+}
+
+class SearchEntry extends React.Component<ISearchEntryProps> {
+  constructor(props: ISearchEntryProps) {
+    super(props);
+  }
+
+  /**
+   * Focus the input.
+   */
+  focusInput() {
+    (this.refs.searchInputNode as HTMLInputElement).focus();
+  }
+
+  componentDidUpdate() {
+    if (this.props.forceFocus) {
+      this.focusInput();
+    }
+  }
+
+  render() {
+    const caseButtonToggleClass = this.props.caseSensitive
+      ? CASE_BUTTON_CLASS_ON
+      : CASE_BUTTON_CLASS_OFF;
+    const regexButtonToggleClass = this.props.useRegex
+      ? REGEX_BUTTON_CLASS_ON
+      : REGEX_BUTTON_CLASS_OFF;
+
+    return (
+      <div className={INPUT_WRAPPER_CLASS}>
+        <input
+          placeholder={this.props.inputText ? null : 'SEARCH'}
+          className={INPUT_CLASS}
+          value={this.props.inputText}
+          onChange={e => this.props.onChange(e)}
+          onKeyDown={e => this.props.onKeydown(e)}
+          ref="searchInputNode"
+        />
+        <button
+          className={caseButtonToggleClass}
+          onClick={() => this.props.onCaseSensitiveToggled()}
+        />
+        <button
+          className={regexButtonToggleClass}
+          onClick={() => this.props.onRegexToggled()}
+        />
+      </div>
+    );
+  }
+}
+
+interface IUpDownProps {
+  onHighlightPrevious: Function;
+  onHightlightNext: Function;
+}
+
+function UpDownButtons(props: IUpDownProps) {
+  return (
+    <div className={UP_DOWN_BUTTON_WRAPPER_CLASS}>
+      <button
+        className={UP_BUTTON_CLASS}
+        onClick={() => props.onHighlightPrevious()}
+      />
+      <button
+        className={DOWN_BUTTON_CLASS}
+        onClick={() => props.onHightlightNext()}
+      />
+    </div>
+  );
+}
+
+interface ISearchIndexProps {
+  currentIndex: number;
+  totalMatches: number;
+}
+
+function SearchIndices(props: ISearchIndexProps) {
+  return (
+    <div className={INDEX_COUNTER_CLASS}>
+      {props.totalMatches === 0
+        ? '-/-'
+        : `${props.currentIndex + 1}/${props.totalMatches}`}
+    </div>
+  );
+}
+
+interface ISearchOverlayProps {
+  overlayState: IDisplayState;
+  onCaseSensitiveToggled: Function;
+  onRegexToggled: Function;
+  onHightlightNext: Function;
+  onHighlightPrevious: Function;
+  onStartQuery: Function;
+  onEndSearch: Function;
+}
+
+class SearchOverlay extends React.Component<
+  ISearchOverlayProps,
+  IDisplayState
+> {
+  constructor(props: ISearchOverlayProps) {
+    super(props);
+    this.state = props.overlayState;
+  }
+
+  private _onChange(event: React.ChangeEvent) {
+    this.setState({ inputText: (event.target as HTMLInputElement).value });
+  }
+
+  private _onKeydown(event: KeyboardEvent) {
+    if (event.keyCode === 13) {
+      event.preventDefault();
+      event.stopPropagation();
+      this._executeSearch(!event.shiftKey);
+    }
+    if (event.keyCode === 27) {
+      event.preventDefault();
+      event.stopPropagation();
+      this.props.onEndSearch();
+    }
+  }
+
+  private _executeSearch(goForward: boolean) {
+    // execute search!
+    let query;
+    try {
+      query = Private.parseQuery(
+        this.state.inputText,
+        this.props.overlayState.caseSensitive,
+        this.props.overlayState.useRegex
+      );
+      this.setState({ errorMessage: '' });
+    } catch (e) {
+      this.setState({ errorMessage: e.message });
+      return;
+    }
+
+    if (Private.regexEqual(this.props.overlayState.query, query)) {
+      if (goForward) {
+        this.props.onHightlightNext();
+      } else {
+        this.props.onHighlightPrevious();
+      }
+      return;
+    }
+
+    this.props.onStartQuery(query);
+  }
+
+  private onClose() {
+    // clean up and close widget
+    this.props.onEndSearch();
+  }
+
+  render() {
+    return [
+      <SearchEntry
+        useRegex={this.props.overlayState.useRegex}
+        caseSensitive={this.props.overlayState.caseSensitive}
+        onCaseSensitiveToggled={() => this.props.onCaseSensitiveToggled()}
+        onRegexToggled={() => this.props.onRegexToggled()}
+        onKeydown={(e: KeyboardEvent) => this._onKeydown(e)}
+        onChange={(e: React.ChangeEvent) => this._onChange(e)}
+        inputText={this.state.inputText}
+        forceFocus={this.props.overlayState.forceFocus}
+        key={0}
+      />,
+      <SearchIndices
+        currentIndex={this.props.overlayState.currentIndex}
+        totalMatches={this.props.overlayState.totalMatches}
+        key={1}
+      />,
+      <UpDownButtons
+        onHighlightPrevious={() => this._executeSearch(false)}
+        onHightlightNext={() => this._executeSearch(true)}
+        key={2}
+      />,
+      <div
+        className={CLOSE_BUTTON_CLASS}
+        onClick={() => this.onClose()}
+        key={3}
+      />,
+      <div
+        className={REGEX_ERROR_CLASS}
+        hidden={this.state.errorMessage && this.state.errorMessage.length === 0}
+        key={4}
+      >
+        {this.state.errorMessage}
+      </div>
+    ];
+  }
+}
+
+export function createSearchOverlay(
+  options: createSearchOverlay.IOptions
+): Widget {
+  const {
+    widgetChanged,
+    overlayState,
+    onCaseSensitiveToggled,
+    onRegexToggled,
+    onHightlightNext,
+    onHighlightPrevious,
+    onStartQuery,
+    onEndSearch
+  } = options;
+  const widget = ReactWidget.create(
+    <UseSignal signal={widgetChanged} initialArgs={overlayState}>
+      {(_, args) => {
+        return (
+          <SearchOverlay
+            onCaseSensitiveToggled={onCaseSensitiveToggled}
+            onRegexToggled={onRegexToggled}
+            onHightlightNext={onHightlightNext}
+            onHighlightPrevious={onHighlightPrevious}
+            onStartQuery={onStartQuery}
+            onEndSearch={onEndSearch}
+            overlayState={args}
+          />
+        );
+      }}
+    </UseSignal>
+  );
+  widget.addClass(OVERLAY_CLASS);
+  return widget;
+}
+
+namespace createSearchOverlay {
+  export interface IOptions {
+    widgetChanged: Signal<SearchInstance, IDisplayState>;
+    overlayState: IDisplayState;
+    onCaseSensitiveToggled: Function;
+    onRegexToggled: Function;
+    onHightlightNext: Function;
+    onHighlightPrevious: Function;
+    onStartQuery: Function;
+    onEndSearch: Function;
+  }
+}
+
+namespace Private {
+  export function parseQuery(
+    queryString: string,
+    caseSensitive: boolean,
+    regex: boolean
+  ) {
+    const flag = caseSensitive ? 'g' : 'gi';
+    // escape regex characters in query if its a string search
+    const queryText = regex
+      ? queryString
+      : queryString.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
+    let ret;
+    ret = new RegExp(queryText, flag);
+    if (ret.test('')) {
+      ret = /x^/;
+    }
+    return ret;
+  }
+
+  export function regexEqual(a: RegExp, b: RegExp) {
+    if (!a || !b) {
+      return false;
+    }
+    return (
+      a.source === b.source &&
+      a.global === b.global &&
+      a.ignoreCase === b.ignoreCase &&
+      a.multiline === b.multiline
+    );
+  }
+}

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

@@ -0,0 +1,89 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+import { ISearchProvider, ISearchProviderConstructor } 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._registerDefaultProviders(
+      DEFAULT_NOTEBOOK_SEARCH_PROVIDER,
+      NotebookSearchProvider
+    );
+    this._registerDefaultProviders(
+      DEFAULT_CODEMIRROR_SEARCH_PROVIDER,
+      CodeMirrorSearchProvider
+    );
+  }
+
+  /**
+   * Add a provider to the registry.
+   *
+   * @param key - The provider key.
+   */
+  registerProvider(key: string, provider: ISearchProviderConstructor): void {
+    this._customProviders.set(key, provider);
+  }
+
+  /**
+   * Remove provider from registry.
+   *
+   * @param key - The provider key.
+   * @returns true if removed, false if key did not exist in map.
+   */
+  deregisterProvider(key: string): boolean {
+    return this._customProviders.delete(key);
+  }
+
+  /**
+   * Returns a matching provider for the widget.
+   *
+   * @param widget - The widget to search over.
+   * @returns the search provider, or undefined if none exists.
+   */
+  getProviderForWidget(widget: any): ISearchProvider | undefined {
+    return (
+      this._findMatchingProvider(this._customProviders, widget) ||
+      this._findMatchingProvider(this._defaultProviders, widget)
+    );
+  }
+
+  private _registerDefaultProviders(
+    key: string,
+    provider: ISearchProviderConstructor
+  ): void {
+    this._defaultProviders.set(key, provider);
+  }
+
+  private _findMatchingProvider(
+    providerMap: Private.ProviderMap,
+    widget: any
+  ): ISearchProvider | undefined {
+    // iterate through all providers and ask each one if it can search on the
+    // widget.
+    for (let P of providerMap.values()) {
+      if (P.canSearchOn(widget)) {
+        return new P();
+      }
+    }
+    return undefined;
+  }
+
+  private _defaultProviders: Private.ProviderMap = new Map<
+    string,
+    ISearchProviderConstructor
+  >();
+  private _customProviders: Private.ProviderMap = new Map<
+    string,
+    ISearchProviderConstructor
+  >();
+}
+
+namespace Private {
+  export type ProviderMap = Map<string, ISearchProviderConstructor>;
+}

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

@@ -0,0 +1,4 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+export * from './providers/notebooksearchprovider';
+export * from './providers/codemirrorsearchprovider';

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

@@ -0,0 +1,126 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+.jp-DocumentSearch-input {
+  border: none;
+  outline: none;
+  font-size: var(--jp-ui-font-size1);
+}
+
+.jp-DocumentSearch-overlay {
+  position: absolute;
+  background-color: var(--jp-layout-color0);
+  border-bottom: var(--jp-border-width) solid var(--jp-border-color0);
+  border-left: var(--jp-border-width) solid var(--jp-border-color0);
+  border-bottom-right-radius: var(--jp-border-radius);
+  border-bottom-left-radius: var(--jp-border-radius);
+  top: 0;
+  right: 0;
+  z-index: 5;
+  min-width: 300px;
+  padding: 2px;
+  font-size: var(--jp-ui-font-size1);
+  display: flex;
+  align-items: center;
+}
+
+.jp-DocumentSearch-overlay * {
+  color: var(--jp-ui-font-color0);
+}
+
+.jp-DocumentSearch-input-wrapper {
+  border: var(--jp-border-width) solid var(--jp-border-color0);
+  border-radius: var(--jp-border-radius);
+  display: flex;
+  background-color: var(--jp-layout-color0);
+}
+
+.jp-DocumentSearch-input-wrapper * {
+  background-color: var(--jp-layout-color0);
+}
+
+.jp-DocumentSearch-input-wrapper button {
+  outline: 0;
+  border: none;
+  width: 20px;
+  height: 20px;
+  box-sizing: border-box;
+  background-color: var(--jp-layout-color0);
+  background-repeat: no-repeat;
+}
+
+.jp-DocumentSearch-regex-button {
+  background-image: var(--jp-icon-search-regex);
+}
+
+.jp-DocumentSearch-case-button {
+  background-image: var(--jp-icon-search-case-sensitive);
+}
+
+.jp-DocumentSearch-input-button:before {
+  display: block;
+  padding-top: 100%;
+}
+
+.jp-DocumentSearch-input-button-off {
+  opacity: var(--jp-search-toggle-off-opacity);
+}
+
+.jp-DocumentSearch-input-button-off:hover {
+  opacity: var(--jp-search-toggle-hover-opacity);
+}
+
+.jp-DocumentSearch-input-button-on {
+  opacity: var(--jp-search-toggle-on-opacity);
+}
+
+.jp-DocumentSearch-index-counter {
+  padding-left: 10px;
+  padding-right: 10px;
+  user-select: none;
+  min-width: 50px;
+  display: inline-block;
+}
+
+.jp-DocumentSearch-up-down-wrapper {
+  display: inline-block;
+}
+
+.jp-DocumentSearch-up-down-wrapper button {
+  outline: 0;
+  border: none;
+  width: 20px;
+  height: 20px;
+  padding-bottom: 5px;
+  background-color: var(--jp-layout-color0);
+  background-repeat: no-repeat;
+}
+
+.jp-DocumentSearch-up-button {
+  background-image: var(--jp-icon-search-arrow-up);
+}
+
+.jp-DocumentSearch-down-button {
+  background-image: var(--jp-icon-search-arrow-down);
+}
+
+.jp-DocumentSearch-close-button {
+  display: inline-block;
+  margin-left: 4px;
+  background-size: 16px;
+  height: 16px;
+  width: 16px;
+  background-image: var(--jp-icon-close);
+  background-position: center;
+  background-repeat: no-repeat;
+}
+
+.jp-DocumentSearch-close-button:hover {
+  background-size: 16px;
+  background-image: var(--jp-icon-close-circle);
+}
+
+.jp-DocumentSearch-regex-error {
+  color: var(--jp-error-color0);
+}

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

@@ -0,0 +1,34 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/**/*"],
+  "references": [
+    {
+      "path": "../application"
+    },
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../cells"
+    },
+    {
+      "path": "../codeeditor"
+    },
+    {
+      "path": "../codemirror"
+    },
+    {
+      "path": "../docregistry"
+    },
+    {
+      "path": "../mainmenu"
+    },
+    {
+      "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.19.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"
     },

+ 11 - 0
packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 22 20" style="enable-background:new 0 0 22 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<rect class="st0" width="22" height="20"/>
+<polygon class="st1" points="11,13.7 4.6,7.4 5.4,6.6 11,12.3 16.6,6.6 17.4,7.4 "/>
+</svg>

+ 11 - 0
packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Up" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 22 20" style="enable-background:new 0 0 22 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<rect class="st0" width="22" height="20"/>
+<polygon class="st1" points="16.6,13.4 11,7.7 5.4,13.4 4.6,12.6 11,6.3 17.4,12.6 "/>
+</svg>

+ 19 - 0
packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<rect class="st0" width="20" height="20"/>
+<rect x="2" y="2" class="st1" width="16" height="16"/>
+<path d="M7.6,8h0.9l3.5,8h-1.1L10,14H6l-0.9,2H4L7.6,8z M8,9.1L6.4,13h3.2L8,9.1z"/>
+<path d="M16.6,9.8c-0.2,0.1-0.4,0.1-0.7,0.1c-0.2,0-0.4-0.1-0.6-0.2c-0.1-0.1-0.2-0.4-0.2-0.7c-0.3,0.3-0.6,0.5-0.9,0.7
+	c-0.3,0.1-0.7,0.2-1.1,0.2c-0.3,0-0.5,0-0.7-0.1c-0.2-0.1-0.4-0.2-0.6-0.3c-0.2-0.1-0.3-0.3-0.4-0.5c-0.1-0.2-0.1-0.4-0.1-0.7
+	c0-0.3,0.1-0.6,0.2-0.8c0.1-0.2,0.3-0.4,0.4-0.5C12,7,12.2,6.9,12.4,6.8s0.5-0.1,0.7-0.2c0.3-0.1,0.5-0.1,0.7-0.1
+	c0.2,0,0.4-0.1,0.6-0.1c0.2,0,0.3-0.1,0.4-0.2C15,6.1,15.1,6,15.1,5.8c0-1-1.1-1-1.3-1c-0.4,0-1.4,0-1.4,1.2h-0.9
+	c0-0.4,0.1-0.7,0.2-1c0.1-0.2,0.3-0.4,0.5-0.6c0.2-0.2,0.5-0.3,0.8-0.3C13.2,4,13.5,4,13.8,4c0.3,0,0.5,0,0.8,0.1
+	c0.3,0,0.5,0.1,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5C15.9,5,16,5.2,16,5.6v2.9c0,0.2,0,0.4,0,0.5c0,0.1,0.1,0.2,0.3,0.2
+	c0.1,0,0.2,0,0.3,0V9.8z M15.1,6.9C13.9,7.5,12,7.1,12,8.3c0,1.4,3.1,1,3.1-0.5V6.9z"/>
+</svg>

+ 19 - 0
packages/theme-dark-extension/style/icons/jupyter/search_regex.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<g id="Regex_Icon">
+	<rect class="st0" width="20" height="20"/>
+	<rect x="2" y="2" class="st1" width="16" height="16"/>
+	<circle cx="5.5" cy="14.5" r="1.5"/>
+	<g>
+		<rect x="12" y="4" width="1" height="8"/>
+		<rect x="12" y="4" transform="matrix(-0.5 -0.866 0.866 -0.5 11.8218 22.8253)" width="1" height="8"/>
+		<rect x="12" y="4" transform="matrix(-0.5 0.866 -0.866 -0.5 25.6782 1.1747)" width="1" height="8"/>
+	</g>
+</g>
+</svg>

+ 4 - 0
packages/theme-dark-extension/style/urls.css

@@ -91,6 +91,10 @@
   --jp-icon-save: url('icons/md/save.svg');
   --jp-icon-search-white: url('icons/md/search.svg');
   --jp-icon-search: url('icons/md/search.svg');
+  --jp-icon-search-arrow-up: url('icons/jupyter/search_arrow_up.svg');
+  --jp-icon-search-arrow-down: url('icons/jupyter/search_arrow_down.svg');
+  --jp-icon-search-case-sensitive: url('icons/jupyter/search_case_sensitive.svg');
+  --jp-icon-search-regex: url('icons/jupyter/search_regex.svg');
   --jp-icon-settings: url('icons/jupyter/settings.svg');
   --jp-icon-spreadsheet-selected: url('icons/jupyter/csv_selected.svg');
   --jp-icon-spreadsheet: url('icons/jupyter/csv.svg');

+ 6 - 0
packages/theme-dark-extension/style/variables.css

@@ -368,4 +368,10 @@ all of MD as it is not optimized for dense, information rich UIs.
   /* Sidebar-related styles */
 
   --jp-sidebar-min-width: 180px;
+
+  /* Search-related styles */
+
+  --jp-search-toggle-off-opacity: 0.5;
+  --jp-search-toggle-hover-opacity: 0.75;
+  --jp-search-toggle-on-opacity: 1;
 }

+ 11 - 0
packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 22 20" style="enable-background:new 0 0 22 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<rect class="st0" width="22" height="20"/>
+<polygon class="st1" points="11,13.7 4.6,7.4 5.4,6.6 11,12.3 16.6,6.6 17.4,7.4 "/>
+</svg>

+ 11 - 0
packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Up" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 22 20" style="enable-background:new 0 0 22 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<rect class="st0" width="22" height="20"/>
+<polygon class="st1" points="16.6,13.4 11,7.7 5.4,13.4 4.6,12.6 11,6.3 17.4,12.6 "/>
+</svg>

+ 19 - 0
packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<rect class="st0" width="20" height="20"/>
+<rect x="2" y="2" class="st1" width="16" height="16"/>
+<path d="M7.6,8h0.9l3.5,8h-1.1L10,14H6l-0.9,2H4L7.6,8z M8,9.1L6.4,13h3.2L8,9.1z"/>
+<path d="M16.6,9.8c-0.2,0.1-0.4,0.1-0.7,0.1c-0.2,0-0.4-0.1-0.6-0.2c-0.1-0.1-0.2-0.4-0.2-0.7c-0.3,0.3-0.6,0.5-0.9,0.7
+	c-0.3,0.1-0.7,0.2-1.1,0.2c-0.3,0-0.5,0-0.7-0.1c-0.2-0.1-0.4-0.2-0.6-0.3c-0.2-0.1-0.3-0.3-0.4-0.5c-0.1-0.2-0.1-0.4-0.1-0.7
+	c0-0.3,0.1-0.6,0.2-0.8c0.1-0.2,0.3-0.4,0.4-0.5C12,7,12.2,6.9,12.4,6.8s0.5-0.1,0.7-0.2c0.3-0.1,0.5-0.1,0.7-0.1
+	c0.2,0,0.4-0.1,0.6-0.1c0.2,0,0.3-0.1,0.4-0.2C15,6.1,15.1,6,15.1,5.8c0-1-1.1-1-1.3-1c-0.4,0-1.4,0-1.4,1.2h-0.9
+	c0-0.4,0.1-0.7,0.2-1c0.1-0.2,0.3-0.4,0.5-0.6c0.2-0.2,0.5-0.3,0.8-0.3C13.2,4,13.5,4,13.8,4c0.3,0,0.5,0,0.8,0.1
+	c0.3,0,0.5,0.1,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5C15.9,5,16,5.2,16,5.6v2.9c0,0.2,0,0.4,0,0.5c0,0.1,0.1,0.2,0.3,0.2
+	c0.1,0,0.2,0,0.3,0V9.8z M15.1,6.9C13.9,7.5,12,7.1,12,8.3c0,1.4,3.1,1,3.1-0.5V6.9z"/>
+</svg>

+ 19 - 0
packages/theme-light-extension/style/icons/jupyter/search_regex.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#A5A5A5;}
+</style>
+<g id="Regex_Icon">
+	<rect class="st0" width="20" height="20"/>
+	<rect x="2" y="2" class="st1" width="16" height="16"/>
+	<circle cx="5.5" cy="14.5" r="1.5"/>
+	<g>
+		<rect x="12" y="4" width="1" height="8"/>
+		<rect x="12" y="4" transform="matrix(-0.5 -0.866 0.866 -0.5 11.8218 22.8253)" width="1" height="8"/>
+		<rect x="12" y="4" transform="matrix(-0.5 0.866 -0.866 -0.5 25.6782 1.1747)" width="1" height="8"/>
+	</g>
+</g>
+</svg>

+ 4 - 0
packages/theme-light-extension/style/urls.css

@@ -90,6 +90,10 @@
   --jp-icon-save: url('icons/md/save.svg');
   --jp-icon-search-white: url('icons/md/search-white.svg');
   --jp-icon-search: url('icons/md/search.svg');
+  --jp-icon-search-arrow-up: url('icons/jupyter/search_arrow_up.svg');
+  --jp-icon-search-arrow-down: url('icons/jupyter/search_arrow_down.svg');
+  --jp-icon-search-case-sensitive: url('icons/jupyter/search_case_sensitive.svg');
+  --jp-icon-search-regex: url('icons/jupyter/search_regex.svg');
   --jp-icon-settings: url('icons/jupyter/settings.svg');
   --jp-icon-spreadsheet-selected: url('icons/jupyter/csv_selected.svg');
   --jp-icon-spreadsheet: url('icons/jupyter/csv.svg');

+ 6 - 0
packages/theme-light-extension/style/variables.css

@@ -365,4 +365,10 @@ all of MD as it is not optimized for dense, information rich UIs.
   /* Sidebar-related styles */
 
   --jp-sidebar-min-width: 180px;
+
+  /* Search-related styles */
+
+  --jp-search-toggle-off-opacity: 0.4;
+  --jp-search-toggle-hover-opacity: 0.65;
+  --jp-search-toggle-on-opacity: 1;
 }

+ 3 - 3
yarn.lock

@@ -692,9 +692,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"

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor