Skip to content

Commit 41b4581

Browse files
committed
Implement preview on highlighting quickpick in quick search
Fixes #191259
1 parent 6e9b9a4 commit 41b4581

File tree

2 files changed

+152
-96
lines changed

2 files changed

+152
-96
lines changed

src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts

+102-87
Original file line numberDiff line numberDiff line change
@@ -71,95 +71,128 @@ function isEditorSymbolQuickPickItem(pick?: IAnythingQuickPickItem): pick is IEd
7171
return !!candidate?.range && !!candidate.resource;
7272
}
7373

74-
export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnythingQuickPickItem> {
74+
export class PickState {
75+
picker: IQuickPick<IAnythingQuickPickItem> | undefined = undefined;
7576

76-
static PREFIX = '';
77+
editorViewState: {
78+
editor: EditorInput;
79+
group: IEditorGroup;
80+
state: ICodeEditorViewState | IDiffEditorViewState | undefined;
81+
} | undefined = undefined;
7782

78-
private static readonly NO_RESULTS_PICK: IAnythingQuickPickItem = {
79-
label: localize('noAnythingResults', "No matching results")
80-
};
83+
scorerCache: FuzzyScorerCache = Object.create(null);
84+
fileQueryCache: FileQueryCacheState | undefined = undefined;
8185

82-
private static readonly MAX_RESULTS = 512;
86+
lastOriginalFilter: string | undefined = undefined;
87+
lastFilter: string | undefined = undefined;
88+
lastRange: IRange | undefined = undefined;
8389

84-
private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
90+
lastGlobalPicks: PicksWithActive<IAnythingQuickPickItem> | undefined = undefined;
8591

86-
private static SYMBOL_PICKS_MERGE_DELAY = 200; // allow some time to merge fast and slow picks to reduce flickering
92+
isQuickNavigating: boolean | undefined = undefined;
8793

88-
private readonly pickState = new class {
94+
private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
8995

90-
picker: IQuickPick<IAnythingQuickPickItem> | undefined = undefined;
96+
constructor(
97+
private readonly instantiationService: IInstantiationService,
98+
private readonly contextService: IWorkspaceContextService,
99+
private readonly searchService: ISearchService,
100+
private readonly editorService: IEditorService) { }
101+
102+
set(picker: IQuickPick<IAnythingQuickPickItem>): void {
103+
104+
// Picker for this run
105+
this.picker = picker;
106+
Event.once(picker.onDispose)(() => {
107+
if (picker === this.picker) {
108+
this.picker = undefined; // clear the picker when disposed to not keep it in memory for too long
109+
}
110+
});
91111

92-
editorViewState: {
93-
editor: EditorInput;
94-
group: IEditorGroup;
95-
state: ICodeEditorViewState | IDiffEditorViewState | undefined;
96-
} | undefined = undefined;
112+
// Caches
113+
const isQuickNavigating = !!picker.quickNavigate;
114+
if (!isQuickNavigating) {
115+
this.fileQueryCache = this.createFileQueryCache();
116+
this.scorerCache = Object.create(null);
117+
}
97118

98-
scorerCache: FuzzyScorerCache = Object.create(null);
99-
fileQueryCache: FileQueryCacheState | undefined = undefined;
119+
// Other
120+
this.isQuickNavigating = isQuickNavigating;
121+
this.lastOriginalFilter = undefined;
122+
this.lastFilter = undefined;
123+
this.lastRange = undefined;
124+
this.lastGlobalPicks = undefined;
125+
this.editorViewState = undefined;
126+
}
100127

101-
lastOriginalFilter: string | undefined = undefined;
102-
lastFilter: string | undefined = undefined;
103-
lastRange: IRange | undefined = undefined;
128+
rememberEditorViewState(): void {
129+
if (this.editorViewState) {
130+
return; // return early if already done
131+
}
104132

105-
lastGlobalPicks: PicksWithActive<IAnythingQuickPickItem> | undefined = undefined;
133+
const activeEditorPane = this.editorService.activeEditorPane;
134+
if (activeEditorPane) {
135+
this.editorViewState = {
136+
group: activeEditorPane.group,
137+
editor: activeEditorPane.input,
138+
state: getIEditor(activeEditorPane.getControl())?.saveViewState() ?? undefined,
139+
};
140+
}
141+
}
106142

107-
isQuickNavigating: boolean | undefined = undefined;
143+
getFileQueryOptions(input: { filePattern?: string; cacheKey?: string; maxResults?: number }): IFileQueryBuilderOptions {
144+
return {
145+
_reason: 'openFileHandler', // used for telemetry - do not change
146+
extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
147+
filePattern: input.filePattern || '',
148+
cacheKey: input.cacheKey,
149+
maxResults: input.maxResults || 0,
150+
sortByScore: true
151+
};
152+
}
108153

109-
constructor(private readonly provider: AnythingQuickAccessProvider, private readonly editorService: IEditorService) { }
154+
async restoreEditorViewState(): Promise<void> {
155+
if (this.editorViewState) {
156+
const options: IEditorOptions = {
157+
viewState: this.editorViewState.state,
158+
preserveFocus: true /* import to not close the picker as a result */
159+
};
110160

111-
set(picker: IQuickPick<IAnythingQuickPickItem>): void {
161+
await this.editorViewState.group.openEditor(this.editorViewState.editor, options);
162+
}
163+
}
112164

113-
// Picker for this run
114-
this.picker = picker;
115-
Event.once(picker.onDispose)(() => {
116-
if (picker === this.picker) {
117-
this.picker = undefined; // clear the picker when disposed to not keep it in memory for too long
118-
}
119-
});
120165

121-
// Caches
122-
const isQuickNavigating = !!picker.quickNavigate;
123-
if (!isQuickNavigating) {
124-
this.fileQueryCache = this.provider.createFileQueryCache();
125-
this.scorerCache = Object.create(null);
126-
}
166+
private createFileQueryCache(): FileQueryCacheState {
167+
return new FileQueryCacheState(
168+
cacheKey => this.fileQueryBuilder.file(this.contextService.getWorkspace().folders, this.getFileQueryOptions({ cacheKey })),
169+
query => this.searchService.fileSearch(query),
170+
cacheKey => this.searchService.clearCache(cacheKey),
171+
this.fileQueryCache
172+
).load();
173+
}
174+
}
127175

128-
// Other
129-
this.isQuickNavigating = isQuickNavigating;
130-
this.lastOriginalFilter = undefined;
131-
this.lastFilter = undefined;
132-
this.lastRange = undefined;
133-
this.lastGlobalPicks = undefined;
134-
this.editorViewState = undefined;
135-
}
176+
export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnythingQuickPickItem> {
136177

137-
rememberEditorViewState(): void {
138-
if (this.editorViewState) {
139-
return; // return early if already done
140-
}
178+
static PREFIX = '';
141179

142-
const activeEditorPane = this.editorService.activeEditorPane;
143-
if (activeEditorPane) {
144-
this.editorViewState = {
145-
group: activeEditorPane.group,
146-
editor: activeEditorPane.input,
147-
state: getIEditor(activeEditorPane.getControl())?.saveViewState() ?? undefined,
148-
};
149-
}
150-
}
180+
private static readonly NO_RESULTS_PICK: IAnythingQuickPickItem = {
181+
label: localize('noAnythingResults', "No matching results")
182+
};
151183

152-
async restoreEditorViewState(): Promise<void> {
153-
if (this.editorViewState) {
154-
const options: IEditorOptions = {
155-
viewState: this.editorViewState.state,
156-
preserveFocus: true /* import to not close the picker as a result */
157-
};
184+
private static readonly MAX_RESULTS = 512;
158185

159-
await this.editorViewState.group.openEditor(this.editorViewState.editor, options);
160-
}
161-
}
162-
}(this, this.editorService);
186+
private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
187+
188+
private static SYMBOL_PICKS_MERGE_DELAY = 200; // allow some time to merge fast and slow picks to reduce flickering
189+
190+
private readonly pickState = new PickState(
191+
this.instantiationService,
192+
this.contextService,
193+
this.searchService,
194+
this.editorService
195+
);
163196

164197
get defaultFilterValue(): DefaultQuickAccessFilterValue | undefined {
165198
if (this.configuration.preserveInput) {
@@ -510,14 +543,6 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
510543

511544
private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
512545

513-
private createFileQueryCache(): FileQueryCacheState {
514-
return new FileQueryCacheState(
515-
cacheKey => this.fileQueryBuilder.file(this.contextService.getWorkspace().folders, this.getFileQueryOptions({ cacheKey })),
516-
query => this.searchService.fileSearch(query),
517-
cacheKey => this.searchService.clearCache(cacheKey),
518-
this.pickState.fileQueryCache
519-
).load();
520-
}
521546

522547
private async getFilePicks(query: IPreparedQuery, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
523548
if (!query.normalized) {
@@ -661,24 +686,14 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
661686
return this.searchService.fileSearch(
662687
this.fileQueryBuilder.file(
663688
this.contextService.getWorkspace().folders,
664-
this.getFileQueryOptions({
689+
this.pickState.getFileQueryOptions({
665690
filePattern,
666691
cacheKey: this.pickState.fileQueryCache?.cacheKey,
667692
maxResults: AnythingQuickAccessProvider.MAX_RESULTS
668693
})
669694
), token);
670695
}
671696

672-
private getFileQueryOptions(input: { filePattern?: string; cacheKey?: string; maxResults?: number }): IFileQueryBuilderOptions {
673-
return {
674-
_reason: 'openFileHandler', // used for telemetry - do not change
675-
extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
676-
filePattern: input.filePattern || '',
677-
cacheKey: input.cacheKey,
678-
maxResults: input.maxResults || 0,
679-
sortByScore: true
680-
};
681-
}
682697

683698
private async getAbsolutePathFileResult(query: IPreparedQuery, token: CancellationToken): Promise<URI | undefined> {
684699
if (!query.containsPathSeparator) {

src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts

+50-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
1717
import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService';
1818
import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
1919
import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess';
20-
import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
20+
import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput';
2121
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
2222
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
2323
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
@@ -27,7 +27,9 @@ import { SearchView, getEditorSelectionFromMatch } from 'vs/workbench/contrib/se
2727
import { IWorkbenchSearchConfiguration, getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
2828
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
2929
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
30-
import { IPatternInfo, ITextQuery, VIEW_ID } from 'vs/workbench/services/search/common/search';
30+
import { IPatternInfo, ISearchService, ITextQuery, VIEW_ID } from 'vs/workbench/services/search/common/search';
31+
import { PickState } from 'vs/workbench/contrib/search/browser/anythingQuickAccess';
32+
import { Event } from 'vs/base/common/event';
3133

3234
export const TEXT_SEARCH_QUICK_ACCESS_PREFIX = '%';
3335

@@ -42,9 +44,19 @@ const DEFAULT_TEXT_QUERY_BUILDER_OPTIONS: ITextQueryBuilderOptions = {
4244
const MAX_FILES_SHOWN = 30;
4345
const MAX_RESULTS_PER_FILE = 10;
4446

45-
export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuickAccessItem> {
47+
interface ITextSearchQuickAccessItem extends IPickerQuickAccessItem {
48+
match?: Match;
49+
}
50+
export class TextSearchQuickAccess extends PickerQuickAccessProvider<ITextSearchQuickAccessItem> {
4651
private queryBuilder: QueryBuilder;
4752
private searchModel: SearchModel;
53+
private navigatedAwayUsingPick = false;
54+
private readonly pickState = new PickState(
55+
this._instantiationService,
56+
this._contextService,
57+
this._searchService,
58+
this._editorService
59+
);
4860

4961
private _getTextQueryBuilderOptions(charsPerLine: number): ITextQueryBuilderOptions {
5062
return {
@@ -69,6 +81,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
6981
@ILabelService private readonly _labelService: ILabelService,
7082
@IViewsService private readonly _viewsService: IViewsService,
7183
@IConfigurationService private readonly _configurationService: IConfigurationService,
84+
@ISearchService private readonly _searchService: ISearchService
7285
) {
7386
super(TEXT_SEARCH_QUICK_ACCESS_PREFIX, { canAcceptInBackground: true, shouldSkipTrimPickFilter: true });
7487

@@ -81,17 +94,44 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
8194
super.dispose();
8295
}
8396

84-
override provide(picker: IQuickPick<IPickerQuickAccessItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
97+
override provide(picker: IQuickPick<ITextSearchQuickAccessItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
8598
const disposables = new DisposableStore();
8699
if (TEXT_SEARCH_QUICK_ACCESS_PREFIX.length < picker.value.length) {
87100
picker.valueSelection = [TEXT_SEARCH_QUICK_ACCESS_PREFIX.length, picker.value.length];
88101
}
89102
picker.customButton = true;
90103
picker.customLabel = '$(link-external)';
91-
picker.onDidCustom(() => {
104+
disposables.add(picker.onDidCustom(() => {
92105
this.moveToSearchViewlet(this.searchModel, undefined);
93106
picker.hide();
94-
});
107+
}));
108+
disposables.add(picker.onDidChangeActive(() => {
109+
const [item] = picker.activeItems;
110+
111+
if (item.match) {
112+
if (!this.navigatedAwayUsingPick) {
113+
// we must remember our curret view state to be able to restore
114+
this.pickState.rememberEditorViewState();
115+
this.navigatedAwayUsingPick = true;
116+
}
117+
// open it
118+
this._editorService.openEditor({
119+
resource: item.match.parent().resource,
120+
options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection: item.match.range() }
121+
});
122+
}
123+
}));
124+
125+
// Restore view state upon cancellation if we changed it
126+
// but only when the picker was closed via explicit user
127+
// gesture and not e.g. when focus was lost because that
128+
// could mean the user clicked into the editor directly.
129+
disposables.add(Event.once(picker.onDidHide)(({ reason }) => {
130+
if (reason === QuickInputHideReason.Gesture) {
131+
this.pickState.restoreEditorViewState();
132+
}
133+
}));
134+
95135
disposables.add(super.provide(picker, token, runOptions));
96136
disposables.add(picker.onDidHide(() => this.searchModel.searchResult.toggleHighlights(false)));
97137
disposables.add(picker.onDidAccept(() => this.searchModel.searchResult.toggleHighlights(false)));
@@ -164,11 +204,11 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
164204
}
165205
}
166206

167-
private _getPicksFromMatches(matches: FileMatch[], limit: number): (IQuickPickSeparator | IPickerQuickAccessItem)[] {
207+
private _getPicksFromMatches(matches: FileMatch[], limit: number): (IQuickPickSeparator | ITextSearchQuickAccessItem)[] {
168208
matches = matches.sort(searchComparer);
169209

170210
const files = matches.length > limit ? matches.slice(0, limit) : matches;
171-
const picks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = [];
211+
const picks: Array<ITextSearchQuickAccessItem | IQuickPickSeparator> = [];
172212

173213
for (let fileIndex = 0; fileIndex < matches.length; fileIndex++) {
174214
if (fileIndex === limit) {
@@ -245,7 +285,8 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
245285
trigger: (): TriggerAction => {
246286
this.moveToSearchViewlet(this.searchModel, element);
247287
return TriggerAction.CLOSE_PICKER;
248-
}
288+
},
289+
match: element
249290
});
250291
}
251292
}

0 commit comments

Comments
 (0)