diff --git a/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx b/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx new file mode 100644 index 0000000000000..ea2e8307b5111 --- /dev/null +++ b/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx @@ -0,0 +1,141 @@ +/******************************************************************************** + * Copyright (C) 2021 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from '@theia/core/shared/react'; +import { Key, KeyCode } from '@theia/core/lib/browser'; +import debounce = require('@theia/core/shared/lodash.debounce'); + +interface HistoryState { + history: string[]; + index: number; +}; +type InputAttributes = React.InputHTMLAttributes; + +export class SearchInWorkspaceInput extends React.Component { + static LIMIT = 100; + + private input = React.createRef(); + + constructor(props: InputAttributes) { + super(props); + this.state = { + history: [], + index: 0, + }; + } + + setHistory(history: string[]): void { + this.setState(prevState => ({ + ...prevState, + history, + })); + } + + setIndex(index: number): void { + const { history } = this.state; + this.value = history[index]; + this.setState(prevState => ({ + ...prevState, + index, + })); + } + + get value(): string { + return this.input.current?.value ?? ''; + } + + set value(value: string) { + if (this.input.current) { + this.input.current.value = value; + } + } + + /** + * Handle history navigation without overriding the parent's onKeyDown handler, if any. + */ + protected readonly onKeyDown = (e: React.KeyboardEvent): void => { + if (Key.ARROW_UP.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { + e.preventDefault(); + this.previousValue(); + } else if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { + e.preventDefault(); + this.nextValue(); + } + this.props.onKeyDown?.(e); + }; + + /** + * Switch the input's text to the previous value, if any. + */ + previousValue(): void { + const { history, index } = this.state; + if (!this.value) { + this.value = history[index]; + } else if (index > 0 && index < history.length) { + this.setIndex(index - 1); + } + } + + /** + * Switch the input's text to the next value, if any. + */ + nextValue(): void { + const { history, index } = this.state; + if (index === history.length - 1) { + this.value = ''; + } else if (!this.value) { + this.value = history[index]; + } else if (index >= 0 && index < history.length - 1) { + this.setIndex(index + 1); + } + } + + /** + * Handle history collection without overriding the parent's onChange handler, if any. + */ + protected readonly onChange = (e: React.ChangeEvent): void => { + this.addToHistory(); + this.props.onChange?.(e); + }; + + /** + * Add a nonempty current value to the history, if not already present. (Debounced, 1 second delay.) + */ + readonly addToHistory = debounce(this.doAddToHistory, 1000); + + private doAddToHistory(): void { + if (!this.value) { + return; + } + const history = this.state.history + .filter(term => term !== this.value) + .concat(this.value) + .slice(-SearchInWorkspaceInput.LIMIT); + this.setHistory(history); + this.setIndex(history.length - 1); + } + + render(): React.ReactNode { + return ( + + ); + } +} diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx index b54c618dedb42..42ac99ab82ba2 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx @@ -27,6 +27,7 @@ import { CancellationTokenSource } from '@theia/core'; import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory'; import { EditorManager } from '@theia/editor/lib/browser'; import { SearchInWorkspacePreferences } from './search-in-workspace-preferences'; +import { SearchInWorkspaceInput } from './components/search-in-workspace-input'; export interface SearchFieldState { className: string; @@ -63,6 +64,11 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected searchTerm = ''; protected replaceTerm = ''; + private searchRef = React.createRef(); + private replaceRef = React.createRef(); + private includeRef = React.createRef(); + private excludeRef = React.createRef(); + protected _showReplaceField = false; protected get showReplaceField(): boolean { return this._showReplaceField; @@ -171,7 +177,11 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge searchInWorkspaceOptions: this.searchInWorkspaceOptions, searchTerm: this.searchTerm, replaceTerm: this.replaceTerm, - showReplaceField: this.showReplaceField + showReplaceField: this.showReplaceField, + searchHistoryState: this.searchRef.current?.state, + replaceHistoryState: this.replaceRef.current?.state, + includeHistoryState: this.includeRef.current?.state, + excludeHistoryState: this.excludeRef.current?.state, }; } @@ -188,6 +198,10 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.showReplaceField = oldState.showReplaceField; this.resultTreeWidget.replaceTerm = this.replaceTerm; this.resultTreeWidget.showReplaceButtons = this.showReplaceField; + this.searchRef.current?.setState(oldState.searchHistoryState); + this.replaceRef.current?.setState(oldState.replaceHistoryState); + this.includeRef.current?.setState(oldState.includeHistoryState); + this.excludeRef.current?.setState(oldState.excludeHistoryState); this.refresh(); } @@ -404,9 +418,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected doSearch(e: React.KeyboardEvent): void { if (e.target) { const searchValue = (e.target as HTMLInputElement).value; - if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { - this.resultTreeWidget.focusFirstResult(); - } else if (this.searchTerm === searchValue && Key.ENTER.keyCode !== KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { + if (this.searchTerm === searchValue && Key.ENTER.keyCode !== KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { return; } else { this.searchTerm = searchValue; @@ -438,7 +450,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge } protected renderSearchField(): React.ReactNode { - const input = ; + ref={this.searchRef} + />; const notification = this.renderNotification(); const optionContainer = this.renderOptionContainer(); const tooMany = this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'tooManyResults' : ''; @@ -481,7 +494,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected renderReplaceField(): React.ReactNode { const replaceAllButtonContainer = this.renderReplaceAllButtonContainer(); return
- - + onBlur={this.handleBlurReplaceInputBox} + ref={this.replaceRef} + /> {replaceAllButtonContainer}
; } @@ -575,7 +589,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge const value = currentValue && currentValue.join(', ') || ''; return
{'files to ' + kind}
- + onBlur={kind === 'include' ? this.handleBlurIncludesInputBox : this.handleBlurExcludesInputBox} + ref={kind === 'include' ? this.includeRef : this.excludeRef} + />
; }