Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,305 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { PickerQuickAccessProvider, IPickerQuickAccessItem, IPickerQuickAccessProviderOptions } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { or, matchesPrefix, matchesWords, matchesContiguousSubString } from 'vs/base/common/filters';
import { withNullAsUndefined } from 'vs/base/common/types';
import { LRUCache } from 'vs/base/common/map';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
export interface ICommandQuickPick extends IPickerQuickAccessItem {
commandId: string;
commandAlias?: string;
}
export interface ICommandsQuickAccessOptions extends IPickerQuickAccessProviderOptions<ICommandQuickPick> {
showAlias: boolean;
}
export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAccessProvider<ICommandQuickPick> implements IDisposable {
static PREFIX = '>';
private static WORD_FILTER = or(matchesPrefix, matchesWords, matchesContiguousSubString);
private readonly commandsHistory = this._register(this.instantiationService.createInstance(CommandsHistory));
constructor(
protected options: ICommandsQuickAccessOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@ICommandService private readonly commandService: ICommandService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@INotificationService private readonly notificationService: INotificationService
) {
super(AbstractCommandsQuickAccessProvider.PREFIX, options);
}
protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>> {
// Ask subclass for all command picks
const allCommandPicks = await this.getCommandPicks(disposables, token);
if (token.isCancellationRequested) {
return [];
}
// Filter
const filteredCommandPicks: ICommandQuickPick[] = [];
for (const commandPick of allCommandPicks) {
const labelHighlights = withNullAsUndefined(AbstractCommandsQuickAccessProvider.WORD_FILTER(filter, commandPick.label));
const aliasHighlights = commandPick.commandAlias ? withNullAsUndefined(AbstractCommandsQuickAccessProvider.WORD_FILTER(filter, commandPick.commandAlias)) : undefined;
// Add if matching in label or alias
if (labelHighlights || aliasHighlights) {
commandPick.highlights = {
label: labelHighlights,
detail: this.options.showAlias ? aliasHighlights : undefined
};
filteredCommandPicks.push(commandPick);
}
// Also add if we have a 100% command ID match
else if (filter === commandPick.commandId) {
filteredCommandPicks.push(commandPick);
}
}
// Add description to commands that have duplicate labels
const mapLabelToCommand = new Map<string, ICommandQuickPick>();
for (const commandPick of filteredCommandPicks) {
const existingCommandForLabel = mapLabelToCommand.get(commandPick.label);
if (existingCommandForLabel) {
commandPick.description = commandPick.commandId;
existingCommandForLabel.description = existingCommandForLabel.commandId;
} else {
mapLabelToCommand.set(commandPick.label, commandPick);
}
}
// Sort by MRU order and fallback to name otherwise
filteredCommandPicks.sort((commandPickA, commandPickB) => {
const commandACounter = this.commandsHistory.peek(commandPickA.commandId);
const commandBCounter = this.commandsHistory.peek(commandPickB.commandId);
if (commandACounter && commandBCounter) {
return commandACounter > commandBCounter ? -1 : 1; // use more recently used command before older
}
if (commandACounter) {
return -1; // first command was used, so it wins over the non used one
}
if (commandBCounter) {
return 1; // other command was used so it wins over the command
}
// both commands were never used, so we sort by name
return commandPickA.label.localeCompare(commandPickB.label);
});
const commandPicks: Array<ICommandQuickPick | IQuickPickSeparator> = [];
let addSeparator = false;
for (let i = 0; i < filteredCommandPicks.length; i++) {
const commandPick = filteredCommandPicks[i];
const keybinding = this.keybindingService.lookupKeybinding(commandPick.commandId);
const ariaLabel = keybinding ?
localize('commandPickAriaLabelWithKeybinding', "{0}, {1}", commandPick.label, keybinding.getAriaLabel()) :
commandPick.label;
// Separator: recently used
if (i === 0 && this.commandsHistory.peek(commandPick.commandId)) {
commandPicks.push({ type: 'separator', label: localize('recentlyUsed', "recently used") });
addSeparator = true;
}
// Separator: other commands
if (i !== 0 && addSeparator && !this.commandsHistory.peek(commandPick.commandId)) {
commandPicks.push({ type: 'separator', label: localize('morecCommands', "other commands") });
addSeparator = false; // only once
}
// Command
commandPicks.push({
...commandPick,
ariaLabel,
detail: this.options.showAlias && commandPick.commandAlias !== commandPick.label ? commandPick.commandAlias : undefined,
keybinding,
accept: async () => {
// Add to history
this.commandsHistory.push(commandPick.commandId);
// Telementry
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: commandPick.commandId,
from: 'quick open'
});
// Run
try {
await this.commandService.executeCommand(commandPick.commandId);
} catch (error) {
if (!isPromiseCanceledError(error)) {
this.notificationService.error(localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error)));
}
}
}
});
}
return commandPicks;
}
/**
* Subclasses to provide the actual command entries.
*/
protected abstract getCommandPicks(disposables: DisposableStore, token: CancellationToken): Promise<Array<ICommandQuickPick>>;
}
interface ISerializedCommandHistory {
usesLRU?: boolean;
entries: { key: string; value: number }[];
}
interface ICommandsQuickAccessConfiguration {
workbench: {
commandPalette: {
history: number;
preserveInput: boolean;
}
};
}
export class CommandsHistory extends Disposable {
static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50;
private static readonly PREF_KEY_CACHE = 'commandPalette.mru.cache';
private static readonly PREF_KEY_COUNTER = 'commandPalette.mru.counter';
private static cache: LRUCache<string, number> | undefined;
private static counter = 1;
private configuredCommandsHistoryLength = 0;
constructor(
@IStorageService private readonly storageService: IStorageService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService
) {
super();
// opt-in to syncing
storageKeysSyncRegistryService.registerStorageKey({ key: CommandsHistory.PREF_KEY_CACHE, version: 1 });
storageKeysSyncRegistryService.registerStorageKey({ key: CommandsHistory.PREF_KEY_COUNTER, version: 1 });
this.updateConfiguration();
this.load();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.configurationService.onDidChangeConfiguration(() => this.updateConfiguration()));
}
private updateConfiguration(): void {
this.configuredCommandsHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(this.configurationService);
if (CommandsHistory.cache && CommandsHistory.cache.limit !== this.configuredCommandsHistoryLength) {
CommandsHistory.cache.limit = this.configuredCommandsHistoryLength;
CommandsHistory.saveState(this.storageService);
}
}
private load(): void {
const raw = this.storageService.get(CommandsHistory.PREF_KEY_CACHE, StorageScope.GLOBAL);
let serializedCache: ISerializedCommandHistory | undefined;
if (raw) {
try {
serializedCache = JSON.parse(raw);
} catch (error) {
// invalid data
}
}
const cache = CommandsHistory.cache = new LRUCache<string, number>(this.configuredCommandsHistoryLength, 1);
if (serializedCache) {
let entries: { key: string; value: number }[];
if (serializedCache.usesLRU) {
entries = serializedCache.entries;
} else {
entries = serializedCache.entries.sort((a, b) => a.value - b.value);
}
entries.forEach(entry => cache.set(entry.key, entry.value));
}
CommandsHistory.counter = this.storageService.getNumber(CommandsHistory.PREF_KEY_COUNTER, StorageScope.GLOBAL, CommandsHistory.counter);
}
push(commandId: string): void {
if (!CommandsHistory.cache) {
return;
}
CommandsHistory.cache.set(commandId, CommandsHistory.counter++); // set counter to command
CommandsHistory.saveState(this.storageService);
}
peek(commandId: string): number | undefined {
return CommandsHistory.cache?.peek(commandId);
}
static saveState(storageService: IStorageService): void {
if (!CommandsHistory.cache) {
return;
}
const serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] };
CommandsHistory.cache.forEach((value, key) => serializedCache.entries.push({ key, value }));
storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache), StorageScope.GLOBAL);
storageService.store(CommandsHistory.PREF_KEY_COUNTER, CommandsHistory.counter, StorageScope.GLOBAL);
}
static getConfiguredCommandHistoryLength(configurationService: IConfigurationService): number {
const config = <ICommandsQuickAccessConfiguration>configurationService.getValue();
const configuredCommandHistoryLength = config.workbench?.commandPalette?.history;
if (typeof configuredCommandHistoryLength === 'number') {
return configuredCommandHistoryLength;
}
return CommandsHistory.DEFAULT_COMMANDS_HISTORY_LENGTH;
}
static clearHistory(configurationService: IConfigurationService, storageService: IStorageService): void {
const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(configurationService);
CommandsHistory.cache = new LRUCache<string, number>(commandHistoryLength);
CommandsHistory.counter = 1;
CommandsHistory.saveState(storageService);
}
}

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IQuickAccessProvider, IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess';
import { Registry } from 'vs/platform/registry/common/platform';
import { localize } from 'vs/nls';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
interface IHelpQuickAccessPickItem extends IQuickPickItem {
prefix: string;
}
export class HelpQuickAccessProvider implements IQuickAccessProvider {
static PREFIX = '?';
private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
constructor(@IQuickInputService private readonly quickInputService: IQuickInputService) { }
provide(picker: IQuickPick<IHelpQuickAccessPickItem>): IDisposable {
const disposables = new DisposableStore();
// Open a picker with the selected value if picked
disposables.add(picker.onDidAccept(() => {
const [item] = picker.selectedItems;
if (item) {
this.quickInputService.quickAccess.show(item.prefix, { preserveValue: true });
}
}));
// Also open a picker when we detect the user typed the exact
// name of a provider (e.g. `?term` for terminals)
disposables.add(picker.onDidChangeValue(value => {
const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length));
if (providerDescriptor && providerDescriptor.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) {
this.quickInputService.quickAccess.show(providerDescriptor.prefix, { preserveValue: true });
}
}));
// Fill in all providers separated by editor/global scope
const { editorProviders, globalProviders } = this.getQuickAccessProviders();
picker.items = editorProviders.length === 0 || globalProviders.length === 0 ?
// Without groups
[
...(editorProviders.length === 0 ? globalProviders : editorProviders)
] :
// With groups
[
{ label: localize('globalCommands', "global commands"), type: 'separator' },
...globalProviders,
{ label: localize('editorCommands', "editor commands"), type: 'separator' },
...editorProviders
];
return disposables;
}
private getQuickAccessProviders(): { editorProviders: IHelpQuickAccessPickItem[], globalProviders: IHelpQuickAccessPickItem[] } {
const globalProviders: IHelpQuickAccessPickItem[] = [];
const editorProviders: IHelpQuickAccessPickItem[] = [];
for (const provider of this.registry.getQuickAccessProviders().sort((providerA, providerB) => providerA.prefix.localeCompare(providerB.prefix))) {
if (provider.prefix === HelpQuickAccessProvider.PREFIX) {
continue; // exclude help which is already active
}
for (const helpEntry of provider.helpEntries) {
const prefix = helpEntry.prefix || provider.prefix;
const label = prefix || '\u2026' /* ... */;
(helpEntry.needsEditor ? editorProviders : globalProviders).push({
prefix,
label,
ariaLabel: localize('helpPickAriaLabel', "{0}, {1}", label, helpEntry.description),
description: helpEntry.description
});
}
}
return { editorProviders, globalProviders };
}
}

View File

@@ -0,0 +1,334 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput';
import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess';
import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { timeout } from 'vs/base/common/async';
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: IQuickPickAcceptEvent): 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.
*/
canAcceptInBackground?: boolean;
/**
* Enables to show a pick entry when no results are returned from a search.
*/
noResultsPick?: T;
}
export type Pick<T> = T | IQuickPickSeparator;
export type PicksWithActive<T> = { items: ReadonlyArray<Pick<T>>, active?: T };
export type Picks<T> = ReadonlyArray<Pick<T>> | PicksWithActive<T>;
export type FastAndSlowPicks<T> = { picks: Picks<T>, additionalPicks: Promise<Picks<T>> };
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 {
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();
}
provide(picker: IQuickPick<T>, token: CancellationToken): 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);
const applyPicks = (picks: Picks<T>, skipEmpty?: boolean): boolean => {
let items: ReadonlyArray<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;
}
if (picksFilter.length > 0 && this.options?.noResultsPick) {
items = [this.options.noResultsPick];
}
}
picker.items = items;
if (activeItem) {
picker.activeItems = [activeItem];
}
return true;
};
// No Picks
if (providedPicks === null) {
// Ignore
}
// Fast and Slow Picks
else if (isFastAndSlowPicks(providedPicks)) {
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.
(async () => {
await timeout(PickerQuickAccessProvider.FAST_PICKS_RACE_DELAY);
if (picksToken.isCancellationRequested) {
return;
}
if (!slowPicksApplied) {
fastPicksApplied = applyPicks(providedPicks.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;
if (picksToken.isCancellationRequested) {
return;
}
let picks: ReadonlyArray<Pick<T>>;
let activePick: Pick<T> | undefined = undefined;
if (isPicksWithActive(providedPicks.picks)) {
picks = providedPicks.picks.items;
activePick = providedPicks.picks.active;
} else {
picks = providedPicks.picks;
}
let additionalPicks: ReadonlyArray<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;
}
})()
]);
}
// Fast Picks
else if (!(providedPicks instanceof Promise)) {
applyPicks(providedPicks);
}
// Slow Picks
else {
picker.busy = true;
try {
const awaitedPicks = await providedPicks;
if (picksToken.isCancellationRequested) {
return;
}
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();
items.splice(index, 1);
picker.items = items;
}
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): Picks<T> | Promise<Picks<T>> | FastAndSlowPicks<T> | null;
}

View File

@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickInputService, IQuickPick, IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions, DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess';
import { Registry } from 'vs/platform/registry/common/platform';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { once } from 'vs/base/common/functional';
export class QuickAccessController extends Disposable implements IQuickAccessController {
private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
private readonly mapProviderToDescriptor = new Map<IQuickAccessProviderDescriptor, IQuickAccessProvider>();
private readonly lastAcceptedPickerValues = new Map<IQuickAccessProviderDescriptor, string>();
private visibleQuickAccess: {
picker: IQuickPick<IQuickPickItem>,
descriptor: IQuickAccessProviderDescriptor | undefined,
value: string
} | undefined = undefined;
constructor(
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
}
show(value = '', options?: IQuickAccessOptions): void {
// Find provider for the value to show
const [provider, descriptor] = this.getOrInstantiateProvider(value);
// Return early if quick access is already showing on that same prefix
const visibleQuickAccess = this.visibleQuickAccess;
const visibleDescriptor = visibleQuickAccess?.descriptor;
if (visibleQuickAccess && descriptor && visibleDescriptor === descriptor) {
// Apply value only if it is more specific than the prefix
// from the provider and we are not instructed to preserve
if (value !== descriptor.prefix && !options?.preserveValue) {
visibleQuickAccess.picker.value = value;
}
// Always adjust selection
this.adjustValueSelection(visibleQuickAccess.picker, descriptor, options);
return;
}
// Rewrite the filter value based on certain rules unless disabled
if (descriptor && !options?.preserveValue) {
let newValue: string | undefined = undefined;
// If we have a visible provider with a value, take it's filter value but
// rewrite to new provider prefix in case they differ
if (visibleQuickAccess && visibleDescriptor && visibleDescriptor !== descriptor) {
const newValueCandidateWithoutPrefix = visibleQuickAccess.value.substr(visibleDescriptor.prefix.length);
if (newValueCandidateWithoutPrefix) {
newValue = `${descriptor.prefix}${newValueCandidateWithoutPrefix}`;
}
}
// Otherwise, take a default value as instructed
if (!newValue) {
const defaultFilterValue = provider?.defaultFilterValue;
if (defaultFilterValue === DefaultQuickAccessFilterValue.LAST) {
newValue = this.lastAcceptedPickerValues.get(descriptor);
} else if (typeof defaultFilterValue === 'string') {
newValue = `${descriptor.prefix}${defaultFilterValue}`;
}
}
if (typeof newValue === 'string') {
value = newValue;
}
}
// Create a picker for the provider to use with the initial value
// and adjust the filtering to exclude the prefix from filtering
const disposables = new DisposableStore();
const picker = disposables.add(this.quickInputService.createQuickPick());
picker.value = value;
this.adjustValueSelection(picker, descriptor, options);
picker.placeholder = descriptor?.placeholder;
picker.quickNavigate = options?.quickNavigateConfiguration;
picker.hideInput = !!picker.quickNavigate && !visibleQuickAccess; // only hide input if there was no picker opened already
if (typeof options?.itemActivation === 'number' || options?.quickNavigateConfiguration) {
picker.itemActivation = options?.itemActivation ?? ItemActivation.SECOND /* quick nav is always second */;
}
picker.contextKey = descriptor?.contextKey;
picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0);
if (descriptor?.placeholder) {
picker.ariaLabel = descriptor?.placeholder;
}
// Register listeners
const cancellationToken = this.registerPickerListeners(picker, provider, descriptor, value, disposables);
// Ask provider to fill the picker as needed if we have one
if (provider) {
disposables.add(provider.provide(picker, cancellationToken));
}
// Finally, show the picker. This is important because a provider
// may not call this and then our disposables would leak that rely
// on the onDidHide event.
picker.show();
}
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void {
let valueSelection: [number, number];
// Preserve: just always put the cursor at the end
if (options?.preserveValue) {
valueSelection = [picker.value.length, picker.value.length];
}
// Otherwise: select the value up until the prefix
else {
valueSelection = [descriptor?.prefix.length ?? 0, picker.value.length];
}
picker.valueSelection = valueSelection;
}
private registerPickerListeners(picker: IQuickPick<IQuickPickItem>, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, disposables: DisposableStore): CancellationToken {
// Remember as last visible picker and clean up once picker get's disposed
const visibleQuickAccess = this.visibleQuickAccess = { picker, descriptor, value };
disposables.add(toDisposable(() => {
if (visibleQuickAccess === this.visibleQuickAccess) {
this.visibleQuickAccess = undefined;
}
}));
// Whenever the value changes, check if the provider has
// changed and if so - re-create the picker from the beginning
disposables.add(picker.onDidChangeValue(value => {
const [providerForValue] = this.getOrInstantiateProvider(value);
if (providerForValue !== provider) {
this.show(value, { preserveValue: true } /* do not rewrite value from user typing! */);
} else {
visibleQuickAccess.value = value; // remember the value in our visible one
}
}));
// Remember picker input for future use when accepting
if (descriptor) {
disposables.add(picker.onDidAccept(() => {
this.lastAcceptedPickerValues.set(descriptor, picker.value);
}));
}
// Create a cancellation token source that is valid as long as the
// picker has not been closed without picking an item
const cts = disposables.add(new CancellationTokenSource());
once(picker.onDidHide)(() => {
if (picker.selectedItems.length === 0) {
cts.cancel();
}
// Start to dispose once picker hides
disposables.dispose();
});
return cts.token;
}
private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] {
const providerDescriptor = this.registry.getQuickAccessProvider(value);
if (!providerDescriptor) {
return [undefined, undefined];
}
let provider = this.mapProviderToDescriptor.get(providerDescriptor);
if (!provider) {
provider = this.instantiationService.createInstance(providerDescriptor.ctor);
this.mapProviderToDescriptor.set(providerDescriptor, provider);
}
return [provider, providerDescriptor];
}
}

View File

@@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput, IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService, Themable } from 'vs/platform/theme/common/themeService';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, listFocusBackground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground } from 'vs/platform/theme/common/colorRegistry';
import { CancellationToken } from 'vs/base/common/cancellation';
import { computeStyles } from 'vs/platform/theme/common/styler';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { QuickInputController, IQuickInputStyles, IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
import { WorkbenchList, IWorkbenchListOptions } from 'vs/platform/list/browser/listService';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess';
import { QuickAccessController } from 'vs/platform/quickinput/browser/quickAccess';
export interface IQuickInputControllerHost extends ILayoutService { }
export class QuickInputService extends Themable implements IQuickInputService {
declare readonly _serviceBrand: undefined;
get backButton(): IQuickInputButton { return this.controller.backButton; }
get onShow() { return this.controller.onShow; }
get onHide() { return this.controller.onHide; }
private _controller: QuickInputController | undefined;
private get controller(): QuickInputController {
if (!this._controller) {
this._controller = this._register(this.createController());
}
return this._controller;
}
private _quickAccess: IQuickAccessController | undefined;
get quickAccess(): IQuickAccessController {
if (!this._quickAccess) {
this._quickAccess = this._register(this.instantiationService.createInstance(QuickAccessController));
}
return this._quickAccess;
}
private readonly contexts = new Map<string, IContextKey<boolean>>();
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService protected readonly contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
@ILayoutService protected readonly layoutService: ILayoutService
) {
super(themeService);
}
protected createController(host: IQuickInputControllerHost = this.layoutService, options?: Partial<IQuickInputOptions>): QuickInputController {
const defaultOptions: IQuickInputOptions = {
idPrefix: 'quickInput_', // Constant since there is still only one.
container: host.container,
ignoreFocusOut: () => false,
isScreenReaderOptimized: () => this.accessibilityService.isScreenReaderOptimized(),
backKeybindingLabel: () => undefined,
setContextKey: (id?: string) => this.setContextKey(id),
returnFocus: () => host.focus(),
createList: <T>(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: IListRenderer<T, any>[],
options: IWorkbenchListOptions<T>,
) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List<T>,
styles: this.computeStyles()
};
const controller = this._register(new QuickInputController({
...defaultOptions,
...options
}));
controller.layout(host.dimension, host.offset?.top ?? 0);
// Layout changes
this._register(host.onLayout(dimension => controller.layout(dimension, host.offset?.top ?? 0)));
// Context keys
this._register(controller.onShow(() => this.resetContextKeys()));
this._register(controller.onHide(() => this.resetContextKeys()));
return controller;
}
private setContextKey(id?: string) {
let key: IContextKey<boolean> | undefined;
if (id) {
key = this.contexts.get(id);
if (!key) {
key = new RawContextKey<boolean>(id, false)
.bindTo(this.contextKeyService);
this.contexts.set(id, key);
}
}
if (key && key.get()) {
return; // already active context
}
this.resetContextKeys();
if (key) {
key.set(true);
}
}
private resetContextKeys() {
this.contexts.forEach(context => {
if (context.get()) {
context.reset();
}
});
}
pick<T extends IQuickPickItem, O extends IPickOptions<T>>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options: O = <O>{}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> {
return this.controller.pick(picks, options, token);
}
input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise<string | undefined> {
return this.controller.input(options, token);
}
createQuickPick<T extends IQuickPickItem>(): IQuickPick<T> {
return this.controller.createQuickPick();
}
createInputBox(): IInputBox {
return this.controller.createInputBox();
}
focus() {
this.controller.focus();
}
toggle() {
this.controller.toggle();
}
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) {
this.controller.navigate(next, quickNavigate);
}
accept(keyMods?: IKeyMods) {
return this.controller.accept(keyMods);
}
back() {
return this.controller.back();
}
cancel() {
return this.controller.cancel();
}
protected updateStyles() {
this.controller.applyStyles(this.computeStyles());
}
private computeStyles(): IQuickInputStyles {
return {
widget: {
...computeStyles(this.theme, {
quickInputBackground,
quickInputForeground,
quickInputTitleBackground,
contrastBorder,
widgetShadow
}),
},
inputBox: computeStyles(this.theme, {
inputForeground,
inputBackground,
inputBorder,
inputValidationInfoBackground,
inputValidationInfoForeground,
inputValidationInfoBorder,
inputValidationWarningBackground,
inputValidationWarningForeground,
inputValidationWarningBorder,
inputValidationErrorBackground,
inputValidationErrorForeground,
inputValidationErrorBorder
}),
countBadge: computeStyles(this.theme, {
badgeBackground,
badgeForeground,
badgeBorder: contrastBorder
}),
button: computeStyles(this.theme, {
buttonForeground,
buttonBackground,
buttonHoverBackground,
buttonBorder: contrastBorder
}),
progressBar: computeStyles(this.theme, {
progressBarBackground
}),
list: computeStyles(this.theme, {
listBackground: quickInputBackground,
// Look like focused when inactive.
listInactiveFocusForeground: listFocusForeground,
listInactiveFocusBackground: listFocusBackground,
listFocusOutline: activeContrastBorder,
listInactiveFocusOutline: activeContrastBorder,
pickerGroupBorder,
pickerGroupForeground
})
};
}
}

View File

@@ -0,0 +1,204 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickPick, IQuickPickItem, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Registry } from 'vs/platform/registry/common/platform';
import { coalesce } from 'vs/base/common/arrays';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ItemActivation } from 'vs/base/parts/quickinput/common/quickInput';
export interface IQuickAccessOptions {
/**
* Allows to enable quick navigate support in quick input.
*/
quickNavigateConfiguration?: IQuickNavigateConfiguration;
/**
* Allows to configure a different item activation strategy.
* By default the first item in the list will get activated.
*/
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;
}
export interface IQuickAccessController {
/**
* Open the quick access picker with the optional value prefilled.
*/
show(value?: string, options?: IQuickAccessOptions): void;
}
export enum DefaultQuickAccessFilterValue {
/**
* Keep the value as it is given to quick access.
*/
PRESERVE = 0,
/**
* Use the value that was used last time something was accepted from the picker.
*/
LAST = 1
}
export interface IQuickAccessProvider {
/**
* Allows to set a default filter value when the provider opens. This can be:
* - `undefined` to not specify any default value
* - `DefaultFilterValues.PRESERVE` to use the value that was last typed
* - `string` for the actual value to use
*
* Note: the default filter will only be used if quick access was opened with
* the exact prefix of the provider. Otherwise the filter value is preserved.
*/
readonly defaultFilterValue?: string | DefaultQuickAccessFilterValue;
/**
* Called whenever a prefix was typed into quick pick that matches the provider.
*
* @param picker the picker to use for showing provider results. The picker is
* automatically shown after the method returns, no need to call `show()`.
* @param token providers have to check the cancellation token everytime after
* a long running operation or from event handlers because it could be that the
* picker has been closed or changed meanwhile. The token can be used to find out
* that the picker was closed without picking an entry (e.g. was canceled by the user).
* @return a disposable that will automatically be disposed when the picker
* closes or is replaced by another picker.
*/
provide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable;
}
export interface IQuickAccessProviderHelp {
/**
* The prefix to show for the help entry. If not provided,
* the prefix used for registration will be taken.
*/
prefix?: string;
/**
* A description text to help understand the intent of the provider.
*/
description: string;
/**
* Separation between provider for editors and global ones.
*/
needsEditor: boolean;
}
export interface IQuickAccessProviderDescriptor {
/**
* The actual provider that will be instantiated as needed.
*/
readonly ctor: { new(...services: any /* TS BrandedService but no clue how to type this properly */[]): IQuickAccessProvider };
/**
* The prefix for quick access picker to use the provider for.
*/
readonly prefix: string;
/**
* A placeholder to use for the input field when the provider is active.
* This will also be read out by screen readers and thus helps for
* accessibility.
*/
readonly placeholder?: string;
/**
* Documentation for the provider in the quick access help.
*/
readonly helpEntries: IQuickAccessProviderHelp[];
/**
* A context key that will be set automatically when the
* picker for the provider is showing.
*/
readonly contextKey?: string;
}
export const Extensions = {
Quickaccess: 'workbench.contributions.quickaccess'
};
export interface IQuickAccessRegistry {
/**
* Registers a quick access provider to the platform.
*/
registerQuickAccessProvider(provider: IQuickAccessProviderDescriptor): IDisposable;
/**
* Get all registered quick access providers.
*/
getQuickAccessProviders(): IQuickAccessProviderDescriptor[];
/**
* Get a specific quick access provider for a given prefix.
*/
getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined;
}
export class QuickAccessRegistry implements IQuickAccessRegistry {
private providers: IQuickAccessProviderDescriptor[] = [];
private defaultProvider: IQuickAccessProviderDescriptor | undefined = undefined;
registerQuickAccessProvider(provider: IQuickAccessProviderDescriptor): IDisposable {
// Extract the default provider when no prefix is present
if (provider.prefix.length === 0) {
this.defaultProvider = provider;
} else {
this.providers.push(provider);
}
// sort the providers by decreasing prefix length, such that longer
// prefixes take priority: 'ext' vs 'ext install' - the latter should win
this.providers.sort((providerA, providerB) => providerB.prefix.length - providerA.prefix.length);
return toDisposable(() => {
this.providers.splice(this.providers.indexOf(provider), 1);
if (this.defaultProvider === provider) {
this.defaultProvider = undefined;
}
});
}
getQuickAccessProviders(): IQuickAccessProviderDescriptor[] {
return coalesce([this.defaultProvider, ...this.providers]);
}
getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined {
const result = prefix ? (this.providers.find(provider => prefix.startsWith(provider.prefix)) || undefined) : undefined;
return result || this.defaultProvider;
}
clear(): Function {
const providers = [...this.providers];
const defaultProvider = this.defaultProvider;
this.providers = [];
this.defaultProvider = undefined;
return () => {
this.providers = providers;
this.defaultProvider = defaultProvider;
};
}
}
Registry.add(Extensions.Quickaccess, new QuickAccessRegistry());

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess';
export * from 'vs/base/parts/quickinput/common/quickInput';
export const IQuickInputService = createDecorator<IQuickInputService>('quickInputService');
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export interface IQuickInputService {
readonly _serviceBrand: undefined;
/**
* Provides access to the back button in quick input.
*/
readonly backButton: IQuickInputButton;
/**
* Provides access to the quick access providers.
*/
readonly quickAccess: IQuickAccessController;
/**
* Allows to register on the event that quick input is showing.
*/
readonly onShow: Event<void>;
/**
* Allows to register on the event that quick input is hiding.
*/
readonly onHide: Event<void>;
/**
* Opens the quick input box for selecting items and returns a promise
* with the user selected item(s) if any.
*/
pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: true }, token?: CancellationToken): Promise<T[] | undefined>;
pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: false }, token?: CancellationToken): Promise<T | undefined>;
pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: Omit<IPickOptions<T>, 'canPickMany'>, token?: CancellationToken): Promise<T | undefined>;
/**
* Opens the quick input box for text input and returns a promise with the user typed value if any.
*/
input(options?: IInputOptions, token?: CancellationToken): Promise<string | undefined>;
/**
* Provides raw access to the quick pick controller.
*/
createQuickPick<T extends IQuickPickItem>(): IQuickPick<T>;
/**
* Provides raw access to the quick input controller.
*/
createInputBox(): IInputBox;
/**
* Moves focus into quick input.
*/
focus(): void;
/**
* Toggle the checked state of the selected item.
*/
toggle(): void;
/**
* Navigate inside the opened quick input list.
*/
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void;
/**
* Navigate back in a multi-step quick input.
*/
back(): Promise<void>;
/**
* Accept the selected item.
*
* @param keyMods allows to override the state of key
* modifiers that should be present when invoking.
*/
accept(keyMods?: IKeyMods): Promise<void>;
/**
* Cancels quick input and closes it.
*/
cancel(): Promise<void>;
}