diff --git a/packages/codemirror/src/editor.ts b/packages/codemirror/src/editor.ts index d2989872244d..84c5c1e2f3cc 100644 --- a/packages/codemirror/src/editor.ts +++ b/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) }; } diff --git a/packages/csvviewer-extension/src/searchprovider.ts b/packages/csvviewer-extension/src/searchprovider.ts index 8f1a54a39296..a92754fe8277 100644 --- a/packages/csvviewer-extension/src/searchprovider.ts +++ b/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 { + async highlightPrevious(): Promise { 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 { + 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 { + 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; private _query: RegExp; private _changed = new Signal(this); diff --git a/packages/documentsearch/src/interfaces.ts b/packages/documentsearch/src/interfaces.ts index 3dda679caccc..b3dea7643575 100644 --- a/packages/documentsearch/src/interfaces.ts +++ b/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; + /** + * 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; + + /** + * 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; + /** * 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; } diff --git a/packages/documentsearch/src/providers/codemirrorsearchprovider.ts b/packages/documentsearch/src/providers/codemirrorsearchprovider.ts index cf972e707e57..bf579c0d8c38 100644 --- a/packages/documentsearch/src/providers/codemirrorsearchprovider.ts +++ b/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 { 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 { + // 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 { + 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); private _overlay: any; diff --git a/packages/documentsearch/src/providers/notebooksearchprovider.ts b/packages/documentsearch/src/providers/notebooksearchprovider.ts index 9b17f5d6cc1d..56916481b3fa 100644 --- a/packages/documentsearch/src/providers/notebooksearchprovider.ts +++ b/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 { + 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 { + 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[] = []; diff --git a/packages/documentsearch/src/searchinstance.ts b/packages/documentsearch/src/searchinstance.ts index f548472df27f..4c8b74d52f2e 100644 --- a/packages/documentsearch/src/searchinstance.ts +++ b/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); private _activeProvider: ISearchProvider; diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index b027f0bef04f..28517ca3cf69 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/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 { constructor(props: ISearchEntryProps) { super(props); @@ -53,7 +73,7 @@ class SearchEntry extends React.Component { } componentDidUpdate() { - if (this.props.forceFocus) { + if (this.props.forceFocus && this.props.inputFocused) { this.focusInput(); } } @@ -66,21 +86,27 @@ class SearchEntry extends React.Component { ? REGEX_BUTTON_CLASS_ON : REGEX_BUTTON_CLASS_OFF; + const wrapperClass = `${INPUT_WRAPPER_CLASS} ${ + this.props.inputFocused ? FOCUSED_INPUT : '' + }`; + return ( -
+
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" /> + +
+ ); + } +} + interface IUpDownProps { onHighlightPrevious: Function; onHightlightNext: Function; @@ -113,7 +185,7 @@ function UpDownButtons(props: IUpDownProps) { + )} + { + 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} + /> + + this._executeSearch(false)} + onHightlightNext={() => this._executeSearch(true)} /> - , + +
, +
+ {!this.props.isReadOnly && this.state.replaceEntryShown ? ( + 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} +
, @@ -279,7 +437,10 @@ export function createSearchOverlay( onHightlightNext, onHighlightPrevious, onStartQuery, - onEndSearch + onReplaceCurrent, + onReplaceAll, + onEndSearch, + isReadOnly } = options; const widget = ReactWidget.create( @@ -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; } } diff --git a/packages/documentsearch/style/index.css b/packages/documentsearch/style/index.css index a13c155660ef..0b7b2febae53 100644 --- a/packages/documentsearch/style/index.css +++ b/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); +} diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg old mode 100755 new mode 100644 index c170d160351a..d6553fb23416 --- a/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg +++ b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg @@ -1,11 +1,11 @@ + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> - - + + diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg old mode 100755 new mode 100644 index 6e37e815964a..25d8a178c58f --- a/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg +++ b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg @@ -1,11 +1,11 @@ + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> - - + + diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg b/packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg old mode 100755 new mode 100644 index 4d90a69731fb..f3375e6a365c --- a/packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg +++ b/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"> + 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"/> diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_regex.svg b/packages/theme-dark-extension/style/icons/jupyter/search_regex.svg old mode 100755 new mode 100644 index 24cf5a0db817..1c948c79d97f --- a/packages/theme-dark-extension/style/icons/jupyter/search_regex.svg +++ b/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"> @@ -12,8 +12,8 @@ - - + + diff --git a/packages/theme-dark-extension/style/variables.css b/packages/theme-dark-extension/style/variables.css index cd23c54011ca..4ba8867b57ae 100644 --- a/packages/theme-dark-extension/style/variables.css +++ b/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; diff --git a/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg b/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg old mode 100755 new mode 100644 index c170d160351a..ada7f71e1b85 --- a/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg +++ b/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg @@ -1,11 +1,11 @@ + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> - - + + diff --git a/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg b/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg old mode 100755 new mode 100644 index 6e37e815964a..308856c36f46 --- a/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg +++ b/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg @@ -1,11 +1,11 @@ - + - - + + diff --git a/packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg b/packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg old mode 100755 new mode 100644 index 4d90a69731fb..8d863c177c24 --- a/packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg +++ b/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"> - - - - + + + + + + diff --git a/packages/theme-light-extension/style/icons/jupyter/search_regex.svg b/packages/theme-light-extension/style/icons/jupyter/search_regex.svg old mode 100755 new mode 100644 index 24cf5a0db817..e009ad0004fd --- a/packages/theme-light-extension/style/icons/jupyter/search_regex.svg +++ b/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"> - + - + - - - + + + diff --git a/packages/theme-light-extension/style/variables.css b/packages/theme-light-extension/style/variables.css index f5ac130c4261..8afd956b3705 100644 --- a/packages/theme-light-extension/style/variables.css +++ b/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;