mirror of
https://github.com/Microsoft/vscode
synced 2024-08-25 03:56:03 +00:00
Support setting keyboard shortcut from Action Item Context Menu (#209431)
fixes #208221
This commit is contained in:
parent
03ef5ffe3f
commit
5c00163b30
|
@ -212,26 +212,31 @@ export class WorkbenchToolBar extends ToolBar {
|
|||
}
|
||||
}
|
||||
|
||||
const primaryActions = [];
|
||||
|
||||
if (action instanceof MenuItemAction && action.menuKeybinding) {
|
||||
primaryActions.push(action.menuKeybinding);
|
||||
}
|
||||
|
||||
// add "hide foo" actions
|
||||
let hideAction: IAction;
|
||||
if (!noHide && (action instanceof MenuItemAction || action instanceof SubmenuItemAction)) {
|
||||
if (!action.hideActions) {
|
||||
// no context menu for MenuItemAction instances that support no hiding
|
||||
// those are fake actions and need to be cleaned up
|
||||
return;
|
||||
}
|
||||
hideAction = action.hideActions.hide;
|
||||
primaryActions.push(action.hideActions.hide);
|
||||
|
||||
} else {
|
||||
hideAction = toAction({
|
||||
primaryActions.push(toAction({
|
||||
id: 'label',
|
||||
label: localize('hide', "Hide"),
|
||||
enabled: false,
|
||||
run() { }
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const actions = Separator.join([hideAction], toggleActions);
|
||||
const actions = Separator.join(primaryActions, toggleActions);
|
||||
|
||||
// add "Reset Menu" action
|
||||
if (this._options?.resetMenu && !menuIds) {
|
||||
|
|
|
@ -479,6 +479,7 @@ export class MenuItemAction implements IAction {
|
|||
alt: ICommandAction | undefined,
|
||||
options: IMenuActionOptions | undefined,
|
||||
readonly hideActions: IMenuItemHide | undefined,
|
||||
readonly menuKeybinding: IAction | undefined,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService private _commandService: ICommandService
|
||||
) {
|
||||
|
@ -513,7 +514,7 @@ export class MenuItemAction implements IAction {
|
|||
}
|
||||
|
||||
this.item = item;
|
||||
this.alt = alt ? new MenuItemAction(alt, undefined, options, hideActions, contextKeyService, _commandService) : undefined;
|
||||
this.alt = alt ? new MenuItemAction(alt, undefined, options, hideActions, undefined, contextKeyService, _commandService) : undefined;
|
||||
this._options = options;
|
||||
this.class = icon && ThemeIcon.asClassName(icon);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { IMenu, IMenuActionOptions, IMenuChangeEvent, IMenuCreateOptions, IMenuI
|
|||
import { ICommandAction, ILocalizedString } from 'vs/platform/action/common/action';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Separator, toAction } from 'vs/base/common/actions';
|
||||
import { IAction, Separator, toAction } from 'vs/base/common/actions';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { removeFastWithoutKeepingOrder } from 'vs/base/common/arrays';
|
||||
import { localize } from 'vs/nls';
|
||||
|
@ -162,7 +162,7 @@ class MenuInfo {
|
|||
private readonly _hiddenStates: PersistedMenuHideState,
|
||||
private readonly _collectContextKeysForSubmenus: boolean,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService
|
||||
) {
|
||||
this.refresh();
|
||||
}
|
||||
|
@ -245,8 +245,8 @@ class MenuInfo {
|
|||
const menuHide = createMenuHide(this._id, isMenuItem ? item.command : item, this._hiddenStates);
|
||||
if (isMenuItem) {
|
||||
// MenuItemAction
|
||||
activeActions.push(new MenuItemAction(item.command, item.alt, options, menuHide, this._contextKeyService, this._commandService));
|
||||
|
||||
const menuKeybinding = createMenuKeybindingAction(this._id, item.command, this._commandService);
|
||||
activeActions.push(new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService));
|
||||
} else {
|
||||
// SubmenuItemAction
|
||||
const groups = new MenuInfo(item.submenu, this._hiddenStates, this._collectContextKeysForSubmenus, this._commandService, this._contextKeyService).createActionGroups(options);
|
||||
|
@ -336,7 +336,7 @@ class MenuImpl implements IMenu {
|
|||
hiddenStates: PersistedMenuHideState,
|
||||
options: Required<IMenuCreateOptions>,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
this._menuInfo = new MenuInfo(id, hiddenStates, options.emitEventsForSubmenuChanges, commandService, contextKeyService);
|
||||
|
||||
|
@ -437,3 +437,20 @@ function createMenuHide(menu: MenuId, command: ICommandAction | ISubmenuItem, st
|
|||
get isHidden() { return !toggle.checked; },
|
||||
};
|
||||
}
|
||||
|
||||
function createMenuKeybindingAction(menu: MenuId, command: ICommandAction | ISubmenuItem, commandService: ICommandService): IAction | undefined {
|
||||
if (isISubmenuItem(command)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const configureKeybindingAction = toAction({
|
||||
id: `configureKeybinding/${menu.id}/${command.id}`,
|
||||
label: localize('configure keybinding', "Configure Keybinding"),
|
||||
run() {
|
||||
const when = command.precondition?.serialize();
|
||||
commandService.executeCommand('workbench.action.openGlobalKeybindings', `@command:${command.id}` + (when ? ` +when:${when}` : ''));
|
||||
}
|
||||
});
|
||||
|
||||
return configureKeybindingAction;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|||
|
||||
export interface ICommandQuickPick extends IPickerQuickAccessItem {
|
||||
readonly commandId: string;
|
||||
readonly commandWhen?: string;
|
||||
readonly commandAlias?: string;
|
||||
readonly commandDescription?: ILocalizedString;
|
||||
tfIdfScore?: number;
|
||||
|
|
|
@ -374,7 +374,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
actionViewItemProvider: (action, options) => {
|
||||
if (this.location === ChatAgentLocation.Panel) {
|
||||
if ((action.id === SubmitAction.ID || action.id === CancelAction.ID) && action instanceof MenuItemAction) {
|
||||
const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined);
|
||||
const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined);
|
||||
return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import { CommandInformationResult, IAiRelatedInformationService, RelatedInformat
|
|||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { createKeybindingCommandQuery } from 'vs/workbench/services/preferences/browser/keybindingsEditorModel';
|
||||
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
|
||||
|
||||
export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider {
|
||||
|
@ -132,7 +133,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
|
|||
tooltip: localize('configure keybinding', "Configure Keybinding"),
|
||||
}],
|
||||
trigger: (): TriggerAction => {
|
||||
this.preferencesService.openGlobalKeybindingSettings(false, { query: `@command:${picks.commandId}` });
|
||||
this.preferencesService.openGlobalKeybindingSettings(false, { query: createKeybindingCommandQuery(picks.commandId, picks.commandWhen) });
|
||||
return TriggerAction.CLOSE_PICKER;
|
||||
},
|
||||
}));
|
||||
|
@ -243,6 +244,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
|
|||
: { value: metadataDescription, original: metadataDescription };
|
||||
globalCommandPicks.push({
|
||||
commandId: action.item.id,
|
||||
commandWhen: action.item.precondition?.serialize(),
|
||||
commandAlias,
|
||||
label: stripIcons(label),
|
||||
commandDescription,
|
||||
|
|
|
@ -2082,7 +2082,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar {
|
|||
id: SCMInputWidgetCommandId.CancelAction,
|
||||
title: localize('scmInputCancelAction', "Cancel"),
|
||||
icon: Codicon.debugStop,
|
||||
}, undefined, undefined, undefined, contextKeyService, commandService);
|
||||
}, undefined, undefined, undefined, undefined, contextKeyService, commandService);
|
||||
}
|
||||
|
||||
public setInput(input: ISCMInput): void {
|
||||
|
|
|
@ -396,7 +396,7 @@ export class TestingExplorerView extends ViewPane {
|
|||
icon: group === TestRunProfileBitset.Run
|
||||
? icons.testingRunAllIcon
|
||||
: icons.testingDebugAllIcon,
|
||||
}, undefined, undefined, undefined);
|
||||
}, undefined, undefined, undefined, undefined);
|
||||
|
||||
const dropdownAction = new Action('selectRunConfig', 'Select Configuration...', 'codicon-chevron-down', true);
|
||||
|
||||
|
@ -486,7 +486,7 @@ class ResultSummaryView extends Disposable {
|
|||
{ ...new ReRunLastRun().desc, icon: icons.testingRerunIcon },
|
||||
{ ...new DebugLastRun().desc, icon: icons.testingDebugIcon },
|
||||
{},
|
||||
undefined,
|
||||
undefined, undefined
|
||||
), { icon: true, label: false });
|
||||
|
||||
this.render();
|
||||
|
|
|
@ -20,6 +20,7 @@ import { ICommandAction, ILocalizedString } from 'vs/platform/action/common/acti
|
|||
import { isEmptyObject, isString } from 'vs/base/common/types';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, ExtensionIdentifierMap, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
export const KEYBINDING_ENTRY_TEMPLATE_ID = 'keybinding.entry.template';
|
||||
|
||||
|
@ -33,9 +34,17 @@ interface ModifierLabels {
|
|||
user: ModLabels;
|
||||
}
|
||||
|
||||
export function createKeybindingCommandQuery(commandId: string, when?: string): string {
|
||||
const whenPart = when ? ` +when:${when}` : '';
|
||||
return `@command:${commandId}${whenPart}`;
|
||||
}
|
||||
|
||||
const wordFilter = or(matchesPrefix, matchesWords, matchesContiguousSubString);
|
||||
const COMMAND_REGEX = /@command:\s*([^\+]+)/i;
|
||||
const WHEN_REGEX = /\+when:\s*(.+)/i;
|
||||
const SOURCE_REGEX = /@source:\s*(user|default|system|extension)/i;
|
||||
const EXTENSION_REGEX = /@ext:\s*((".+")|([^\s]+))/i;
|
||||
const KEYBINDING_REGEX = /@keybinding:\s*((\".+\")|(\S+))/i;
|
||||
|
||||
export class KeybindingsEditorModel extends EditorModel {
|
||||
|
||||
|
@ -61,23 +70,38 @@ export class KeybindingsEditorModel extends EditorModel {
|
|||
fetch(searchValue: string, sortByPrecedence: boolean = false): IKeybindingItemEntry[] {
|
||||
let keybindingItems = sortByPrecedence ? this._keybindingItemsSortedByPrecedence : this._keybindingItems;
|
||||
|
||||
const commandIdMatches = /@command:\s*(.+)/i.exec(searchValue);
|
||||
// @command:COMMAND_ID
|
||||
const commandIdMatches = COMMAND_REGEX.exec(searchValue);
|
||||
if (commandIdMatches && commandIdMatches[1]) {
|
||||
return keybindingItems.filter(k => k.command === commandIdMatches[1])
|
||||
.map(keybindingItem => (<IKeybindingItemEntry>{ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem, templateId: KEYBINDING_ENTRY_TEMPLATE_ID }));
|
||||
const command = commandIdMatches[1].trim();
|
||||
let filteredKeybindingItems = keybindingItems.filter(k => k.command === command);
|
||||
|
||||
// +when:WHEN_EXPRESSION
|
||||
if (filteredKeybindingItems.length) {
|
||||
const whenMatches = WHEN_REGEX.exec(searchValue);
|
||||
if (whenMatches && whenMatches[1]) {
|
||||
const whenValue = whenMatches[1].trim();
|
||||
filteredKeybindingItems = this.filterByWhen(filteredKeybindingItems, command, whenValue);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredKeybindingItems.map(keybindingItem => (<IKeybindingItemEntry>{ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem, templateId: KEYBINDING_ENTRY_TEMPLATE_ID }));
|
||||
}
|
||||
|
||||
// @source:SOURCE
|
||||
if (SOURCE_REGEX.test(searchValue)) {
|
||||
keybindingItems = this.filterBySource(keybindingItems, searchValue);
|
||||
searchValue = searchValue.replace(SOURCE_REGEX, '');
|
||||
} else {
|
||||
// @ext:EXTENSION_ID
|
||||
const extensionMatches = EXTENSION_REGEX.exec(searchValue);
|
||||
if (extensionMatches && (extensionMatches[2] || extensionMatches[3])) {
|
||||
const extensionId = extensionMatches[2] ? extensionMatches[2].substring(1, extensionMatches[2].length - 1) : extensionMatches[3];
|
||||
keybindingItems = this.filterByExtension(keybindingItems, extensionId);
|
||||
searchValue = searchValue.replace(EXTENSION_REGEX, '');
|
||||
} else {
|
||||
const keybindingMatches = /@keybinding:\s*((\".+\")|(\S+))/i.exec(searchValue);
|
||||
// @keybinding:KEYBINDING
|
||||
const keybindingMatches = KEYBINDING_REGEX.exec(searchValue);
|
||||
if (keybindingMatches && (keybindingMatches[2] || keybindingMatches[3])) {
|
||||
searchValue = keybindingMatches[2] || `"${keybindingMatches[3]}"`;
|
||||
}
|
||||
|
@ -154,6 +178,26 @@ export class KeybindingsEditorModel extends EditorModel {
|
|||
return result;
|
||||
}
|
||||
|
||||
private filterByWhen(keybindingItems: IKeybindingItem[], command: string, when: string): IKeybindingItem[] {
|
||||
if (keybindingItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if a keybinding with the same command id and when clause exists
|
||||
const keybindingItemsWithWhen = keybindingItems.filter(k => k.when === when);
|
||||
if (keybindingItemsWithWhen.length) {
|
||||
return keybindingItemsWithWhen;
|
||||
}
|
||||
|
||||
// Create a new entry with the when clause which does not live in the model
|
||||
// We can reuse some of the properties from the same command with different when clause
|
||||
const commandLabel = keybindingItems[0].commandLabel;
|
||||
|
||||
const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, ContextKeyExpr.deserialize(when), false, null, false);
|
||||
const actionLabels = new Map([[command, commandLabel]]);
|
||||
return [KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, actionLabels, this.getExtensionsMapping())];
|
||||
}
|
||||
|
||||
private splitKeybindingWords(wordsSeparatedBySpaces: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const word of wordsSeparatedBySpaces) {
|
||||
|
@ -163,10 +207,7 @@ export class KeybindingsEditorModel extends EditorModel {
|
|||
}
|
||||
|
||||
override async resolve(actionLabels = new Map<string, string>()): Promise<void> {
|
||||
const extensions = new ExtensionIdentifierMap<IExtensionDescription>();
|
||||
for (const extension of this.extensionService.extensions) {
|
||||
extensions.set(extension.identifier, extension);
|
||||
}
|
||||
const extensions = this.getExtensionsMapping();
|
||||
|
||||
this._keybindingItemsSortedByPrecedence = [];
|
||||
const boundCommands: Map<string, boolean> = new Map<string, boolean>();
|
||||
|
@ -192,6 +233,14 @@ export class KeybindingsEditorModel extends EditorModel {
|
|||
return keybindingItem.command + (keybindingItem?.keybinding?.getAriaLabel() ?? '') + keybindingItem.when + (isString(keybindingItem.source) ? keybindingItem.source : keybindingItem.source.identifier.value);
|
||||
}
|
||||
|
||||
private getExtensionsMapping(): ExtensionIdentifierMap<IExtensionDescription> {
|
||||
const extensions = new ExtensionIdentifierMap<IExtensionDescription>();
|
||||
for (const extension of this.extensionService.extensions) {
|
||||
extensions.set(extension.identifier, extension);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
private static compareKeybindingData(a: IKeybindingItem, b: IKeybindingItem): number {
|
||||
if (a.keybinding && !b.keybinding) {
|
||||
return -1;
|
||||
|
|
Loading…
Reference in a new issue