Skip to content

Commit

Permalink
[search-in-workspace] Enable history for input fields
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel Bodeen <[email protected]>
  • Loading branch information
gbodeen committed Jun 1, 2021
1 parent 31e72f2 commit 8f6d5d3
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>;

export class InputWithHistory extends React.Component<InputAttributes, HistoryState> {
static LIMIT = 100;

private input = React.createRef<HTMLInputElement>();

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<HTMLInputElement>): 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<HTMLInputElement>): 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 (
<input
{...this.props}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
ref={this.input}
/>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +64,11 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
protected searchTerm = '';
protected replaceTerm = '';

private searchRef = React.createRef<InputWithHistory>();
private replaceRef = React.createRef<InputWithHistory>();
private includeRef = React.createRef<InputWithHistory>();
private excludeRef = React.createRef<InputWithHistory>();

protected _showReplaceField = false;
protected get showReplaceField(): boolean {
return this._showReplaceField;
Expand Down Expand Up @@ -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 ?? [],
};
}

Expand All @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -436,7 +456,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
}

protected renderSearchField(): React.ReactNode {
const input = <input
const input = <InputWithHistory
id='search-input-field'
className='theia-input'
title='Search'
Expand All @@ -449,7 +469,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
onKeyDown={this.onKeyDownSearch}
onFocus={this.handleFocusSearchInputBox}
onBlur={this.handleBlurSearchInputBox}
></input>;
ref={this.searchRef}
/>;
const notification = this.renderNotification();
const optionContainer = this.renderOptionContainer();
const tooMany = this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'tooManyResults' : '';
Expand Down Expand Up @@ -479,7 +500,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
protected renderReplaceField(): React.ReactNode {
const replaceAllButtonContainer = this.renderReplaceAllButtonContainer();
return <div className={`replace-field${this.showReplaceField ? '' : ' hidden'}`}>
<input
<InputWithHistory
id='replace-input-field'
className='theia-input'
title='Replace'
Expand All @@ -489,8 +510,9 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
defaultValue={this.replaceTerm}
onKeyUp={this.updateReplaceTerm}
onFocus={this.handleFocusReplaceInputBox}
onBlur={this.handleBlurReplaceInputBox}>
</input>
onBlur={this.handleBlurReplaceInputBox}
ref={this.replaceRef}
/>
{replaceAllButtonContainer}
</div>;
}
Expand Down Expand Up @@ -573,7 +595,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
const value = currentValue && currentValue.join(', ') || '';
return <div className='glob-field'>
<div className='label'>{'files to ' + kind}</div>
<input
<InputWithHistory
className='theia-input'
type='text'
size={1}
Expand Down Expand Up @@ -606,7 +628,9 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
}
}}
onFocus={kind === 'include' ? this.handleFocusIncludesInputBox : this.handleFocusExcludesInputBox}
onBlur={kind === 'include' ? this.handleBlurIncludesInputBox : this.handleBlurExcludesInputBox}></input>
onBlur={kind === 'include' ? this.handleBlurIncludesInputBox : this.handleBlurExcludesInputBox}
ref={kind === 'include' ? this.includeRef : this.excludeRef}
/>
</div>;
}

Expand Down

0 comments on commit 8f6d5d3

Please sign in to comment.