quick access - allow a Promise<FastAndSlowPicks<T>> and adopt for commands (#180664)

* quick access - allow a `Promise<FastAndSlowPicks<T>>` and adopt for commands

* fix telemetry
This commit is contained in:
Benjamin Pasero 2023-04-24 06:41:25 +02:00 committed by GitHub
parent 25da7d524f
commit 3a69e153f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 91 deletions

View file

@ -169,7 +169,7 @@ export class AzureActiveDirectoryService {
"login" : {
"owner": "TylerLeonhardt",
"comment": "Used to determine the usage of the Microsoft Auth Provider.",
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." },
"accountType": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what account types are being used." }
}
*/

View file

@ -19,8 +19,8 @@ import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { alert } from 'vs/base/browser/ui/aria/aria';
interface IEditorLineDecoration {
rangeHighlightId: string;
overviewRulerDecorationId: string;
readonly rangeHighlightId: string;
readonly overviewRulerDecorationId: string;
}
export interface IEditorNavigationQuickAccessOptions {

View file

@ -37,10 +37,14 @@ export class StandaloneCommandsQuickAccessProvider extends AbstractEditorCommand
super({ showAlias: false }, instantiationService, keybindingService, commandService, telemetryService, dialogService);
}
protected getCommandPicks(): Array<ICommandQuickPick> {
protected async getCommandPicks(): Promise<Array<ICommandQuickPick>> {
return this.getCodeEditorCommandPicks();
}
protected hasAdditionalCommandPicks(): boolean {
return false;
}
protected async getAdditionalCommandPicks(): Promise<ICommandQuickPick[]> {
return [];
}

View file

@ -24,12 +24,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export interface ICommandQuickPick extends IPickerQuickAccessItem {
commandId: string;
commandAlias?: string;
readonly commandId: string;
readonly commandAlias?: string;
}
export interface ICommandsQuickAccessOptions extends IPickerQuickAccessProviderOptions<ICommandQuickPick> {
showAlias: boolean;
readonly showAlias: boolean;
suggestedCommandIds?: Set<string>;
}
@ -56,10 +56,10 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
this.options = options;
}
protected _getPicks(filter: string, _disposables: DisposableStore, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): Picks<ICommandQuickPick> | FastAndSlowPicks<ICommandQuickPick> {
protected async _getPicks(filter: string, _disposables: DisposableStore, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): Promise<Picks<ICommandQuickPick> | FastAndSlowPicks<ICommandQuickPick>> {
// Ask subclass for all command picks
const allCommandPicks = this.getCommandPicks(token);
const allCommandPicks = await this.getCommandPicks(token);
if (token.isCancellationRequested) {
return [];
@ -166,10 +166,20 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
commandPicks.push(this.toCommandPick(commandPick, runOptions));
}
if (!this.hasAdditionalCommandPicks(filter, token)) {
return commandPicks;
}
return {
picks: commandPicks,
additionalPicks: this.getAdditionalCommandPicks(allCommandPicks, filteredCommandPicks, filter, token)
.then(additionalCommandPicks => additionalCommandPicks.map(commandPick => this.toCommandPick(commandPick, runOptions)))
additionalPicks: (async (): Promise<Picks<ICommandQuickPick>> => {
const additionalCommandPicks = await this.getAdditionalCommandPicks(allCommandPicks, filteredCommandPicks, filter, token);
if (token.isCancellationRequested) {
return [];
}
return additionalCommandPicks.map(commandPick => this.toCommandPick(commandPick, runOptions));
})()
};
}
@ -177,6 +187,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
if (commandPick.type === 'separator') {
return commandPick;
}
const keybinding = this.keybindingService.lookupKeybinding(commandPick.commandId);
const ariaLabel = keybinding ?
localize('commandPickAriaLabelWithKeybinding', "{0}, {1}", commandPick.label, keybinding.getAriaLabel()) :
@ -210,27 +221,22 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
};
}
/**
* Subclasses to provide the actual command entries.
*/
protected abstract getCommandPicks(token: CancellationToken): Array<ICommandQuickPick>;
protected abstract getCommandPicks(token: CancellationToken): Promise<Array<ICommandQuickPick>>;
/**
* Subclasses to provide the actual command entries.
*/
protected abstract hasAdditionalCommandPicks(filter: string, token: CancellationToken): boolean;
protected abstract getAdditionalCommandPicks(allPicks: ICommandQuickPick[], picksSoFar: ICommandQuickPick[], filter: string, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>>;
}
interface ISerializedCommandHistory {
usesLRU?: boolean;
entries: { key: string; value: number }[];
readonly usesLRU?: boolean;
readonly entries: { key: string; value: number }[];
}
interface ICommandsQuickAccessConfiguration {
workbench: {
commandPalette: {
history: number;
preserveInput: boolean;
readonly workbench: {
readonly commandPalette: {
readonly history: number;
readonly preserveInput: boolean;
};
};
}

View file

@ -11,7 +11,7 @@ import { Extensions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuic
import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
interface IHelpQuickAccessPickItem extends IQuickPickItem {
prefix: string;
readonly prefix: string;
}
export class HelpQuickAccessProvider implements IQuickAccessProvider {
@ -75,4 +75,3 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
});
}
}

View file

@ -64,18 +64,37 @@ export interface IPickerQuickAccessProviderOptions<T extends IPickerQuickAccessI
/**
* Enables support for opening picks in the background via gesture.
*/
canAcceptInBackground?: boolean;
readonly canAcceptInBackground?: boolean;
/**
* Enables to show a pick entry when no results are returned from a search.
*/
noResultsPick?: T | ((filter: string) => T);
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: Picks<T>; additionalPicks: Promise<Picks<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>;
@ -91,8 +110,6 @@ function isFastAndSlowPicks<T>(obj: unknown): obj is FastAndSlowPicks<T> {
export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem> extends Disposable implements IQuickAccessProvider {
private static FAST_PICKS_RACE_DELAY = 200; // timeout before we accept fast results before slow results are present
constructor(private prefix: string, protected options?: IPickerQuickAccessProviderOptions<T>) {
super();
}
@ -158,51 +175,50 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
return true;
};
// No Picks
if (providedPicks === null) {
// Ignore
}
// Fast and Slow Picks
else if (isFastAndSlowPicks(providedPicks)) {
const applyFastAndSlowPicks = async (fastAndSlowPicks: FastAndSlowPicks<T>): Promise<void> => {
let fastPicksApplied = false;
let slowPicksApplied = false;
await Promise.all([
// Fast Picks: to reduce amount of flicker, we race against
// the slow picks over 500ms and then set the fast picks.
// If the slow picks are faster, we reduce the flicker by
// only setting the items once.
// 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 () => {
await timeout(PickerQuickAccessProvider.FAST_PICKS_RACE_DELAY);
if (picksToken.isCancellationRequested) {
return;
if (typeof fastAndSlowPicks.mergeDelay === 'number') {
await timeout(fastAndSlowPicks.mergeDelay);
if (picksToken.isCancellationRequested) {
return;
}
}
if (!slowPicksApplied) {
fastPicksApplied = applyPicks(providedPicks.picks, true /* skip over empty to reduce flicker */);
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 providedPicks.additionalPicks;
const awaitedAdditionalPicks = await fastAndSlowPicks.additionalPicks;
if (picksToken.isCancellationRequested) {
return;
}
let picks: readonly Pick<T>[];
let activePick: Pick<T> | undefined = undefined;
if (isPicksWithActive(providedPicks.picks)) {
picks = providedPicks.picks.items;
activePick = providedPicks.picks.active;
if (isPicksWithActive(fastAndSlowPicks.picks)) {
picks = fastAndSlowPicks.picks.items;
activePick = fastAndSlowPicks.picks.active;
} else {
picks = providedPicks.picks;
picks = fastAndSlowPicks.picks;
}
let additionalPicks: readonly Pick<T>[];
@ -243,6 +259,16 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
}
})()
]);
};
// No Picks
if (providedPicks === null) {
// Ignore
}
// Fast and Slow Picks
else if (isFastAndSlowPicks(providedPicks)) {
await applyFastAndSlowPicks(providedPicks);
}
// Fast Picks
@ -259,7 +285,11 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
return;
}
applyPicks(awaitedPicks);
if (isFastAndSlowPicks(awaitedPicks)) {
await applyFastAndSlowPicks(awaitedPicks);
} else {
applyPicks(awaitedPicks);
}
} finally {
if (!picksToken.isCancellationRequested) {
picker.busy = false;
@ -343,5 +373,5 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
* @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> | null;
protected abstract _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): Picks<T> | Promise<Picks<T> | FastAndSlowPicks<T>> | FastAndSlowPicks<T> | null;
}

View file

@ -20,9 +20,9 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
private readonly lastAcceptedPickerValues = new Map<IQuickAccessProviderDescriptor, string>();
private visibleQuickAccess: {
picker: IQuickPick<IQuickPickItem>;
descriptor: IQuickAccessProviderDescriptor | undefined;
value: string;
readonly picker: IQuickPick<IQuickPickItem>;
readonly descriptor: IQuickAccessProviderDescriptor | undefined;
readonly value: string;
} | undefined = undefined;
constructor(

View file

@ -14,14 +14,14 @@ import { Registry } from 'vs/platform/registry/common/platform';
* quick access.
*/
export interface IQuickAccessProviderRunOptions {
from?: string;
readonly from?: string;
}
/**
* The specific options for the AnythingQuickAccessProvider. Put here to share between layers.
*/
export interface AnythingQuickAccessProviderRunOptions extends IQuickAccessProviderRunOptions {
includeHelp?: boolean;
readonly includeHelp?: boolean;
}
export interface IQuickAccessOptions {
@ -29,25 +29,25 @@ export interface IQuickAccessOptions {
/**
* Allows to enable quick navigate support in quick input.
*/
quickNavigateConfiguration?: IQuickNavigateConfiguration;
readonly quickNavigateConfiguration?: IQuickNavigateConfiguration;
/**
* Allows to configure a different item activation strategy.
* By default the first item in the list will get activated.
*/
itemActivation?: ItemActivation;
readonly itemActivation?: ItemActivation;
/**
* Whether to take the input value as is and not restore it
* from any existing value if quick access is visible.
*/
preserveValue?: boolean;
readonly preserveValue?: boolean;
/**
* Provider specific options for this particular showing of the
* quick access.
*/
providerOptions?: IQuickAccessProviderRunOptions;
readonly providerOptions?: IQuickAccessProviderRunOptions;
}
export interface IQuickAccessController {
@ -114,12 +114,12 @@ export interface IQuickAccessProviderHelp {
* The prefix to show for the help entry. If not provided,
* the prefix used for registration will be taken.
*/
prefix?: string;
readonly prefix?: string;
/**
* A description text to help understand the intent of the provider.
*/
description: string;
readonly description: string;
/**
* The command to bring up this quick access provider.
@ -181,6 +181,7 @@ export interface IQuickAccessRegistry {
}
export class QuickAccessRegistry implements IQuickAccessRegistry {
private providers: IQuickAccessProviderDescriptor[] = [];
private defaultProvider: IQuickAccessProviderDescriptor | undefined = undefined;

View file

@ -17,18 +17,18 @@ export const defaultQuickAccessContextKeyValue = 'inFilesPicker';
export const defaultQuickAccessContext = ContextKeyExpr.and(inQuickPickContext, ContextKeyExpr.has(defaultQuickAccessContextKeyValue));
export interface IWorkbenchQuickAccessConfiguration {
workbench: {
commandPalette: {
history: number;
preserveInput: boolean;
experimental: {
suggestCommands: boolean;
useSemanticSimilarity: boolean;
readonly workbench: {
readonly commandPalette: {
readonly history: number;
readonly preserveInput: boolean;
readonly experimental: {
readonly suggestCommands: boolean;
readonly useSemanticSimilarity: boolean;
};
};
quickOpen: {
enableExperimentalNewVersion: boolean;
preserveInput: boolean;
readonly quickOpen: {
readonly enableExperimentalNewVersion: boolean;
readonly preserveInput: boolean;
};
};
}

View file

@ -7,9 +7,9 @@ import { localize } from 'vs/nls';
import { ICommandQuickPick, CommandsHistory } from 'vs/platform/quickinput/browser/commandsQuickAccess';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction, Action2 } from 'vs/platform/actions/common/actions';
// import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { CancellationToken } from 'vs/base/common/cancellation';
// import { timeout } from 'vs/base/common/async';
import { raceTimeout, timeout } from 'vs/base/common/async';
import { AbstractEditorCommandsQuickAccessProvider } from 'vs/editor/contrib/quickAccess/browser/commandsQuickAccess';
import { IEditor } from 'vs/editor/common/editorCommon';
import { Language } from 'vs/base/common/platform';
@ -34,25 +34,21 @@ import { stripIcons } from 'vs/base/common/iconLabels';
import { isFirefox } from 'vs/base/browser/browser';
import { IProductService } from 'vs/platform/product/common/productService';
import { ISemanticSimilarityService } from 'vs/workbench/services/semanticSimilarity/common/semanticSimilarityService';
import { timeout } from 'vs/base/common/async';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { ILogService } from 'vs/platform/log/common/log';
import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider {
private static SEMANTIC_SIMILARITY_MAX_PICKS = 3;
private static SEMANTIC_SIMILARITY_THRESHOLD = 0.8;
private static SEMANTIC_SIMILARITY_DEBOUNCE = 200;
// TODO: bring this back once we have a chosen strategy for FastAndSlowPicks where Fast is also Promise based
// If extensions are not yet registered, we wait for a little moment to give them
// a chance to register so that the complete set of commands shows up as result
// We do not want to delay functionality beyond that time though to keep the commands
// functional.
// private readonly extensionRegistrationRace = Promise.race([
// timeout(800),
// this.extensionService.whenInstalledExtensionsRegistered()
// ]);
private readonly extensionRegistrationRace = raceTimeout(this.extensionService.whenInstalledExtensionsRegistered(), 800);
private useSemanticSimilarity = false;
@ -69,7 +65,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
constructor(
@IEditorService private readonly editorService: IEditorService,
@IMenuService private readonly menuService: IMenuService,
// @IExtensionService private readonly extensionService: IExtensionService,
@IExtensionService private readonly extensionService: IExtensionService,
@IInstantiationService instantiationService: IInstantiationService,
@IKeybindingService keybindingService: IKeybindingService,
@ICommandService commandService: ICommandService,
@ -125,11 +121,10 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
this.useSemanticSimilarity = config.experimental.useSemanticSimilarity;
}
protected getCommandPicks(token: CancellationToken): Array<ICommandQuickPick> {
protected async getCommandPicks(token: CancellationToken): Promise<Array<ICommandQuickPick>> {
// TODO: bring this back once we have a chosen strategy for FastAndSlowPicks where Fast is also Promise based
// wait for extensions registration or 800ms once
// await this.extensionRegistrationRace;
await this.extensionRegistrationRace;
if (token.isCancellationRequested) {
return [];
@ -138,23 +133,32 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
return [
...this.getCodeEditorCommandPicks(),
...this.getGlobalCommandPicks()
].map(c => ({
...c,
].map(picks => ({
...picks,
buttons: [{
iconClass: ThemeIcon.asClassName(Codicon.gear),
tooltip: localize('configure keybinding', "Configure Keybinding"),
}],
trigger: (): TriggerAction => {
this.preferencesService.openGlobalKeybindingSettings(false, { query: `@command:${c.commandId}` });
this.preferencesService.openGlobalKeybindingSettings(false, { query: `@command:${picks.commandId}` });
return TriggerAction.CLOSE_PICKER;
},
}));
}
protected async getAdditionalCommandPicks(allPicks: ICommandQuickPick[], picksSoFar: ICommandQuickPick[], filter: string, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>> {
protected hasAdditionalCommandPicks(filter: string, token: CancellationToken): boolean {
if (!this.useSemanticSimilarity || filter === '' || token.isCancellationRequested || !this.semanticSimilarityService.isEnabled()) {
return false;
}
return true;
}
protected async getAdditionalCommandPicks(allPicks: ICommandQuickPick[], picksSoFar: ICommandQuickPick[], filter: string, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>> {
if (!this.hasAdditionalCommandPicks(filter, token)) {
return [];
}
const format = allPicks.map(p => p.commandId);
let scores: number[];
try {
@ -164,6 +168,11 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
} catch (e) {
return [];
}
if (token.isCancellationRequested) {
return [];
}
const sortedIndices = scores.map((_, i) => i).sort((a, b) => scores[b] - scores[a]);
const setOfPicksSoFar = new Set(picksSoFar.map(p => p.commandId));
const additionalPicks: Array<ICommandQuickPick | IQuickPickSeparator> = picksSoFar.length > 0
@ -179,12 +188,14 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
if (score < CommandsQuickAccessProvider.SEMANTIC_SIMILARITY_THRESHOLD || numOfSmartPicks === CommandsQuickAccessProvider.SEMANTIC_SIMILARITY_MAX_PICKS) {
break;
}
const pick = allPicks[i];
if (!setOfPicksSoFar.has(pick.commandId)) {
additionalPicks.push(pick);
numOfSmartPicks++;
}
}
return additionalPicks;
}

View file

@ -84,6 +84,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
private static SYMBOL_PICKS_MERGE_DELAY = 200; // allow some time to merge fast and slow picks to reduce flickering
private readonly pickState = new class {
picker: IQuickPick<IAnythingQuickPickItem> | undefined = undefined;
@ -394,7 +396,10 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
{ type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") },
...additionalPicks
] : [];
})()
})(),
// allow some time to merge files and symbols to reduce flickering
mergeDelay: AnythingQuickAccessProvider.SYMBOL_PICKS_MERGE_DELAY
};
}