Make Search All Files search as you type (#84004)

* Make Search All Files search as you type
Closes #46326.

* Fix tests

* Cancel search before queueing a new one.
Reduces background load while user is typing rest of query

* Reduce progress indicator flicker in fast searches

* Don't trigger search on control chars

* Remove ".only" form test

* Remove unused emitter parameter

* Clean up diff

* Delay adding possibly partial search string to histroy

* Clean up diff
This commit is contained in:
Jackson Kearl 2019-11-06 12:48:36 -08:00 committed by GitHub
parent 73d9d76ebd
commit 43fc44efa6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 58 additions and 16 deletions

View file

@ -167,6 +167,9 @@ export class PatternInputWidget extends Widget {
export class ExcludePatternInputWidget extends PatternInputWidget {
private _onChangeIgnoreBoxEmitter = this._register(new Emitter<void>());
onChangeIgnoreBox = this._onChangeIgnoreBoxEmitter.event;
constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null),
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService
@ -200,6 +203,7 @@ export class ExcludePatternInputWidget extends PatternInputWidget {
isChecked: true,
}));
this._register(this.useExcludesAndIgnoreFilesBox.onChange(viaKeyboard => {
this._onChangeIgnoreBoxEmitter.fire();
if (!viaKeyboard) {
this.inputBox.focus();
}

View file

@ -774,6 +774,16 @@ configurationRegistry.registerConfiguration({
],
default: 'auto',
description: nls.localize('search.actionsPosition', "Controls the positioning of the actionbar on rows in the search view.")
},
'search.searchOnType': {
type: 'boolean',
default: true,
description: nls.localize('search.searchOnType', "Search all files as you type.")
},
'search.searchOnTypeDebouncePeriod': {
type: 'number',
default: 300,
markdownDescription: nls.localize('search.searchOnTypeDebouncePeriod', "When `#search.searchOnType#` is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when `search.searchOnType` is disabled.")
}
}
});

View file

@ -265,7 +265,7 @@ export class RefreshAction extends Action {
get enabled(): boolean {
const searchView = getSearchView(this.viewletService, this.panelService);
return !!searchView && searchView.hasSearchResults();
return !!searchView && searchView.hasSearchPattern();
}
update(): void {

View file

@ -128,6 +128,7 @@ export class SearchView extends ViewletPanel {
private searchWithoutFolderMessageElement: HTMLElement | undefined;
private currentSearchQ = Promise.resolve();
private addToSearchHistoryDelayer: Delayer<void>;
constructor(
options: IViewletPanelOptions,
@ -182,6 +183,8 @@ export class SearchView extends ViewletPanel {
this.delayedRefresh = this._register(new Delayer<void>(250));
this.addToSearchHistoryDelayer = this._register(new Delayer<void>(500));
this.actions = [
this._register(this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL)),
this._register(this.instantiationService.createInstance(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL))
@ -282,6 +285,7 @@ export class SearchView extends ViewletPanel {
this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true));
this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget
this.inputPatternExcludes.onChangeIgnoreBox(() => this.onQueryChanged(true));
this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused);
this.messagesElement = dom.append(this.container, $('.messages'));
@ -382,7 +386,7 @@ export class SearchView extends ViewletPanel {
}
this._register(this.searchWidget.onSearchSubmit(() => this.onQueryChanged()));
this._register(this.searchWidget.onSearchCancel(() => this.cancelSearch()));
this._register(this.searchWidget.onSearchCancel(({ focus }) => this.cancelSearch(focus)));
this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.onQueryChanged(true)));
this._register(this.searchWidget.onDidHeightChange(() => this.reLayout()));
@ -796,6 +800,7 @@ export class SearchView extends ViewletPanel {
this.searchWidget.searchInput.setValue(selectedText);
updatedText = true;
this.onQueryChanged();
}
}
@ -921,6 +926,10 @@ export class SearchView extends ViewletPanel {
return !this.viewModel.searchResult.isEmpty();
}
hasSearchPattern(): boolean {
return this.searchWidget.searchInput.getValue().length > 0;
}
clearSearchResults(): void {
this.viewModel.searchResult.clear();
this.showEmptyStage();
@ -934,9 +943,9 @@ export class SearchView extends ViewletPanel {
aria.status(nls.localize('ariaSearchResultsClearStatus', "The search results have been cleared"));
}
cancelSearch(): boolean {
cancelSearch(focus: boolean = true): boolean {
if (this.viewModel.cancelSearch()) {
this.searchWidget.focus();
if (focus) { this.searchWidget.focus(); }
return true;
}
return false;
@ -1241,7 +1250,7 @@ export class SearchView extends ViewletPanel {
}
private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): void {
this.searchWidget.searchInput.onSearchSubmit();
this.addToSearchHistoryDelayer.trigger(() => this.searchWidget.searchInput.onSearchSubmit());
this.inputPatternExcludes.onSearchSubmit();
this.inputPatternIncludes.onSearchSubmit();
@ -1254,7 +1263,7 @@ export class SearchView extends ViewletPanel {
private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): Thenable<void> {
let progressComplete: () => void;
this.progressService.withProgress({ location: VIEWLET_ID }, _progress => {
this.progressService.withProgress({ location: VIEWLET_ID, delay: 300 }, _progress => {
return new Promise(resolve => progressComplete = resolve);
});

View file

@ -118,15 +118,15 @@ export class SearchWidget extends Widget {
private replaceActive: IContextKey<boolean>;
private replaceActionBar!: ActionBar;
private _replaceHistoryDelayer: Delayer<void>;
private _searchDelayer: Delayer<void>;
private ignoreGlobalFindBufferOnNextFocus = false;
private previousGlobalFindBufferValue: string | null = null;
private _onSearchSubmit = this._register(new Emitter<void>());
readonly onSearchSubmit: Event<void> = this._onSearchSubmit.event;
private _onSearchCancel = this._register(new Emitter<void>());
readonly onSearchCancel: Event<void> = this._onSearchCancel.event;
private _onSearchCancel = this._register(new Emitter<{ focus: boolean }>());
readonly onSearchCancel: Event<{ focus: boolean }> = this._onSearchCancel.event;
private _onReplaceToggled = this._register(new Emitter<void>());
readonly onReplaceToggled: Event<void> = this._onReplaceToggled.event;
@ -165,6 +165,7 @@ export class SearchWidget extends Widget {
this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService);
this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService);
this._replaceHistoryDelayer = new Delayer<void>(500);
this._searchDelayer = this._register(new Delayer<void>(this.searchConfiguration.searchOnTypeDebouncePeriod));
this.render(container, options);
this.configurationService.onDidChangeConfiguration(e => {
@ -323,9 +324,6 @@ export class SearchWidget extends Widget {
this.searchInput.setRegex(!!options.isRegex);
this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
this.searchInput.setWholeWords(!!options.isWholeWords);
this._register(this.onSearchSubmit(() => {
this.searchInput.inputBox.addToHistory();
}));
this._register(this.searchInput.onCaseSensitiveKeyDown((keyboardEvent: IKeyboardEvent) => this.onCaseSensitiveKeyDown(keyboardEvent)));
this._register(this.searchInput.onRegexKeyDown((keyboardEvent: IKeyboardEvent) => this.onRegexKeyDown(keyboardEvent)));
this._register(this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged()));
@ -464,7 +462,7 @@ export class SearchWidget extends Widget {
}
else if (keyboardEvent.equals(KeyCode.Escape)) {
this._onSearchCancel.fire();
this._onSearchCancel.fire({ focus: true });
keyboardEvent.preventDefault();
}
@ -484,6 +482,18 @@ export class SearchWidget extends Widget {
else if (keyboardEvent.equals(KeyCode.DownArrow)) {
stopPropagationForMultiLineDownwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
}
if ((keyboardEvent.browserEvent.key.length === 1 && !(keyboardEvent.ctrlKey || keyboardEvent.metaKey) ||
keyboardEvent.equals(KeyCode.Backspace) ||
keyboardEvent.equals(KeyCode.UpArrow) ||
keyboardEvent.equals(KeyCode.DownArrow))
&& this.searchConfiguration.searchOnType) {
// Check to see if this input changes the query, being either a printable key (`key` is length 1, and not modified), or backspace, or history scroll
// If so, trigger a new search eventually, and preemptively cancel the old one as it's results will soon be discarded anyways.
this._onSearchCancel.fire({ focus: false });
this._searchDelayer.trigger((() => this.submitSearch()));
}
}
private onCaseSensitiveKeyDown(keyboardEvent: IKeyboardEvent) {

View file

@ -20,7 +20,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
import { ReplacePattern } from 'vs/workbench/services/search/common/replace';
import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange } from 'vs/workbench/services/search/common/search';
import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange } from 'vs/workbench/services/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
@ -28,6 +28,7 @@ import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers';
import { withNullAsUndefined } from 'vs/base/common/types';
import { memoize } from 'vs/base/common/decorators';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class Match {
@ -906,6 +907,7 @@ export class SearchModel extends Disposable {
constructor(
@ISearchService private readonly searchService: ISearchService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
@ -1019,7 +1021,8 @@ export class SearchModel extends Disposable {
"options": { "${inline}": [ "${IPatternInfo}" ] },
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
"scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"searchOnTypeEnabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('searchResultsShown', {
@ -1028,7 +1031,8 @@ export class SearchModel extends Disposable {
options,
duration,
type: stats && stats.type,
scheme
scheme,
searchOnTypeEnabled: this.configurationService.getValue<ISearchConfigurationProperties>('search').searchOnType
});
return completed;
}

View file

@ -154,6 +154,9 @@ suite('SearchModel', () => {
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))),
aRawMatch('file://c:/2',
new TextSearchMatch('preview 2', lineOneRange))];
const config = new TestConfigurationService();
config.setUserConfiguration('search', { searchOnType: true });
instantiationService.stub(IConfigurationService, config);
instantiationService.stub(ISearchService, searchServiceWithResults(results));
const testObject: SearchModel = instantiationService.createInstance(SearchModel);

View file

@ -329,6 +329,8 @@ export interface ISearchConfigurationProperties {
actionsPosition: 'auto' | 'right';
maintainFileSearchCache: boolean;
collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand';
searchOnType: boolean;
searchOnTypeDebouncePeriod: number;
}
export interface ISearchConfiguration extends IFilesConfiguration {