-
Notifications
You must be signed in to change notification settings - Fork 31k
/
Copy pathpickerQuickAccess.ts
377 lines (318 loc) · 12.1 KB
/
pickerQuickAccess.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { timeout } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess';
import { isFunction } from 'vs/base/common/types';
export enum TriggerAction {
/**
* Do nothing after the button was clicked.
*/
NO_ACTION,
/**
* Close the picker.
*/
CLOSE_PICKER,
/**
* Update the results of the picker.
*/
REFRESH_PICKER,
/**
* Remove the item from the picker.
*/
REMOVE_ITEM
}
export interface IPickerQuickAccessItem extends IQuickPickItem {
/**
* A method that will be executed when the pick item is accepted from
* the picker. The picker will close automatically before running this.
*
* @param keyMods the state of modifier keys when the item was accepted.
* @param event the underlying event that caused the accept to trigger.
*/
accept?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void;
/**
* A method that will be executed when a button of the pick item was
* clicked on.
*
* @param buttonIndex index of the button of the item that
* was clicked.
*
* @param the state of modifier keys when the button was triggered.
*
* @returns a value that indicates what should happen after the trigger
* which can be a `Promise` for long running operations.
*/
trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise<TriggerAction>;
}
export interface IPickerQuickAccessProviderOptions<T extends IPickerQuickAccessItem> {
/**
* Enables support for opening picks in the background via gesture.
*/
readonly canAcceptInBackground?: boolean;
/**
* Enables to show a pick entry when no results are returned from a search.
*/
readonly noResultsPick?: T | ((filter: string) => T);
}
export type Pick<T> = T | IQuickPickSeparator;
export type PicksWithActive<T> = { items: readonly Pick<T>[]; active?: T };
export type Picks<T> = readonly Pick<T>[] | PicksWithActive<T>;
export type FastAndSlowPicks<T> = {
/**
* Picks that will show instantly or after a short delay
* based on the `mergeDelay` property to reduce flicker.
*/
readonly picks: Picks<T>;
/**
* Picks that will show after they have been resolved.
*/
readonly additionalPicks: Promise<Picks<T>>;
/**
* A delay in milliseconds to wait before showing the
* `picks` to give a chance to merge with `additionalPicks`
* for reduced flicker.
*/
readonly mergeDelay?: number;
};
function isPicksWithActive<T>(obj: unknown): obj is PicksWithActive<T> {
const candidate = obj as PicksWithActive<T>;
return Array.isArray(candidate.items);
}
function isFastAndSlowPicks<T>(obj: unknown): obj is FastAndSlowPicks<T> {
const candidate = obj as FastAndSlowPicks<T>;
return !!candidate.picks && candidate.additionalPicks instanceof Promise;
}
export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem> extends Disposable implements IQuickAccessProvider {
constructor(private prefix: string, protected options?: IPickerQuickAccessProviderOptions<T>) {
super();
}
provide(picker: IQuickPick<T>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
const disposables = new DisposableStore();
// Apply options if any
picker.canAcceptInBackground = !!this.options?.canAcceptInBackground;
// Disable filtering & sorting, we control the results
picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false;
// Set initial picks and update on type
let picksCts: CancellationTokenSource | undefined = undefined;
const picksDisposable = disposables.add(new MutableDisposable());
const updatePickerItems = async () => {
const picksDisposables = picksDisposable.value = new DisposableStore();
// Cancel any previous ask for picks and busy
picksCts?.dispose(true);
picker.busy = false;
// Create new cancellation source for this run
picksCts = new CancellationTokenSource(token);
// Collect picks and support both long running and short or combined
const picksToken = picksCts.token;
const picksFilter = picker.value.substr(this.prefix.length).trim();
const providedPicks = this._getPicks(picksFilter, picksDisposables, picksToken, runOptions);
const applyPicks = (picks: Picks<T>, skipEmpty?: boolean): boolean => {
let items: readonly Pick<T>[];
let activeItem: T | undefined = undefined;
if (isPicksWithActive(picks)) {
items = picks.items;
activeItem = picks.active;
} else {
items = picks;
}
if (items.length === 0) {
if (skipEmpty) {
return false;
}
// We show the no results pick if we have no input to prevent completely empty pickers #172613
if ((picksFilter.length > 0 || picker.hideInput) && this.options?.noResultsPick) {
if (isFunction(this.options.noResultsPick)) {
items = [this.options.noResultsPick(picksFilter)];
} else {
items = [this.options.noResultsPick];
}
}
}
picker.items = items;
if (activeItem) {
picker.activeItems = [activeItem];
}
return true;
};
const applyFastAndSlowPicks = async (fastAndSlowPicks: FastAndSlowPicks<T>): Promise<void> => {
let fastPicksApplied = false;
let slowPicksApplied = false;
await Promise.all([
// Fast Picks: if `mergeDelay` is configured, in order to reduce
// amount of flicker, we race against the slow picks over some delay
// and then set the fast picks.
// If the slow picks are faster, we reduce the flicker by only
// setting the items once.
(async () => {
if (typeof fastAndSlowPicks.mergeDelay === 'number') {
await timeout(fastAndSlowPicks.mergeDelay);
if (picksToken.isCancellationRequested) {
return;
}
}
if (!slowPicksApplied) {
fastPicksApplied = applyPicks(fastAndSlowPicks.picks, true /* skip over empty to reduce flicker */);
}
})(),
// Slow Picks: we await the slow picks and then set them at
// once together with the fast picks, but only if we actually
// have additional results.
(async () => {
picker.busy = true;
try {
const awaitedAdditionalPicks = await fastAndSlowPicks.additionalPicks;
if (picksToken.isCancellationRequested) {
return;
}
let picks: readonly Pick<T>[];
let activePick: Pick<T> | undefined = undefined;
if (isPicksWithActive(fastAndSlowPicks.picks)) {
picks = fastAndSlowPicks.picks.items;
activePick = fastAndSlowPicks.picks.active;
} else {
picks = fastAndSlowPicks.picks;
}
let additionalPicks: readonly Pick<T>[];
let additionalActivePick: Pick<T> | undefined = undefined;
if (isPicksWithActive(awaitedAdditionalPicks)) {
additionalPicks = awaitedAdditionalPicks.items;
additionalActivePick = awaitedAdditionalPicks.active;
} else {
additionalPicks = awaitedAdditionalPicks;
}
if (additionalPicks.length > 0 || !fastPicksApplied) {
// If we do not have any activePick or additionalActivePick
// we try to preserve the currently active pick from the
// fast results. This fixes an issue where the user might
// have made a pick active before the additional results
// kick in.
// See https://github.com/microsoft/vscode/issues/102480
let fallbackActivePick: Pick<T> | undefined = undefined;
if (!activePick && !additionalActivePick) {
const fallbackActivePickCandidate = picker.activeItems[0];
if (fallbackActivePickCandidate && picks.indexOf(fallbackActivePickCandidate) !== -1) {
fallbackActivePick = fallbackActivePickCandidate;
}
}
applyPicks({
items: [...picks, ...additionalPicks],
active: activePick || additionalActivePick || fallbackActivePick
});
}
} finally {
if (!picksToken.isCancellationRequested) {
picker.busy = false;
}
slowPicksApplied = true;
}
})()
]);
};
// No Picks
if (providedPicks === null) {
// Ignore
}
// Fast and Slow Picks
else if (isFastAndSlowPicks(providedPicks)) {
await applyFastAndSlowPicks(providedPicks);
}
// Fast Picks
else if (!(providedPicks instanceof Promise)) {
applyPicks(providedPicks);
}
// Slow Picks
else {
picker.busy = true;
try {
const awaitedPicks = await providedPicks;
if (picksToken.isCancellationRequested) {
return;
}
if (isFastAndSlowPicks(awaitedPicks)) {
await applyFastAndSlowPicks(awaitedPicks);
} else {
applyPicks(awaitedPicks);
}
} finally {
if (!picksToken.isCancellationRequested) {
picker.busy = false;
}
}
}
};
disposables.add(picker.onDidChangeValue(() => updatePickerItems()));
updatePickerItems();
// Accept the pick on accept and hide picker
disposables.add(picker.onDidAccept(event => {
const [item] = picker.selectedItems;
if (typeof item?.accept === 'function') {
if (!event.inBackground) {
picker.hide(); // hide picker unless we accept in background
}
item.accept(picker.keyMods, event);
}
}));
// Trigger the pick with button index if button triggered
disposables.add(picker.onDidTriggerItemButton(async ({ button, item }) => {
if (typeof item.trigger === 'function') {
const buttonIndex = item.buttons?.indexOf(button) ?? -1;
if (buttonIndex >= 0) {
const result = item.trigger(buttonIndex, picker.keyMods);
const action = (typeof result === 'number') ? result : await result;
if (token.isCancellationRequested) {
return;
}
switch (action) {
case TriggerAction.NO_ACTION:
break;
case TriggerAction.CLOSE_PICKER:
picker.hide();
break;
case TriggerAction.REFRESH_PICKER:
updatePickerItems();
break;
case TriggerAction.REMOVE_ITEM: {
const index = picker.items.indexOf(item);
if (index !== -1) {
const items = picker.items.slice();
const removed = items.splice(index, 1);
const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]);
const keepScrollPositionBefore = picker.keepScrollPosition;
picker.keepScrollPosition = true;
picker.items = items;
if (activeItems) {
picker.activeItems = activeItems;
}
picker.keepScrollPosition = keepScrollPositionBefore;
}
break;
}
}
}
}
}));
return disposables;
}
/**
* Returns an array of picks and separators as needed. If the picks are resolved
* long running, the provided cancellation token should be used to cancel the
* operation when the token signals this.
*
* The implementor is responsible for filtering and sorting the picks given the
* provided `filter`.
*
* @param filter a filter to apply to the picks.
* @param disposables can be used to register disposables that should be cleaned
* up when the picker closes.
* @param token for long running tasks, implementors need to check on cancellation
* through this token.
* @returns the picks either directly, as promise or combined fast and slow results.
* Pickers can return `null` to signal that no change in picks is needed.
*/
protected abstract _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): Picks<T> | Promise<Picks<T> | FastAndSlowPicks<T>> | FastAndSlowPicks<T> | null;
}