Skip to content

Commit dda6a4c

Browse files
author
Benjamin Pasero
committed
quick access - allow to restore an active item
Use this to make the file/symbol result active again when narrowing out of a editor symbol search.
1 parent ec90e96 commit dda6a4c

3 files changed

Lines changed: 101 additions & 33 deletions

File tree

src/vs/platform/quickinput/browser/pickerQuickAccess.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ export interface IPickerQuickAccessProviderOptions {
6363
canAcceptInBackground?: boolean;
6464
}
6565

66-
export type FastAndSlowPicksType<T> = { picks: Array<T | IQuickPickSeparator>, additionalPicks: Promise<Array<T | IQuickPickSeparator>> };
66+
export type Pick<T> = T | IQuickPickSeparator;
67+
export type Picks<T> = Array<Pick<T>> | { items: Array<Pick<T>>, active?: T };
68+
export type FastAndSlowPicks<T> = { picks: Picks<T>, additionalPicks: Promise<Picks<T>> };
6769

68-
function isFastAndSlowPicksType<T>(obj: unknown): obj is FastAndSlowPicksType<T> {
69-
const candidate = obj as FastAndSlowPicksType<T>;
70+
function isFastAndSlowPicks<T>(obj: unknown): obj is FastAndSlowPicks<T> {
71+
const candidate = obj as FastAndSlowPicks<T>;
7072

71-
return Array.isArray(candidate.picks) && candidate.additionalPicks instanceof Promise;
73+
return !!candidate.picks && candidate.additionalPicks instanceof Promise;
7274
}
7375

7476
export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem> extends Disposable implements IQuickAccessProvider {
@@ -103,15 +105,26 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
103105

104106
// Collect picks and support both long running and short or combined
105107
const picksToken = picksCts.token;
106-
const res = this.getPicks(picker.value.substr(this.prefix.length).trim(), picksDisposables, picksToken);
108+
const providedPicks = this.getPicks(picker.value.substr(this.prefix.length).trim(), picksDisposables, picksToken);
109+
110+
function applyPicks(picks: Picks<T>): void {
111+
if (Array.isArray(picks)) {
112+
picker.items = picks;
113+
} else {
114+
picker.items = picks.items;
115+
if (picks.active) {
116+
picker.activeItems = [picks.active];
117+
}
118+
}
119+
}
107120

108121
// No Picks
109-
if (res === null) {
122+
if (providedPicks === null) {
110123
// Ignore
111124
}
112125

113126
// Fast and Slow Picks
114-
else if (isFastAndSlowPicksType(res)) {
127+
else if (isFastAndSlowPicks(providedPicks)) {
115128
let fastPicksHandlerDone = false;
116129
let slowPicksHandlerDone = false;
117130

@@ -129,7 +142,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
129142
}
130143

131144
if (!slowPicksHandlerDone) {
132-
picker.items = res.picks;
145+
applyPicks(providedPicks.picks);
133146
}
134147
} finally {
135148
fastPicksHandlerDone = true;
@@ -142,13 +155,34 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
142155
(async () => {
143156
picker.busy = true;
144157
try {
145-
const additionalPicks = await res.additionalPicks;
158+
const awaitedAdditionalPicks = await providedPicks.additionalPicks;
146159
if (picksToken.isCancellationRequested) {
147160
return;
148161
}
149162

163+
let picks: Array<Pick<T>>;
164+
let activePick: Pick<T> | undefined = undefined;
165+
if (Array.isArray(providedPicks.picks)) {
166+
picks = providedPicks.picks;
167+
} else {
168+
picks = providedPicks.picks.items;
169+
activePick = providedPicks.picks.active;
170+
}
171+
172+
let additionalPicks: Array<Pick<T>>;
173+
let additionalActivePick: Pick<T> | undefined = undefined;
174+
if (Array.isArray(awaitedAdditionalPicks)) {
175+
additionalPicks = awaitedAdditionalPicks;
176+
} else {
177+
additionalPicks = awaitedAdditionalPicks.items;
178+
additionalActivePick = awaitedAdditionalPicks.active;
179+
}
180+
150181
if (additionalPicks.length > 0 || !fastPicksHandlerDone) {
151-
picker.items = [...res.picks, ...additionalPicks];
182+
applyPicks({
183+
items: [...picks, ...additionalPicks],
184+
active: activePick || additionalActivePick
185+
});
152186
}
153187
} finally {
154188
if (!picksToken.isCancellationRequested) {
@@ -162,20 +196,20 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
162196
}
163197

164198
// Fast Picks
165-
else if (Array.isArray(res)) {
166-
picker.items = res;
199+
else if (!(providedPicks instanceof Promise)) {
200+
applyPicks(providedPicks);
167201
}
168202

169203
// Slow Picks
170204
else {
171205
picker.busy = true;
172206
try {
173-
const items = await res;
207+
const awaitedPicks = await providedPicks;
174208
if (picksToken.isCancellationRequested) {
175209
return;
176210
}
177211

178-
picker.items = items;
212+
applyPicks(awaitedPicks);
179213
} finally {
180214
if (!picksToken.isCancellationRequested) {
181215
picker.busy = false;
@@ -251,5 +285,5 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
251285
* @returns the picks either directly, as promise or combined fast and slow results.
252286
* Pickers can return `null` to signal that no change in picks is needed.
253287
*/
254-
protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array<T | IQuickPickSeparator> | Promise<Array<T | IQuickPickSeparator>> | FastAndSlowPicksType<T> | null;
288+
protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Picks<T> | Promise<Picks<T>> | FastAndSlowPicks<T> | null;
255289
}

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

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import 'vs/css!./media/anythingQuickAccess';
7-
import { IQuickPickSeparator, IQuickInputButton, IKeyMods, quickPickItemScorerAccessor, QuickPickItemScorerAccessor, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
8-
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction, FastAndSlowPicksType } from 'vs/platform/quickinput/browser/pickerQuickAccess';
7+
import { IQuickInputButton, IKeyMods, quickPickItemScorerAccessor, QuickPickItemScorerAccessor, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
8+
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction, FastAndSlowPicks, Picks } from 'vs/platform/quickinput/browser/pickerQuickAccess';
99
import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem, ScorerCache } from 'vs/base/common/fuzzyScorer';
1010
import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
1111
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -88,7 +88,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
8888
lastOriginalFilter: string | undefined = undefined;
8989
lastFilter: string | undefined = undefined;
9090
lastRange: IRange | undefined = undefined;
91+
9192
lastActiveGlobalPick: IAnythingQuickPickItem | undefined = undefined;
93+
lastActiveEditorSymbolPick: IAnythingQuickPickItem | undefined = undefined;
9294

9395
isQuickNavigating: boolean | undefined = undefined;
9496

@@ -117,6 +119,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
117119
this.lastFilter = undefined;
118120
this.lastRange = undefined;
119121
this.lastActiveGlobalPick = undefined;
122+
this.lastActiveEditorSymbolPick = undefined;
120123
this.editorViewState = undefined;
121124
}
122125

@@ -240,7 +243,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
240243
return toDisposable(() => this.clearDecorations(activeEditorControl));
241244
}
242245

243-
protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> | FastAndSlowPicksType<IAnythingQuickPickItem> | null {
246+
protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): Promise<Picks<IAnythingQuickPickItem>> | FastAndSlowPicks<IAnythingQuickPickItem> | null {
244247

245248
// Find a suitable range from the pattern looking for ":", "#" or ","
246249
// unless we have the `@` editor symbol character inside the filter
@@ -269,16 +272,24 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
269272
this.pickState.lastOriginalFilter = originalFilter;
270273
this.pickState.lastFilter = filter;
271274

272-
// Remember last active pick (unless editor symbol)
275+
// Remember last active pick (global or editor symbol)
273276
const activePick = this.pickState.picker?.activeItems[0];
274-
if (activePick && !isEditorSymbolQuickPickItem(activePick)) {
275-
this.pickState.lastActiveGlobalPick = activePick;
277+
if (activePick) {
278+
if (isEditorSymbolQuickPickItem(activePick)) {
279+
// remember the editor symbol pick, but do not unset the
280+
// global pick as we can use it later to restore it when
281+
// the user narrows out of the editor symbol search
282+
this.pickState.lastActiveEditorSymbolPick = activePick;
283+
} else {
284+
this.pickState.lastActiveGlobalPick = activePick;
285+
this.pickState.lastActiveEditorSymbolPick = undefined;
286+
}
276287
}
277288

278289
return this.doGetPicks(filter, disposables, token);
279290
}
280291

281-
private doGetPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> | FastAndSlowPicksType<IAnythingQuickPickItem> | null {
292+
private doGetPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Picks<IAnythingQuickPickItem>> | FastAndSlowPicks<IAnythingQuickPickItem> | null {
282293
const query = prepareQuery(filter);
283294

284295
// Return early if we have editor symbol picks. We support this by:
@@ -289,22 +300,32 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
289300
return editorSymbolPicks;
290301
}
291302

303+
// If we have a known last active editor symbol pick, we try to restore
304+
// the last global pick to support the case of narrowing out from a
305+
// editor symbol search back into the global search
306+
let activeGlobalPick: IAnythingQuickPickItem | undefined = undefined;
307+
if (this.pickState.lastActiveEditorSymbolPick) {
308+
activeGlobalPick = this.pickState.lastActiveGlobalPick;
309+
}
310+
292311
// Otherwise return normally with history and file/symbol results
293312
const historyEditorPicks = this.getEditorHistoryPicks(query);
294313

295314
return {
296315

297316
// Fast picks: editor history
298-
picks:
299-
(this.pickState.isQuickNavigating || historyEditorPicks.length === 0) ?
317+
picks: {
318+
items: (this.pickState.isQuickNavigating || historyEditorPicks.length === 0) ?
300319
historyEditorPicks :
301320
[
302321
{ type: 'separator', label: localize('recentlyOpenedSeparator', "recently opened") },
303322
...historyEditorPicks
304323
],
324+
active: activeGlobalPick ? this.findPick(historyEditorPicks, activeGlobalPick) : undefined
325+
},
305326

306327
// Slow picks: files and symbols
307-
additionalPicks: (async (): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> => {
328+
additionalPicks: (async (): Promise<Picks<IAnythingQuickPickItem>> => {
308329

309330
// Exclude any result that is already present in editor history
310331
const additionalPicksExcludes = new ResourceMap<boolean>();
@@ -319,14 +340,27 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
319340
return [];
320341
}
321342

322-
return additionalPicks.length > 0 ? [
323-
{ type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") },
324-
...additionalPicks
325-
] : [];
343+
return {
344+
items: additionalPicks.length > 0 ? [
345+
{ type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") },
346+
...additionalPicks
347+
] : [],
348+
active: activeGlobalPick ? this.findPick(additionalPicks, activeGlobalPick) : undefined
349+
};
326350
})()
327351
};
328352
}
329353

354+
private findPick(picks: IAnythingQuickPickItem[], candidate: IAnythingQuickPickItem): IAnythingQuickPickItem | undefined {
355+
for (const pick of picks) {
356+
if (isEqual(pick.resource, candidate.resource)) {
357+
return pick;
358+
}
359+
}
360+
361+
return undefined;
362+
}
363+
330364
private async getAdditionalPicks(query: IPreparedQuery, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
331365

332366
// Resolve file and symbol picks (if enabled)
@@ -636,7 +670,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
636670

637671
private readonly editorSymbolsQuickAccess = this.instantiationService.createInstance(GotoSymbolQuickAccessProvider);
638672

639-
private getEditorSymbolPicks(query: IPreparedQuery, disposables: DisposableStore, token: CancellationToken): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> | null {
673+
private getEditorSymbolPicks(query: IPreparedQuery, disposables: DisposableStore, token: CancellationToken): Promise<Picks<IAnythingQuickPickItem>> | null {
640674
const filter = query.original.split(GotoSymbolQuickAccessProvider.PREFIX)[1]?.trim();
641675
if (typeof filter !== 'string') {
642676
return null; // we need to be searched for editor symbols via `@`
@@ -655,7 +689,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
655689
return this.doGetEditorSymbolPicks(activeGlobalPick, activeGlobalResource, filter, disposables, token);
656690
}
657691

658-
private async doGetEditorSymbolPicks(activeGlobalPick: IAnythingQuickPickItem, activeGlobalResource: URI, filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> {
692+
private async doGetEditorSymbolPicks(activeGlobalPick: IAnythingQuickPickItem, activeGlobalResource: URI, filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Picks<IAnythingQuickPickItem>> {
659693

660694
// Bring the editor to front to review symbols to go to
661695
try {

src/vs/workbench/test/browser/quickAccess.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
1212
import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
1313
import { DisposableStore, toDisposable, IDisposable } from 'vs/base/common/lifecycle';
1414
import { timeout } from 'vs/base/common/async';
15-
import { PickerQuickAccessProvider, FastAndSlowPicksType } from 'vs/platform/quickinput/browser/pickerQuickAccess';
15+
import { PickerQuickAccessProvider, FastAndSlowPicks } from 'vs/platform/quickinput/browser/pickerQuickAccess';
1616

1717
suite('QuickAccess', () => {
1818

@@ -239,7 +239,7 @@ suite('QuickAccess', () => {
239239
super('bothFastAndSlow');
240240
}
241241

242-
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType<IQuickPickItem> {
242+
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicks<IQuickPickItem> {
243243
fastAndSlowProviderCalled = true;
244244

245245
return {

0 commit comments

Comments
 (0)