mirror of
https://github.com/Microsoft/vscode
synced 2024-11-05 18:29:38 +00:00
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:
parent
73d9d76ebd
commit
43fc44efa6
8 changed files with 58 additions and 16 deletions
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue