浏览代码

Merge pull request #6159 from aschlaep/search-and-replace

Add Find and Replace for notebooks and text files
Jason Grout 6 年之前
父节点
当前提交
f99c216879

+ 15 - 12
packages/codemirror/src/editor.ts

@@ -751,7 +751,19 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
       // Only render selections if the start is not equal to the end.
       // In that case, we don't need to render the cursor.
       if (!JSONExt.deepEqual(selection.start, selection.end)) {
-        const { anchor, head } = this._toCodeMirrorSelection(selection);
+        // Selections only appear to render correctly if the anchor
+        // is before the head in the document. That is, reverse selections
+        // do not appear as intended.
+        let forward: boolean =
+          selection.start.line < selection.end.line ||
+          (selection.start.line === selection.end.line &&
+            selection.start.column <= selection.end.column);
+        let anchor = this._toCodeMirrorPosition(
+          forward ? selection.start : selection.end
+        );
+        let head = this._toCodeMirrorPosition(
+          forward ? selection.end : selection.start
+        );
         let markerOptions: CodeMirror.TextMarkerOptions;
         if (collaborator) {
           markerOptions = this._toTextMarkerOptions({
@@ -823,18 +835,9 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
   private _toCodeMirrorSelection(
     selection: CodeEditor.IRange
   ): CodeMirror.Selection {
-    // Selections only appear to render correctly if the anchor
-    // is before the head in the document. That is, reverse selections
-    // do not appear as intended.
-    let forward: boolean =
-      selection.start.line < selection.end.line ||
-      (selection.start.line === selection.end.line &&
-        selection.start.column <= selection.end.column);
-    let anchor = forward ? selection.start : selection.end;
-    let head = forward ? selection.end : selection.start;
     return {
-      anchor: this._toCodeMirrorPosition(anchor),
-      head: this._toCodeMirrorPosition(head)
+      anchor: this._toCodeMirrorPosition(selection.start),
+      head: this._toCodeMirrorPosition(selection.end)
     };
   }
 

+ 28 - 1
packages/csvviewer-extension/src/searchprovider.ts

@@ -72,11 +72,31 @@ export class CSVSearchProvider implements ISearchProvider {
    *
    * @returns A promise that resolves once the action has completed.
    */
-  highlightPrevious(): Promise<ISearchMatch | undefined> {
+  async highlightPrevious(): Promise<ISearchMatch | undefined> {
     this._target.content.searchService.find(this._query, true);
     return undefined;
   }
 
+  /**
+   * Replace the currently selected match with the provided text
+   * Not implemented in the CSV viewer as it is read-only.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  async replaceCurrentMatch(newText: string): Promise<boolean> {
+    return false;
+  }
+
+  /**
+   * Replace all matches in the notebook with the provided text
+   * Not implemented in the CSV viewer as it is read-only.
+   *
+   * @returns A promise that resolves once the action has completed.
+   */
+  async replaceAllMatches(newText: string): Promise<boolean> {
+    return false;
+  }
+
   /**
    * Signal indicating that something in the search has changed, so the UI should update
    */
@@ -94,6 +114,13 @@ export class CSVSearchProvider implements ISearchProvider {
    */
   readonly currentMatchIndex: number | null = null;
 
+  /**
+   * Set to true if the widget under search is read-only, false
+   * if it is editable.  Will be used to determine whether to show
+   * the replace option.
+   */
+  readonly isReadOnly = true;
+
   private _target: IDocumentWidget<CSVViewer>;
   private _query: RegExp;
   private _changed = new Signal<this, void>(this);

+ 43 - 2
packages/documentsearch/src/interfaces.ts

@@ -26,9 +26,9 @@ export interface IDisplayState {
   useRegex: boolean;
 
   /**
-   * The text in the entry
+   * The text in the search entry
    */
-  inputText: string;
+  searchText: string;
 
   /**
    * The query constructed from the text and the case/regex flags
@@ -44,6 +44,26 @@ export interface IDisplayState {
    * Should the focus forced into the input on the next render?
    */
   forceFocus: boolean;
+
+  /**
+   * Whether or not the search input is currently focused
+   */
+  searchInputFocused: boolean;
+
+  /**
+   * Whether or not the replace input is currently focused
+   */
+  replaceInputFocused: boolean;
+
+  /**
+   * The text in the replace entry
+   */
+  replaceText: string;
+
+  /**
+   * Whether or not the replace entry row is visible
+   */
+  replaceEntryShown: boolean;
 }
 
 export interface ISearchMatch {
@@ -128,6 +148,20 @@ export interface ISearchProvider {
    */
   highlightPrevious(): Promise<ISearchMatch | undefined>;
 
+  /**
+   * Replace the currently selected match with the provided text
+   *
+   * @returns A promise that resolves with a boolean indicating whether a replace occurred.
+   */
+  replaceCurrentMatch(newText: string): Promise<boolean>;
+
+  /**
+   * Replace all matches in the notebook with the provided text
+   *
+   * @returns A promise that resolves with a boolean indicating whether a replace occurred.
+   */
+  replaceAllMatches(newText: string): Promise<boolean>;
+
   /**
    * The same list of matches provided by the startQuery promise resoluton
    */
@@ -142,4 +176,11 @@ export interface ISearchProvider {
    * The current index of the selected match.
    */
   readonly currentMatchIndex: number | null;
+
+  /**
+   * Set to true if the widget under search is read-only, false
+   * if it is editable.  Will be used to determine whether to show
+   * the replace option.
+   */
+  readonly isReadOnly: boolean;
 }

+ 112 - 8
packages/documentsearch/src/providers/codemirrorsearchprovider.ts

@@ -98,7 +98,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
       const match = this._matchState[cursorMatch.from.line][
         cursorMatch.from.ch
       ];
-      this._matchIndex = match.index;
+      this._currentMatch = match;
     }
     return matches;
   }
@@ -112,8 +112,18 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
    */
   async endQuery(): Promise<void> {
     this._matchState = {};
-    this._matchIndex = null;
+    this._currentMatch = null;
     this._cm.removeOverlay(this._overlay);
+    const from = this._cm.getCursor('from');
+    const to = this._cm.getCursor('to');
+    // Setting a reverse selection to allow search-as-you-type to maintain the
+    // current selected match.  See comment in _findNext for more details.
+    if (from !== to) {
+      this._cm.setSelection({
+        start: this._toEditorPos(to),
+        end: this._toEditorPos(from)
+      });
+    }
     CodeMirror.off(this._cm.doc, 'change', this._onDocChanged.bind(this));
   }
 
@@ -140,7 +150,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
       return;
     }
     const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
-    this._matchIndex = match.index;
+    this._currentMatch = match;
     return match;
   }
 
@@ -155,10 +165,60 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
       return;
     }
     const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
-    this._matchIndex = match.index;
+    this._currentMatch = match;
     return match;
   }
 
+  /**
+   * Replace the currently selected match with the provided text
+   *
+   * @returns A promise that resolves with a boolean indicating whether a replace occurred.
+   */
+  async replaceCurrentMatch(newText: string): Promise<boolean> {
+    // If the current selection exactly matches the current match,
+    // replace it.  Otherwise, just select the next match after the cursor.
+    let replaceOccurred = false;
+    if (this._currentMatchIsSelected()) {
+      const cursor = this._cm.getSearchCursor(
+        this._query,
+        this._cm.getCursor('from'),
+        !this._query.ignoreCase
+      );
+      if (!cursor.findNext()) {
+        return replaceOccurred;
+      }
+      replaceOccurred = true;
+      cursor.replace(newText);
+    }
+    await this.highlightNext();
+    return replaceOccurred;
+  }
+
+  /**
+   * Replace all matches in the notebook with the provided text
+   *
+   * @returns A promise that resolves with a boolean indicating whether a replace occurred.
+   */
+  async replaceAllMatches(newText: string): Promise<boolean> {
+    let replaceOccurred = false;
+    return new Promise((resolve, _) => {
+      this._cm.operation(() => {
+        const cursor = this._cm.getSearchCursor(
+          this._query,
+          null,
+          !this._query.ignoreCase
+        );
+        while (cursor.findNext()) {
+          replaceOccurred = true;
+          cursor.replace(newText);
+        }
+        this._matchState = {};
+        this._currentMatch = null;
+        resolve(replaceOccurred);
+      });
+    });
+  }
+
   /**
    * Report whether or not this provider has the ability to search on the given object
    */
@@ -177,6 +237,10 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
     return this._parseMatchesFromState();
   }
 
+  get currentMatch(): ISearchMatch | null {
+    return this._currentMatch;
+  }
+
   /**
    * Signal indicating that something in the search has changed, so the UI should update
    */
@@ -188,9 +252,19 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
    * The current index of the selected match.
    */
   get currentMatchIndex(): number {
-    return this._matchIndex;
+    if (!this._currentMatch) {
+      return null;
+    }
+    return this._currentMatch.index;
   }
 
+  /**
+   * Set to true if the widget under search is read-only, false
+   * if it is editable.  Will be used to determine whether to show
+   * the replace option.
+   */
+  readonly isReadOnly = false;
+
   clearSelection(): void {
     return null;
   }
@@ -328,7 +402,20 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
   private _findNext(reverse: boolean): Private.ICodeMirrorMatch {
     return this._cm.operation(() => {
       const caseSensitive = this._query.ignoreCase;
-      const cursorToGet = reverse ? 'from' : 'to';
+
+      // In order to support search-as-you-type, we needed a way to allow the first
+      // match to be selected when a search is started, but prevent the selected
+      // search to move for each new keypress.  To do this, when a search is ended,
+      // the cursor is reversed, putting the head at the 'from' position.  When a new
+      // search is started, the cursor we want is at the 'from' position, so that the same
+      // match is selected when the next key is entered (if it is still a match).
+      //
+      // When toggling through a search normally, the cursor is always set in the forward
+      // direction, so head is always at the 'to' position.  That way, if reverse = false,
+      // the search proceeds from the 'to' position during normal toggling.  If reverse = true,
+      // the search always proceeds from the 'anchor' position, which is at the 'from'.
+
+      const cursorToGet = reverse ? 'anchor' : 'head';
       const lastPosition = this._cm.getCursor(cursorToGet);
       const position = this._toEditorPos(lastPosition);
       let cursor: CodeMirror.SearchCursor = this._cm.getSearchCursor(
@@ -340,7 +427,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
         // 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;
+          this._currentMatch = null;
           return null;
         }
 
@@ -415,9 +502,26 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
     };
   }
 
+  private _currentMatchIsSelected(): boolean {
+    if (!this._currentMatch) {
+      return false;
+    }
+    const currentSelection = this._cm.getSelection();
+    const currentSelectionLength =
+      currentSelection.end.column - currentSelection.start.column;
+    const selectionIsOneLine =
+      currentSelection.start.line === currentSelection.end.line;
+    return (
+      this._currentMatch.line === currentSelection.start.line &&
+      this._currentMatch.column === currentSelection.start.column &&
+      this._currentMatch.text.length === currentSelectionLength &&
+      selectionIsOneLine
+    );
+  }
+
   private _query: RegExp;
   private _cm: CodeMirrorEditor;
-  private _matchIndex: number;
+  private _currentMatch: ISearchMatch;
   private _matchState: MatchMap = {};
   private _changed = new Signal<this, void>(this);
   private _overlay: any;

+ 68 - 2
packages/documentsearch/src/providers/notebooksearchprovider.ts

@@ -13,7 +13,7 @@ import CodeMirror from 'codemirror';
 
 interface ICellSearchPair {
   cell: Cell;
-  provider: ISearchProvider;
+  provider: CodeMirrorSearchProvider;
 }
 
 export class NotebookSearchProvider implements ISearchProvider {
@@ -180,6 +180,48 @@ export class NotebookSearchProvider implements ISearchProvider {
     return this._currentMatch;
   }
 
+  /**
+   * Replace the currently selected match with the provided text
+   *
+   * @returns A promise that resolves with a boolean indicating whether a replace occurred.
+   */
+  async replaceCurrentMatch(newText: string): Promise<boolean> {
+    const notebook = this._searchTarget.content;
+    const editor = notebook.activeCell.editor as CodeMirrorEditor;
+    let replaceOccurred = false;
+    if (this._currentMatchIsSelected(editor)) {
+      const cellIndex = notebook.widgets.indexOf(notebook.activeCell);
+      const { provider } = this._cmSearchProviders[cellIndex];
+      replaceOccurred = await provider.replaceCurrentMatch(newText);
+      if (replaceOccurred) {
+        this._currentMatch = provider.currentMatch;
+        // If there was a replacement and there is another match, then the CodeMirrorSearchProvider
+        // already highlighted the next match, so we can return early to avoid skipping a match.
+        if (this._currentMatch) {
+          return replaceOccurred;
+        }
+      }
+    }
+    await this.highlightNext();
+    return replaceOccurred;
+  }
+
+  /**
+   * Replace all matches in the notebook with the provided text
+   *
+   * @returns A promise that resolves with a boolean indicating whether a replace occurred.
+   */
+  async replaceAllMatches(newText: string): Promise<boolean> {
+    let replaceOccurred = false;
+    for (let index in this._cmSearchProviders) {
+      const { provider } = this._cmSearchProviders[index];
+      const singleReplaceOccurred = await provider.replaceAllMatches(newText);
+      replaceOccurred = singleReplaceOccurred ? true : replaceOccurred;
+    }
+    this._currentMatch = null;
+    return replaceOccurred;
+  }
+
   /**
    * Report whether or not this provider has the ability to search on the given object
    */
@@ -208,11 +250,18 @@ export class NotebookSearchProvider implements ISearchProvider {
    */
   get currentMatchIndex(): number {
     if (!this._currentMatch) {
-      return 0;
+      return null;
     }
     return this._currentMatch.index;
   }
 
+  /**
+   * Set to true if the widget under search is read-only, false
+   * if it is editable.  Will be used to determine whether to show
+   * the replace option.
+   */
+  readonly isReadOnly = false;
+
   private async _stepNext(
     reverse = false,
     steps = 0
@@ -280,6 +329,23 @@ export class NotebookSearchProvider implements ISearchProvider {
     this._changed.emit(undefined);
   }
 
+  private _currentMatchIsSelected(cm: CodeMirrorEditor): boolean {
+    if (!this._currentMatch) {
+      return false;
+    }
+    const currentSelection = cm.getSelection();
+    const currentSelectionLength =
+      currentSelection.end.column - currentSelection.start.column;
+    const selectionIsOneLine =
+      currentSelection.start.line === currentSelection.end.line;
+    return (
+      this._currentMatch.line === currentSelection.start.line &&
+      this._currentMatch.column === currentSelection.start.column &&
+      this._currentMatch.text.length === currentSelectionLength &&
+      selectionIsOneLine
+    );
+  }
+
   private _searchTarget: NotebookPanel;
   private _query: RegExp;
   private _cmSearchProviders: ICellSearchPair[] = [];

+ 24 - 3
packages/documentsearch/src/searchinstance.ts

@@ -25,7 +25,10 @@ export class SearchInstance implements IDisposable {
       onHightlightNext: this._highlightNext.bind(this),
       onHighlightPrevious: this._highlightPrevious.bind(this),
       onStartQuery: this._startQuery.bind(this),
-      onEndSearch: this.dispose.bind(this)
+      onReplaceCurrent: this._replaceCurrent.bind(this),
+      onReplaceAll: this._replaceAll.bind(this),
+      onEndSearch: this.dispose.bind(this),
+      isReadOnly: this._activeProvider.isReadOnly
     });
 
     this._widget.disposed.connect(() => {
@@ -103,6 +106,20 @@ export class SearchInstance implements IDisposable {
     );
   }
 
+  private async _replaceCurrent(newText: string) {
+    if (this._activeProvider && this._displayState.query && !!newText) {
+      await this._activeProvider.replaceCurrentMatch(newText);
+      this.updateIndices();
+    }
+  }
+
+  private async _replaceAll(newText: string) {
+    if (this._activeProvider && this._displayState.query && !!newText) {
+      await this._activeProvider.replaceAllMatches(newText);
+      this.updateIndices();
+    }
+  }
+
   /**
    * Dispose of the resources held by the search instance.
    */
@@ -177,10 +194,14 @@ export class SearchInstance implements IDisposable {
     totalMatches: 0,
     caseSensitive: false,
     useRegex: false,
-    inputText: '',
+    searchText: '',
     query: null,
     errorMessage: '',
-    forceFocus: true
+    searchInputFocused: true,
+    replaceInputFocused: false,
+    forceFocus: true,
+    replaceText: '',
+    replaceEntryShown: false
   };
   private _displayUpdateSignal = new Signal<this, IDisplayState>(this);
   private _activeProvider: ISearchProvider;

+ 225 - 58
packages/documentsearch/src/searchoverlay.tsx

@@ -9,6 +9,7 @@ import { Widget } from '@phosphor/widgets';
 import * as React from 'react';
 
 const OVERLAY_CLASS = 'jp-DocumentSearch-overlay';
+const OVERLAY_ROW_CLASS = 'jp-DocumentSearch-overlay-row';
 const INPUT_CLASS = 'jp-DocumentSearch-input';
 const INPUT_WRAPPER_CLASS = 'jp-DocumentSearch-input-wrapper';
 const REGEX_BUTTON_CLASS_OFF =
@@ -25,7 +26,15 @@ 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';
-
+const REPLACE_ENTRY_CLASS = 'jp-DocumentSearch-replace-entry';
+const REPLACE_BUTTON_CLASS = 'jp-DocumentSearch-replace-button';
+const REPLACE_BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-replace-button-wrapper';
+const REPLACE_WRAPPER_CLASS = 'jp-DocumentSearch-replace-wrapper-class';
+const REPLACE_TOGGLE_COLLAPSED = 'jp-DocumentSearch-replace-toggle-collapsed';
+const REPLACE_TOGGLE_EXPANDED = 'jp-DocumentSearch-replace-toggle-expanded';
+const FOCUSED_INPUT = 'jp-DocumentSearch-focused-input';
+const TOGGLE_WRAPPER = 'jp-DocumentSearch-toggle-wrapper';
+const TOGGLE_PLACEHOLDER = 'jp-DocumentSearch-toggle-placeholder';
 const BUTTON_CONTENT_CLASS = 'jp-DocumentSearch-button-content';
 const BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-button-wrapper';
 
@@ -34,12 +43,23 @@ interface ISearchEntryProps {
   onRegexToggled: Function;
   onKeydown: Function;
   onChange: Function;
+  onInputFocus: Function;
+  onInputBlur: Function;
+  inputFocused: boolean;
   caseSensitive: boolean;
   useRegex: boolean;
-  inputText: string;
+  searchText: string;
   forceFocus: boolean;
 }
 
+interface IReplaceEntryProps {
+  onReplaceCurrent: Function;
+  onReplaceAll: Function;
+  onReplaceKeydown: Function;
+  onChange: Function;
+  replaceText: string;
+}
+
 class SearchEntry extends React.Component<ISearchEntryProps> {
   constructor(props: ISearchEntryProps) {
     super(props);
@@ -53,7 +73,7 @@ class SearchEntry extends React.Component<ISearchEntryProps> {
   }
 
   componentDidUpdate() {
-    if (this.props.forceFocus) {
+    if (this.props.forceFocus && this.props.inputFocused) {
       this.focusInput();
     }
   }
@@ -66,21 +86,27 @@ class SearchEntry extends React.Component<ISearchEntryProps> {
       ? REGEX_BUTTON_CLASS_ON
       : REGEX_BUTTON_CLASS_OFF;
 
+    const wrapperClass = `${INPUT_WRAPPER_CLASS} ${
+      this.props.inputFocused ? FOCUSED_INPUT : ''
+    }`;
+
     return (
-      <div className={INPUT_WRAPPER_CLASS}>
+      <div className={wrapperClass}>
         <input
-          placeholder={this.props.inputText ? null : 'SEARCH'}
+          placeholder={this.props.searchText ? null : 'Find'}
           className={INPUT_CLASS}
-          value={this.props.inputText}
+          value={this.props.searchText}
           onChange={e => this.props.onChange(e)}
           onKeyDown={e => this.props.onKeydown(e)}
-          tabIndex={1}
+          tabIndex={2}
+          onFocus={e => this.props.onInputFocus()}
+          onBlur={e => this.props.onInputBlur()}
           ref="searchInputNode"
         />
         <button
           className={BUTTON_WRAPPER_CLASS}
           onClick={() => this.props.onCaseSensitiveToggled()}
-          tabIndex={2}
+          tabIndex={4}
         >
           <span
             className={`${caseButtonToggleClass} ${BUTTON_CONTENT_CLASS}`}
@@ -90,7 +116,7 @@ class SearchEntry extends React.Component<ISearchEntryProps> {
         <button
           className={BUTTON_WRAPPER_CLASS}
           onClick={() => this.props.onRegexToggled()}
-          tabIndex={3}
+          tabIndex={5}
         >
           <span
             className={`${regexButtonToggleClass} ${BUTTON_CONTENT_CLASS}`}
@@ -102,6 +128,52 @@ class SearchEntry extends React.Component<ISearchEntryProps> {
   }
 }
 
+class ReplaceEntry extends React.Component<IReplaceEntryProps> {
+  constructor(props: any) {
+    super(props);
+  }
+
+  render() {
+    return (
+      <div className={REPLACE_WRAPPER_CLASS}>
+        <input
+          placeholder={this.props.replaceText ? null : 'Replace'}
+          className={REPLACE_ENTRY_CLASS}
+          value={this.props.replaceText}
+          onKeyDown={e => this.props.onReplaceKeydown(e)}
+          onChange={e => this.props.onChange(e)}
+          tabIndex={3}
+          ref="replaceInputNode"
+        />
+        <button
+          className={REPLACE_BUTTON_WRAPPER_CLASS}
+          onClick={() => this.props.onReplaceCurrent()}
+          tabIndex={9}
+        >
+          <span
+            className={`${REPLACE_BUTTON_CLASS} ${BUTTON_CONTENT_CLASS}`}
+            tabIndex={-1}
+          >
+            Replace
+          </span>
+        </button>
+        <button
+          className={REPLACE_BUTTON_WRAPPER_CLASS}
+          tabIndex={10}
+          onClick={() => this.props.onReplaceAll()}
+        >
+          <span
+            className={`${REPLACE_BUTTON_CLASS} ${BUTTON_CONTENT_CLASS}`}
+            tabIndex={-1}
+          >
+            Replace All
+          </span>
+        </button>
+      </div>
+    );
+  }
+}
+
 interface IUpDownProps {
   onHighlightPrevious: Function;
   onHightlightNext: Function;
@@ -113,7 +185,7 @@ function UpDownButtons(props: IUpDownProps) {
       <button
         className={BUTTON_WRAPPER_CLASS}
         onClick={() => props.onHighlightPrevious()}
-        tabIndex={4}
+        tabIndex={6}
       >
         <span
           className={`${UP_BUTTON_CLASS} ${BUTTON_CONTENT_CLASS}`}
@@ -123,7 +195,7 @@ function UpDownButtons(props: IUpDownProps) {
       <button
         className={BUTTON_WRAPPER_CLASS}
         onClick={() => props.onHightlightNext()}
-        tabIndex={5}
+        tabIndex={7}
       >
         <span
           className={`${DOWN_BUTTON_CLASS} ${BUTTON_CONTENT_CLASS}`}
@@ -157,6 +229,9 @@ interface ISearchOverlayProps {
   onHighlightPrevious: Function;
   onStartQuery: Function;
   onEndSearch: Function;
+  onReplaceCurrent: Function;
+  onReplaceAll: Function;
+  isReadOnly: boolean;
 }
 
 class SearchOverlay extends React.Component<
@@ -168,29 +243,43 @@ class SearchOverlay extends React.Component<
     this.state = props.overlayState;
   }
 
-  private _onChange(event: React.ChangeEvent) {
-    this.setState({ inputText: (event.target as HTMLInputElement).value });
+  private _onSearchChange(event: React.ChangeEvent) {
+    const searchText = (event.target as HTMLInputElement).value;
+    this.setState({ searchText: searchText });
+    this._debouncedStartSearch(true, searchText);
+  }
+
+  private _onReplaceChange(event: React.ChangeEvent) {
+    this.setState({ replaceText: (event.target as HTMLInputElement).value });
   }
 
-  private _onKeydown(event: KeyboardEvent) {
+  private _onSearchKeydown(event: KeyboardEvent) {
     if (event.keyCode === 13) {
       event.preventDefault();
       event.stopPropagation();
       this._executeSearch(!event.shiftKey);
-    }
-    if (event.keyCode === 27) {
+    } else if (event.keyCode === 27) {
       event.preventDefault();
       event.stopPropagation();
       this.props.onEndSearch();
     }
   }
 
-  private _executeSearch(goForward: boolean) {
+  private _onReplaceKeydown(event: KeyboardEvent) {
+    if (event.keyCode === 13) {
+      event.preventDefault();
+      event.stopPropagation();
+      this.props.onReplaceCurrent(this.state.replaceText);
+    }
+  }
+
+  private _executeSearch(goForward: boolean, searchText?: string) {
     // execute search!
     let query;
+    const input = searchText ? searchText : this.state.searchText;
     try {
       query = Private.parseQuery(
-        this.state.inputText,
+        input,
         this.props.overlayState.caseSensitive,
         this.props.overlayState.useRegex
       );
@@ -212,55 +301,124 @@ class SearchOverlay extends React.Component<
     this.props.onStartQuery(query);
   }
 
-  private onClose() {
+  private _onClose() {
     // clean up and close widget
     this.props.onEndSearch();
   }
 
+  private _debounce(func: Function, wait: number) {
+    const context = this;
+    let timeout: number;
+    return function(...args: any[]) {
+      const later = function() {
+        timeout = null;
+        return func.apply(context, args);
+      };
+      clearTimeout(timeout);
+      timeout = setTimeout(later, wait);
+    };
+  }
+
+  private _debouncedStartSearch = this._debounce(
+    this._executeSearch.bind(this),
+    100
+  );
+
+  private _onReplaceToggled() {
+    this.setState({
+      replaceEntryShown: !this.state.replaceEntryShown
+    });
+  }
+
+  private _onSearchInputFocus() {
+    if (!this.state.searchInputFocused) {
+      this.setState({ searchInputFocused: true });
+    }
+  }
+
+  private _onSearchInputBlur() {
+    if (this.state.searchInputFocused) {
+      this.setState({ searchInputFocused: false });
+    }
+  }
+
   render() {
     return [
-      <SearchEntry
-        useRegex={this.props.overlayState.useRegex}
-        caseSensitive={this.props.overlayState.caseSensitive}
-        onCaseSensitiveToggled={() => {
-          this.props.onCaseSensitiveToggled();
-          this._executeSearch(true);
-        }}
-        onRegexToggled={() => {
-          this.props.onRegexToggled();
-          this._executeSearch(true);
-        }}
-        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}
-      />,
-      <button
-        className={BUTTON_WRAPPER_CLASS}
-        onClick={() => this.onClose()}
-        tabIndex={6}
-        key={3}
-      >
-        <span
-          className={`${CLOSE_BUTTON_CLASS} ${BUTTON_CONTENT_CLASS}`}
-          tabIndex={-1}
+      <div className={OVERLAY_ROW_CLASS} key={0}>
+        {this.props.isReadOnly ? (
+          <div className={TOGGLE_PLACEHOLDER} />
+        ) : (
+          <button
+            className={TOGGLE_WRAPPER}
+            onClick={() => this._onReplaceToggled()}
+            tabIndex={1}
+          >
+            <span
+              className={`${
+                this.state.replaceEntryShown
+                  ? REPLACE_TOGGLE_EXPANDED
+                  : REPLACE_TOGGLE_COLLAPSED
+              } ${BUTTON_CONTENT_CLASS}`}
+              tabIndex={-1}
+            />
+          </button>
+        )}
+        <SearchEntry
+          useRegex={this.props.overlayState.useRegex}
+          caseSensitive={this.props.overlayState.caseSensitive}
+          onCaseSensitiveToggled={() => {
+            this.props.onCaseSensitiveToggled();
+            this._executeSearch(true);
+          }}
+          onRegexToggled={() => {
+            this.props.onRegexToggled();
+            this._executeSearch(true);
+          }}
+          onKeydown={(e: KeyboardEvent) => this._onSearchKeydown(e)}
+          onChange={(e: React.ChangeEvent) => this._onSearchChange(e)}
+          onInputFocus={this._onSearchInputFocus.bind(this)}
+          onInputBlur={this._onSearchInputBlur.bind(this)}
+          inputFocused={this.state.searchInputFocused}
+          searchText={this.state.searchText}
+          forceFocus={this.props.overlayState.forceFocus}
+        />
+        <SearchIndices
+          currentIndex={this.props.overlayState.currentIndex}
+          totalMatches={this.props.overlayState.totalMatches}
+        />
+        <UpDownButtons
+          onHighlightPrevious={() => this._executeSearch(false)}
+          onHightlightNext={() => this._executeSearch(true)}
         />
-      </button>,
+        <button
+          className={BUTTON_WRAPPER_CLASS}
+          onClick={() => this._onClose()}
+          tabIndex={8}
+        >
+          <span
+            className={`${CLOSE_BUTTON_CLASS} ${BUTTON_CONTENT_CLASS}`}
+            tabIndex={-1}
+          />
+        </button>
+      </div>,
+      <div className={OVERLAY_ROW_CLASS} key={1}>
+        {!this.props.isReadOnly && this.state.replaceEntryShown ? (
+          <ReplaceEntry
+            onReplaceKeydown={(e: KeyboardEvent) => this._onReplaceKeydown(e)}
+            onChange={(e: React.ChangeEvent) => this._onReplaceChange(e)}
+            onReplaceCurrent={() =>
+              this.props.onReplaceCurrent(this.state.replaceText)
+            }
+            onReplaceAll={() => this.props.onReplaceAll(this.state.replaceText)}
+            replaceText={this.state.replaceText}
+            ref="replaceEntry"
+          />
+        ) : null}
+      </div>,
       <div
         className={REGEX_ERROR_CLASS}
         hidden={this.state.errorMessage && this.state.errorMessage.length === 0}
-        key={4}
+        key={3}
       >
         {this.state.errorMessage}
       </div>
@@ -279,7 +437,10 @@ export function createSearchOverlay(
     onHightlightNext,
     onHighlightPrevious,
     onStartQuery,
-    onEndSearch
+    onReplaceCurrent,
+    onReplaceAll,
+    onEndSearch,
+    isReadOnly
   } = options;
   const widget = ReactWidget.create(
     <UseSignal signal={widgetChanged} initialArgs={overlayState}>
@@ -292,7 +453,10 @@ export function createSearchOverlay(
             onHighlightPrevious={onHighlightPrevious}
             onStartQuery={onStartQuery}
             onEndSearch={onEndSearch}
+            onReplaceCurrent={onReplaceCurrent}
+            onReplaceAll={onReplaceAll}
             overlayState={args}
+            isReadOnly={isReadOnly}
           />
         );
       }}
@@ -312,6 +476,9 @@ namespace createSearchOverlay {
     onHighlightPrevious: Function;
     onStartQuery: Function;
     onEndSearch: Function;
+    onReplaceCurrent: Function;
+    onReplaceAll: Function;
+    isReadOnly: boolean;
   }
 }
 

+ 107 - 16
packages/documentsearch/style/index.css

@@ -11,19 +11,26 @@
 
 .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);
+  background-color: var(--jp-toolbar-background);
+  border-bottom: var(--jp-border-width) solid var(--jp-toolbar-border-color);
+  border-left: var(--jp-border-width) solid var(--jp-toolbar-border-color);
   top: 0;
   right: 0;
   z-index: 7;
   min-width: 300px;
   padding: 2px;
   font-size: var(--jp-ui-font-size1);
+}
+
+.jp-DocumentSearch-overlay button {
+  background-color: var(--jp-toolbar-background);
+  outline: 0;
+}
+
+.jp-DocumentSearch-overlay-row {
   display: flex;
   align-items: center;
+  margin-bottom: 2px;
 }
 
 .jp-DocumentSearch-overlay * {
@@ -39,34 +46,56 @@
   height: 100%;
 }
 
+.jp-DocumentSearch-input-wrapper {
+  border: var(--jp-border-width) solid var(--jp-border-color0);
+  display: flex;
+  background-color: var(--jp-layout-color0);
+  margin: 2px;
+}
+
+.jp-DocumentSearch-focused-input {
+  border: var(--jp-border-width) solid var(--jp-cell-editor-active-border-color);
+}
+
+.jp-DocumentSearch-input-wrapper * {
+  background-color: var(--jp-layout-color0);
+}
+
+.jp-DocumentSearch-toggle-wrapper,
 .jp-DocumentSearch-button-wrapper {
   all: initial;
   overflow: hidden;
   display: inline-block;
-  outline: 0;
   border: none;
-  width: 20px;
-  height: 20px;
   box-sizing: border-box;
   background-repeat: no-repeat;
 }
 
+.jp-DocumentSearch-toggle-wrapper {
+  width: 14px;
+  height: 14px;
+}
+
+.jp-DocumentSearch-button-wrapper {
+  width: 20px;
+  height: 20px;
+}
+
+.jp-DocumentSearch-toggle-wrapper:focus,
 .jp-DocumentSearch-button-wrapper:focus {
   outline: var(--jp-border-width) solid
     var(--jp-cell-editor-active-border-color);
   outline-offset: -1px;
 }
 
+.jp-DocumentSearch-toggle-wrapper,
 .jp-DocumentSearch-button-wrapper,
 .jp-DocumentSearch-button-content:focus {
   outline: none;
 }
 
-.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-toggle-placeholder {
+  width: 5px;
 }
 
 .jp-DocumentSearch-regex-button {
@@ -98,18 +127,24 @@
   padding-left: 10px;
   padding-right: 10px;
   user-select: none;
-  min-width: 50px;
+  min-width: 43px;
   display: inline-block;
 }
 
 .jp-DocumentSearch-up-down-wrapper {
   display: inline-block;
+  padding-right: 2px;
 }
 
 .jp-DocumentSearch-up-down-wrapper button {
-  background-color: var(--jp-layout-color0);
-  vertical-align: middle;
+  outline: 0;
+  border: none;
+  width: 20px;
+  height: 20px;
+  background-repeat: no-repeat;
   background-position: center;
+  vertical-align: middle;
+  margin: 1px 7px 2px;
 }
 
 .jp-DocumentSearch-up-button {
@@ -137,3 +172,59 @@
 .jp-DocumentSearch-regex-error {
   color: var(--jp-error-color0);
 }
+
+.jp-DocumentSearch-replace-entry {
+  border: var(--jp-border-width) solid var(--jp-border-color0);
+  display: flex;
+  background-color: var(--jp-layout-color0);
+  font-size: var(--jp-ui-font-size1);
+  width: 186px;
+  margin: 2px;
+}
+
+.jp-DocumentSearch-replace-button-wrapper {
+  overflow: hidden;
+  display: inline-block;
+  box-sizing: border-box;
+  border: var(--jp-border-width) solid var(--jp-border-color0);
+  margin: 2px;
+}
+
+.jp-DocumentSearch-replace-entry:focus,
+.jp-DocumentSearch-replace-button-wrapper:focus {
+  border: var(--jp-border-width) solid var(--jp-cell-editor-active-border-color);
+}
+
+.jp-DocumentSearch-replace-button {
+  display: inline-block;
+  text-align: center;
+  cursor: pointer;
+  box-sizing: border-box;
+  background-color: var(--jp-toolbar-background);
+  color: var(--jp-font-color1);
+  width: 100%;
+  height: 100%;
+}
+
+.jp-DocumentSearch-replace-button:focus {
+  outline: none;
+}
+
+.jp-DocumentSearch-replace-wrapper-class {
+  margin-left: 14px;
+  display: flex;
+}
+
+.jp-DocumentSearch-replace-toggle-collapsed,
+.jp-DocumentSearch-replace-toggle-expanded {
+  border: none;
+  background-color: var(--jp-toolbar-background);
+}
+
+.jp-DocumentSearch-replace-toggle-collapsed {
+  background-image: var(--jp-image-caretright);
+}
+
+.jp-DocumentSearch-replace-toggle-expanded {
+  background-image: var(--jp-image-caretdown);
+}

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

@@ -1,11 +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">
+	 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;}
+	.st1{fill:#E0E0E0;}
 </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 "/>
+<rect class="st0" width="20" height="20"/>
+<polygon class="st1" points="10,13.7 3.6,7.4 4.4,6.6 10,12.3 15.6,6.6 16.4,7.4 "/>
 </svg>

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

@@ -1,11 +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">
+	 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;}
+	.st1{fill:#E0E0E0;}
 </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 "/>
+<rect class="st0" width="20" height="20"/>
+<polygon class="st1" points="15.6,13.4 10,7.7 4.4,13.4 3.6,12.6 10,6.3 16.4,12.6 "/>
 </svg>

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

@@ -4,16 +4,16 @@
 	 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;}
+	.st1{fill:#D7D7D7;}
 </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"/>
+	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.5,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.2c0.1-0.1,0.2-0.2,0.2-0.4c0-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.3,4,13.6,4,13.9,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.5C16,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.2c0.1,0,0.2,0,0.3,0
+	V9.8z M15.2,6.9C13.9,7.5,12,7.1,12,8.3c0,1.4,3.1,1,3.1-0.5V6.9z"/>
 </svg>

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

@@ -4,7 +4,7 @@
 	 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;}
+	.st1{fill:#D7D7D7;}
 </style>
 <g id="Regex_Icon">
 	<rect class="st0" width="20" height="20"/>
@@ -12,8 +12,8 @@
 	<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"/>
+		<rect x="8.5" y="7.5" transform="matrix(0.866 -0.5 0.5 0.866 -2.3255 7.3219)" width="8" height="1"/>
+		<rect x="12" y="4" transform="matrix(0.5 -0.866 0.866 0.5 -0.6779 14.8252)" width="1" height="8"/>
 	</g>
 </g>
 </svg>

+ 2 - 2
packages/theme-dark-extension/style/variables.css

@@ -372,8 +372,8 @@ all of MD as it is not optimized for dense, information rich UIs.
 
   /* Search-related styles */
 
-  --jp-search-toggle-off-opacity: 0.5;
-  --jp-search-toggle-hover-opacity: 0.75;
+  --jp-search-toggle-off-opacity: 0.6;
+  --jp-search-toggle-hover-opacity: 0.8;
   --jp-search-toggle-on-opacity: 1;
   --jp-search-selected-match-background-color: rgb(255, 225, 0);
   --jp-search-selected-match-color: black;

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

@@ -1,11 +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">
+	 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;}
+	.st1{fill:#646464;}
 </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 "/>
+<rect class="st0" width="20" height="20"/>
+<polygon class="st1" points="9.9,13.6 3.6,7.4 4.4,6.6 9.9,12.2 15.4,6.7 16.1,7.4 "/>
 </svg>

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

@@ -1,11 +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">
+<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;}
+	.st1{fill:#646464;}
 </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 "/>
+<rect class="st0" width="20" height="20"/>
+<polygon class="st1" points="15.4,13.3 9.9,7.7 4.4,13.2 3.6,12.5 9.9,6.3 16.1,12.6 "/>
 </svg>

+ 14 - 11
packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg

@@ -4,16 +4,19 @@
 	 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;}
+	.st1{fill:#414141;}
+	.st2{fill:#FFFFFF;}
 </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"/>
+<g>
+	<rect class="st0" width="20" height="20"/>
+	<rect x="2" y="2" class="st1" width="16" height="16"/>
+	<path class="st2" d="M7.6,8h0.9l3.5,8h-1.1L10,14H6l-0.9,2H4L7.6,8z M8,9.1L6.4,13h3.2L8,9.1z"/>
+	<path class="st2" 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.7
+		c-0.3,0.3-0.6,0.5-0.9,0.7c-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.5
+		c-0.1-0.2-0.1-0.4-0.1-0.7c0-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.5,6.8c0.2-0.1,0.5-0.1,0.7-0.2
+		c0.3-0.1,0.5-0.1,0.7-0.1c0.2,0,0.4-0.1,0.6-0.1c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.2,0.2-0.4c0-1-1.1-1-1.3-1
+		c-0.4,0-1.4,0-1.4,1.2h-0.9c0-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.3,4,13.6,4,13.9,4
+		c0.3,0,0.5,0,0.8,0.1c0.3,0,0.5,0.1,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5C16,5,16,5.2,16,5.6v2.9c0,0.2,0,0.4,0,0.5
+		c0,0.1,0.1,0.2,0.3,0.2c0.1,0,0.2,0,0.3,0V9.8z M15.2,6.9c-1.2,0.6-3.1,0.2-3.1,1.4c0,1.4,3.1,1,3.1-0.5V6.9z"/>
+</g>
 </svg>

+ 7 - 6
packages/theme-light-extension/style/icons/jupyter/search_regex.svg

@@ -4,16 +4,17 @@
 	 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;}
+	.st1{fill:#414141;}
+	.st2{fill:#FFFFFF;}
 </style>
-<g id="Regex_Icon">
+<g id="Regex_Icon_2_">
 	<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"/>
+	<circle class="st2" 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"/>
+		<rect x="12" y="4" class="st2" width="1" height="8"/>
+		<rect x="8.5" y="7.5" transform="matrix(0.866 -0.5 0.5 0.866 -2.3255 7.3219)" class="st2" width="8" height="1"/>
+		<rect x="12" y="4" transform="matrix(0.5 -0.866 0.866 0.5 -0.6779 14.8252)" class="st2" width="1" height="8"/>
 	</g>
 </g>
 </svg>

+ 2 - 2
packages/theme-light-extension/style/variables.css

@@ -369,8 +369,8 @@ all of MD as it is not optimized for dense, information rich UIs.
 
   /* Search-related styles */
 
-  --jp-search-toggle-off-opacity: 0.4;
-  --jp-search-toggle-hover-opacity: 0.65;
+  --jp-search-toggle-off-opacity: 0.5;
+  --jp-search-toggle-hover-opacity: 0.8;
   --jp-search-toggle-on-opacity: 1;
   --jp-search-selected-match-background-color: rgb(245, 200, 0);
   --jp-search-selected-match-color: black;