From 8f6d5d3ba04683513f8f5ffda96477edf699eab4 Mon Sep 17 00:00:00 2001 From: Gabriel Bodeen Date: Wed, 26 May 2021 22:00:22 +0200 Subject: [PATCH] [search-in-workspace] Enable history for input fields Signed-off-by: Gabriel Bodeen --- .../browser/components/input-with-history.tsx | 141 ++++++++++++++++++ .../browser/search-in-workspace-widget.tsx | 46 ++++-- 2 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 packages/search-in-workspace/src/browser/components/input-with-history.tsx diff --git a/packages/search-in-workspace/src/browser/components/input-with-history.tsx b/packages/search-in-workspace/src/browser/components/input-with-history.tsx new file mode 100644 index 0000000000000..2b5bcdf2f8c77 --- /dev/null +++ b/packages/search-in-workspace/src/browser/components/input-with-history.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[]; +}; +type InputAttributes = React.InputHTMLAttributes; + +export class InputWithHistory extends React.Component { + static LIMIT = 100; + + private input = React.createRef(); + + constructor(props: InputAttributes) { + super(props); + this.state = { + history: [], + }; + } + + componentDidUpdate(): void { + // Catch updates from the StatefulWidget restore method + if (this.value && !this.state.history.length) { + this.setHistory([this.value]); + } + } + + setHistory(history: string[]): void { + this.setState(prevState => ({ + ...prevState, + history, + })); + } + + 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 } = this.state; + const index = history.indexOf(this.value); + if (index > 0 && index < history.length) { + this.value = history[index - 1]; + } + } + + /** + * Switch the input's text to the next value, if any. + */ + nextValue(): void { + const { history } = this.state; + const index = history.indexOf(this.value); + if (index >= 0 && index < history.length - 1) { + this.value = history[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; + const index = history.indexOf(this.value); + if (index !== -1) { + history.splice(index, 1); + } + this.applyLimit(history); + this.setHistory(history.concat(this.value)); + } + + private applyLimit(history: string[]): string[] { + if (history.length > InputWithHistory.LIMIT) { + history.splice(0, history.length - InputWithHistory.LIMIT); + } + return history; + } + + 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 b5bdc31a7600f..7dd185c77ceb3 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 { InputWithHistory } from './components/input-with-history'; 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, + searchHistory: this.searchRef.current?.state.history ?? [this.searchTerm], + replaceHistory: this.replaceRef.current?.state.history ?? [this.replaceTerm], + includeHistory: this.includeRef.current?.state.history ?? [], + excludeHistory: this.excludeRef.current?.state.history ?? [], }; } @@ -188,6 +198,18 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge this.showReplaceField = oldState.showReplaceField; this.resultTreeWidget.replaceTerm = this.replaceTerm; this.resultTreeWidget.showReplaceButtons = this.showReplaceField; + if (this.searchRef.current) { + this.searchRef.current.setHistory(oldState.searchHistory ?? []); + } + if (this.replaceRef.current) { + this.replaceRef.current.setHistory(oldState.replaceHistory ?? []); + } + if (this.includeRef.current) { + this.includeRef.current.setHistory(oldState.includeHistory ?? []); + } + if (this.excludeRef.current) { + this.excludeRef.current.setHistory(oldState.excludeHistory ?? []); + } this.refresh(); } @@ -402,9 +424,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; @@ -436,7 +456,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' : ''; @@ -479,7 +500,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected renderReplaceField(): React.ReactNode { const replaceAllButtonContainer = this.renderReplaceAllButtonContainer(); return
- - + onBlur={this.handleBlurReplaceInputBox} + ref={this.replaceRef} + /> {replaceAllButtonContainer}
; } @@ -573,7 +595,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} + />
; }