mirror of
https://github.com/coder/code-server.git
synced 2026-05-16 09:17:25 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
189
lib/vscode/src/vs/platform/quickinput/browser/quickAccess.ts
Normal file
189
lib/vscode/src/vs/platform/quickinput/browser/quickAccess.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
224
lib/vscode/src/vs/platform/quickinput/browser/quickInput.ts
Normal file
224
lib/vscode/src/vs/platform/quickinput/browser/quickInput.ts
Normal 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
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
204
lib/vscode/src/vs/platform/quickinput/common/quickAccess.ts
Normal file
204
lib/vscode/src/vs/platform/quickinput/common/quickAccess.ts
Normal 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());
|
||||
97
lib/vscode/src/vs/platform/quickinput/common/quickInput.ts
Normal file
97
lib/vscode/src/vs/platform/quickinput/common/quickInput.ts
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user