Merge from vscode 073a24de05773f2261f89172987002dc0ae2f1cd (#9711)

This commit is contained in:
Anthony Dresser
2020-03-24 00:24:15 -07:00
committed by GitHub
parent 29741d684e
commit 89ef1b0c2e
226 changed files with 6161 additions and 3288 deletions

View File

@@ -6,7 +6,6 @@
import { localize } from 'vs/nls';
import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { PickerQuickAccessProvider, IPickerQuickAccessItem, IPickerQuickAccessProviderOptions } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { distinct } from 'vs/base/common/arrays';
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';
@@ -22,8 +21,6 @@ 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 { isFirefox } from 'vs/base/browser/browser';
import { timeout } from 'vs/base/common/async';
export interface ICommandQuickPick extends IPickerQuickAccessItem {
commandId: string;
@@ -74,12 +71,9 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
}
}
// Remove duplicates
const distinctCommandPicks = distinct(filteredCommandPicks, pick => `${pick.label}${pick.commandId}`);
// Add description to commands that have duplicate labels
const mapLabelToCommand = new Map<string, ICommandQuickPick>();
for (const commandPick of distinctCommandPicks) {
for (const commandPick of filteredCommandPicks) {
const existingCommandForLabel = mapLabelToCommand.get(commandPick.label);
if (existingCommandForLabel) {
commandPick.description = commandPick.commandId;
@@ -90,7 +84,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
}
// Sort by MRU order and fallback to name otherwise
distinctCommandPicks.sort((commandPickA, commandPickB) => {
filteredCommandPicks.sort((commandPickA, commandPickB) => {
const commandACounter = this.commandsHistory.peek(commandPickA.commandId);
const commandBCounter = this.commandsHistory.peek(commandPickB.commandId);
@@ -113,8 +107,8 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
const commandPicks: Array<ICommandQuickPick | IQuickPickSeparator> = [];
let addSeparator = false;
for (let i = 0; i < distinctCommandPicks.length; i++) {
const commandPick = distinctCommandPicks[i];
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}, commands picker", commandPick.label, keybinding.getAriaLabel()) :
@@ -143,13 +137,6 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
// Add to history
this.commandsHistory.push(commandPick.commandId);
if (!isFirefox) {
// Use a timeout to give the quick open widget a chance to close itself first
// Firefox: since the browser is quite picky for certain commands, we do not
// use a timeout (https://github.com/microsoft/vscode/issues/83288)
await timeout(50);
}
// Telementry
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: commandPick.commandId,
@@ -191,7 +178,7 @@ interface ICommandsQuickAccessConfiguration {
};
}
class CommandsHistory extends Disposable {
export class CommandsHistory extends Disposable {
static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50;

View File

@@ -66,6 +66,10 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
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' /* ... */;

View File

@@ -25,7 +25,12 @@ export enum TriggerAction {
/**
* Update the results of the picker.
*/
REFRESH_PICKER
REFRESH_PICKER,
/**
* Remove the item from the picker.
*/
REMOVE_ITEM
}
export interface IPickerQuickAccessItem extends IQuickPickItem {
@@ -211,6 +216,14 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
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;
}
}
}

View File

@@ -5,18 +5,32 @@
import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions } from 'vs/platform/quickinput/common/quickAccess';
import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions, DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess';
import { Registry } from 'vs/platform/registry/common/platform';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { once } from 'vs/base/common/functional';
interface IInternalQuickAccessOptions extends IQuickAccessOptions {
/**
* Internal option to not rewrite the filter value at all but use it as is.
*/
preserveFilterValue?: boolean;
}
export class QuickAccessController extends Disposable implements IQuickAccessController {
private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
private readonly mapProviderToDescriptor = new Map<IQuickAccessProviderDescriptor, IQuickAccessProvider>();
private lastActivePicker: IQuickPick<IQuickPickItem> | undefined = undefined;
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,
@@ -25,33 +39,131 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
super();
}
show(value = '', options?: IQuickAccessOptions): void {
const disposables = new DisposableStore();
// Hide any previous picker if any
this.lastActivePicker?.hide();
show(value = '', options?: IInternalQuickAccessOptions): 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 and simply take over the filter value if it
// is more specific and select it for the user to be able
// to type over
const visibleQuickAccess = this.visibleQuickAccess;
const visibleDescriptor = visibleQuickAccess?.descriptor;
if (visibleQuickAccess && descriptor && visibleDescriptor === descriptor) {
// Take over the value only if it is not matching
// the existing provider prefix or we are to preserve
if (value !== descriptor.prefix && !options?.preserveFilterValue) {
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?.preserveFilterValue) {
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}`;
}
}
// If the new provider wants to preserve the filter, take it's last remembered value
// If the new provider wants to define the filter, take it as is
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.placeholder = descriptor?.placeholder;
picker.value = value;
this.adjustValueSelection(picker, descriptor, options);
picker.placeholder = descriptor?.placeholder;
picker.quickNavigate = options?.quickNavigateConfiguration;
picker.valueSelection = options?.inputSelection ? [options.inputSelection.start, options.inputSelection.end] : [value.length, value.length];
picker.hideInput = !!picker.quickNavigate && !visibleQuickAccess; // only hide input if there was no picker opened already
picker.autoFocusSecondEntry = !!options?.quickNavigateConfiguration || !!options?.autoFocus?.autoFocusSecondEntry;
picker.contextKey = descriptor?.contextKey;
picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0);
// Remember as last active picker and clean up once picker get's disposed
this.lastActivePicker = picker;
// Register listeners
const cancellationToken = this.registerPickerListeners(disposables, picker, provider, descriptor, value);
// 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?: IInternalQuickAccessOptions): void {
let valueSelection: [number, number];
// Preserve: just always put the cursor at the end
if (options?.preserveFilterValue) {
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(disposables: DisposableStore, picker: IQuickPick<IQuickPickItem>, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string): 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 (picker === this.lastActivePicker) {
this.lastActivePicker = undefined;
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, { preserveFilterValue: 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());
@@ -64,24 +176,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
disposables.dispose();
});
// 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);
}
}));
// Ask provider to fill the picker as needed if we have one
if (provider) {
disposables.add(provider.provide(picker, cts.token));
}
// 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();
return cts.token;
}
private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
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';
@@ -154,8 +154,8 @@ export class QuickInputService extends Themable implements IQuickInputService {
this.controller.navigate(next, quickNavigate);
}
accept() {
return this.controller.accept();
accept(keyMods?: IKeyMods) {
return this.controller.accept(keyMods);
}
back() {

View File

@@ -12,15 +12,15 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IQuickAccessOptions {
/**
* Allows to control the part of text in the input field that should be selected.
*/
inputSelection?: { start: number; end: number; };
/**
* Allows to enable quick navigate support in quick input.
*/
quickNavigateConfiguration?: IQuickNavigateConfiguration;
/**
* Wether to select the second pick item by default instead of the first.
*/
autoFocus?: { autoFocusSecondEntry?: boolean }
}
export interface IQuickAccessController {
@@ -31,8 +31,32 @@ export interface IQuickAccessController {
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.
*

View File

@@ -6,7 +6,7 @@
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 } from 'vs/base/parts/quickinput/common/quickInput';
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';
@@ -84,8 +84,11 @@ export interface IQuickInputService {
/**
* Accept the selected item.
*
* @param keyMods allows to override the state of key
* modifiers that should be present when invoking.
*/
accept(): Promise<void>;
accept(keyMods?: IKeyMods): Promise<void>;
/**
* Cancels quick input and closes it.