From 68efb6fe56bd1476e3642ba5752930ce021f5fad Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 28 Dec 2022 00:43:18 +0100 Subject: [PATCH] fix #121673 (#170138) --- src/vs/platform/action/common/action.ts | 7 +- .../browser/editSessions.contribution.ts | 2 +- .../browser/extensions.contribution.ts | 18 +++- .../extensions/browser/extensionsActions.ts | 2 + .../browser/extensionsWorkbenchService.ts | 9 +- .../contrib/extensions/common/extensions.ts | 2 +- .../notebookKernelQuickPickStrategy.ts | 2 +- .../preferences/browser/keybindingsEditor.ts | 75 +++++++++++++--- .../browser/media/keybindingsEditor.css | 23 ++++- .../browser/preferences.contribution.ts | 4 +- .../common/newFile.contribution.ts | 2 +- .../actions/common/menusExtensionPoint.ts | 2 +- .../browser/keybindingsEditorModel.ts | 90 ++++++++++++------- .../preferences/common/preferences.ts | 5 +- .../browser/keybindingsEditorModel.test.ts | 39 ++++++-- 15 files changed, 217 insertions(+), 65 deletions(-) diff --git a/src/vs/platform/action/common/action.ts b/src/vs/platform/action/common/action.ts index d7723bc08aab9..35bee42711334 100644 --- a/src/vs/platform/action/common/action.ts +++ b/src/vs/platform/action/common/action.ts @@ -57,6 +57,11 @@ export function isICommandActionToggleInfo(thing: ContextKeyExpression | IComman return thing ? (thing).condition !== undefined : false; } +export interface ICommandActionSource { + readonly id: string; + readonly title: string; +} + export interface ICommandAction { id: string; title: string | ICommandActionTitle; @@ -64,7 +69,7 @@ export interface ICommandAction { category?: keyof typeof Categories | ILocalizedString | string; tooltip?: string | ILocalizedString; icon?: Icon; - source?: string; + source?: ICommandActionSource; precondition?: ContextKeyExpression; /** diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 7e0f9ef19f1b6..927a16160a227 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -773,7 +773,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo continueEditSessionOptions.push(new ContinueEditSessionItem( ThemeIcon.isThemeIcon(icon) ? `$(${icon.id}) ${title}` : title, command.id, - command.source, + command.source?.title, when, contribution.documentation )); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index cfba58b6d2975..1f0db41a77196 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1399,18 +1399,32 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')), + order: 1 }, run: async (accessor: ServicesAccessor, id: string) => accessor.get(IPreferencesService).openSettings({ jsonEditor: false, query: `@ext:${id}` }) }); + this.registerExtensionAction({ + id: 'workbench.extensions.action.configureKeybindings', + title: { value: localize('workbench.extensions.action.configureKeybindings', "Extension Keyboard Shortcuts"), original: 'Extension Keyboard Shortcuts' }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasKeybindings')), + order: 2 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query: `@ext:${id}` }) + }); + this.registerExtensionAction({ id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()), + order: 3 }, run: async (accessor: ServicesAccessor, id: string) => { const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 9859b28e168d5..a18a6132c4a80 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1014,6 +1014,8 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['extension', extension.identifier.id]); cksOverlay.push(['isBuiltinExtension', extension.isBuiltin]); cksOverlay.push(['extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration]); + cksOverlay.push(['extensionHasKeybindings', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.keybindings]); + cksOverlay.push(['extensionHasCommands', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes?.commands]); cksOverlay.push(['isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]]); cksOverlay.push(['isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace]); cksOverlay.push(['isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())]); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 11de06e5ff9a2..d0dc84aff9851 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1024,7 +1024,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return null; } - async open(extension: IExtension, options?: IExtensionEditorOptions): Promise { + async open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise { + if (typeof extension === 'string') { + const id = extension; + extension = this.installed.find(e => areSameExtensions(e.identifier, { id })) ?? (await this.getExtensions([{ id: extension }], CancellationToken.None))[0]; + } + if (!extension) { + throw new Error(`Extension not found. ${extension}`); + } const editor = await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP); if (options?.tab && editor instanceof ExtensionEditor) { await editor.openTab(options.tab); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 1bde0302f86da..f890a0e4f24a7 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -112,7 +112,7 @@ export interface IExtensionsWorkbenchService { setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise; setExtensionIgnoresUpdate(extension: IExtension, ignoreAutoUpate: boolean): void; isExtensionIgnoresUpdates(extension: IExtension): boolean; - open(extension: IExtension, options?: IExtensionEditorOptions): Promise; + open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 49236a94c495d..a224313e94657 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -815,7 +815,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { action: sourceAction, picked: false, label: sourceAction.action.label, - detail: (sourceAction.action as MenuItemAction)?.item?.source + detail: (sourceAction.action as MenuItemAction)?.item?.source?.title }; quickPickItems.push(res); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 0e71587ff2a83..fd74217a8f088 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { isIOS, OS } from 'vs/base/common/platform'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ToggleActionViewItem } from 'vs/base/browser/ui/toggle/toggle'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; @@ -49,6 +49,9 @@ import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browse import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { defaultInputBoxStyles, defaultKeybindingLabelStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { isString } from 'vs/base/common/types'; const $ = DOM.$; @@ -435,14 +438,14 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP { label: localize('when', "When"), tooltip: '', - weight: 0.4, + weight: 0.35, templateId: WhenColumnRenderer.TEMPLATE_ID, project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; } }, { label: localize('source', "Source"), tooltip: '', - weight: 0.1, + weight: 0.15, templateId: SourceColumnRenderer.TEMPLATE_ID, project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; } }, @@ -777,10 +780,11 @@ class Delegate implements ITableVirtualDelegate { if (element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) { const commandIdMatched = (element).keybindingItem.commandLabel && (element).commandIdMatches; const commandDefaultLabelMatched = !!(element).commandDefaultLabelMatches; + const extensionIdMatched = !!(element).extensionIdMatches; if (commandIdMatched && commandDefaultLabelMatched) { return 60; } - if (commandIdMatched || commandDefaultLabelMatched) { + if (extensionIdMatched || commandIdMatched || commandDefaultLabelMatched) { return 40; } } @@ -944,7 +948,26 @@ class KeybindingColumnRenderer implements ITableRenderer void): IDisposable { + const disposables: DisposableStore = new DisposableStore(); + disposables.add(DOM.addDisposableListener(element, DOM.EventType.CLICK, DOM.finalHandler(callback))); + disposables.add(DOM.addDisposableListener(element, DOM.EventType.KEY_UP, e => { + const keyboardEvent = new StandardKeyboardEvent(e); + if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) { + e.preventDefault(); + e.stopPropagation(); + callback(); + } + })); + return disposables; } class SourceColumnRenderer implements ITableRenderer { @@ -953,17 +976,49 @@ class SourceColumnRenderer implements ITableRenderer(extensionContainer, $('a.extension-label', { tabindex: 0 })); + const extensionId = new HighlightedLabel(DOM.append(extensionContainer, $('.extension-id-container.code'))); + return { sourceColumn, sourceLabel, extensionLabel, extensionContainer, extensionId, disposables: new DisposableStore() }; } renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: ISourceColumnTemplateData, height: number | undefined): void { - templateData.highlightedLabel.set(keybindingItemEntry.keybindingItem.source, keybindingItemEntry.sourceMatches); + + if (isString(keybindingItemEntry.keybindingItem.source)) { + templateData.extensionContainer.classList.add('hide'); + templateData.sourceLabel.element.classList.remove('hide'); + templateData.sourceColumn.title = ''; + templateData.sourceLabel.set(keybindingItemEntry.keybindingItem.source || '-', keybindingItemEntry.sourceMatches); + } else { + templateData.extensionContainer.classList.remove('hide'); + templateData.sourceLabel.element.classList.add('hide'); + const extension = keybindingItemEntry.keybindingItem.source; + const extensionLabel = extension.displayName ?? extension.identifier.value; + templateData.sourceColumn.title = localize('extension label', "Extension ({0})", extensionLabel); + templateData.extensionLabel.textContent = extensionLabel; + templateData.disposables.add(onClick(templateData.extensionLabel, () => { + this.extensionsWorkbenchService.open(extension.identifier.value); + })); + if (keybindingItemEntry.extensionIdMatches) { + templateData.extensionId.element.classList.remove('hide'); + templateData.extensionId.set(extension.identifier.value, keybindingItemEntry.extensionIdMatches); + } else { + templateData.extensionId.element.classList.add('hide'); + templateData.extensionId.set(undefined); + } + } } - disposeTemplate(templateData: ISourceColumnTemplateData): void { } + disposeTemplate(templateData: ISourceColumnTemplateData): void { + templateData.disposables.dispose(); + } } interface IWhenColumnTemplateData { @@ -1123,8 +1178,8 @@ class AccessibilityProvider implements IListAccessibilityProvider .keybindings-body > .keybindings-table-container .monaco-table-tr .monaco-table-td .command .hide { - display: none; -} - .keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table-tr .monaco-table-td .command.vertical-align-column { flex-direction: column; align-items: flex-start; @@ -164,6 +160,21 @@ height: 24px; } +/** Source column styling **/ +.keybindings-editor>.keybindings-body>.keybindings-table-container .monaco-table-tr .monaco-table-td .source a { + color: var(--vscode-textLink-foreground); + cursor: pointer; +} + +.keybindings-editor>.keybindings-body>.keybindings-table-container .monaco-table-tr .monaco-table-td .source a:hover { + text-decoration: underline; + color: var(--vscode-textLink-activeForeground); +} + +.keybindings-editor>.keybindings-body>.keybindings-table-container .monaco-table-tr .monaco-table-td .source a:active { + color: var(--vscode-textLink-activeForeground); +} + /** columns styling **/ .keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table-tr .monaco-table-td .command, @@ -177,6 +188,10 @@ text-overflow: ellipsis; } +.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table-tr .monaco-table-td .hide { + display: none; +} + .keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table-tr .monaco-table-td .code { font-family: var(--monaco-monospace-font); font-size: 90%; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index a845e61184269..0cdeb7f301e0d 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -935,7 +935,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor() { super({ id: KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, - title: { value: nls.localize('showDefaultKeybindings', "Show Default Keybindings"), original: 'Show Default Keybindings' }, + title: { value: nls.localize('showDefaultKeybindings', "Show System Keybindings"), original: 'Show System Keybindings' }, menu: [ { id: MenuId.EditorTitle, @@ -948,7 +948,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor) { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { - editorPane.search('@source:default'); + editorPane.search('@source:system'); } } }); diff --git a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts index 562f341296370..7a50bf9a74b56 100644 --- a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts +++ b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts @@ -73,7 +73,7 @@ class NewFileTemplatesManager extends Disposable { for (const [groupName, group] of this.menu.getActions({ renderShortTitle: true })) { for (const action of group) { if (action instanceof MenuItemAction) { - items.push({ commandID: action.item.id, from: action.item.source ?? builtInSource, title: action.label, group: groupName }); + items.push({ commandID: action.item.id, from: action.item.source?.title ?? builtInSource, title: action.label, group: groupName }); } } } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 629d38b20ebb4..19b2de2f6e41c 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -675,7 +675,7 @@ commandsExtensionPoint.setHandler(extensions => { _commandRegistrations.add(MenuRegistry.addCommand({ id: command, title, - source: extension.description.displayName ?? extension.description.name, + source: { id: extension.description.identifier.value, title: extension.description.displayName ?? extension.description.name }, shortTitle, tooltip: title, category, diff --git a/src/vs/workbench/services/preferences/browser/keybindingsEditorModel.ts b/src/vs/workbench/services/preferences/browser/keybindingsEditorModel.ts index 95e12f6ba5ebc..de9ed374f7b86 100644 --- a/src/vs/workbench/services/preferences/browser/keybindingsEditorModel.ts +++ b/src/vs/workbench/services/preferences/browser/keybindingsEditorModel.ts @@ -8,22 +8,22 @@ import { distinct, coalesce } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; import { OperatingSystem, Language } from 'vs/base/common/platform'; import { IMatch, IFilter, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ResolvedKeybinding, ResolvedChord } from 'vs/base/common/keybindings'; import { AriaLabelProvider, UserSettingsLabelProvider, UILabelProvider, ModifierLabels as ModLabels } from 'vs/base/common/keybindingLabels'; import { MenuRegistry } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { getAllUnboundCommands } from 'vs/workbench/services/keybinding/browser/unboundCommands'; import { IKeybindingItemEntry, KeybindingMatches, KeybindingMatch, IKeybindingItem } from 'vs/workbench/services/preferences/common/preferences'; import { ICommandAction, ILocalizedString } from 'vs/platform/action/common/action'; -import { isEmptyObject } from 'vs/base/common/types'; +import { isEmptyObject, isString } from 'vs/base/common/types'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export const KEYBINDING_ENTRY_TEMPLATE_ID = 'keybinding.entry.template'; -const SOURCE_DEFAULT = localize('default', "Default"); +const SOURCE_SYSTEM = localize('default', "System"); const SOURCE_EXTENSION = localize('extension', "Extension"); const SOURCE_USER = localize('user', "User"); @@ -34,6 +34,8 @@ interface ModifierLabels { } const wordFilter = or(matchesPrefix, matchesWords, matchesContiguousSubString); +const SOURCE_REGEX = /@source:\s*(user|default|system|extension)/i; +const EXTENSION_REGEX = /@ext:\s*((".+")|([^\s]+))/i; export class KeybindingsEditorModel extends EditorModel { @@ -43,7 +45,8 @@ export class KeybindingsEditorModel extends EditorModel { constructor( os: OperatingSystem, - @IKeybindingService private readonly keybindingsService: IKeybindingService + @IKeybindingService private readonly keybindingsService: IKeybindingService, + @IExtensionService private readonly extensionService: IExtensionService, ) { super(); this._keybindingItems = []; @@ -64,13 +67,20 @@ export class KeybindingsEditorModel extends EditorModel { .map(keybindingItem => ({ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem, templateId: KEYBINDING_ENTRY_TEMPLATE_ID })); } - if (/@source:\s*(user|default|extension)/i.test(searchValue)) { + if (SOURCE_REGEX.test(searchValue)) { keybindingItems = this.filterBySource(keybindingItems, searchValue); - searchValue = searchValue.replace(/@source:\s*(user|default|extension)/i, ''); + searchValue = searchValue.replace(SOURCE_REGEX, ''); } else { - const keybindingMatches = /@keybinding:\s*((\".+\")|(\S+))/i.exec(searchValue); - if (keybindingMatches && (keybindingMatches[2] || keybindingMatches[3])) { - searchValue = keybindingMatches[2] || `"${keybindingMatches[3]}"`; + 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); + if (keybindingMatches && (keybindingMatches[2] || keybindingMatches[3])) { + searchValue = keybindingMatches[2] || `"${keybindingMatches[3]}"`; + } } } @@ -83,18 +93,23 @@ export class KeybindingsEditorModel extends EditorModel { } private filterBySource(keybindingItems: IKeybindingItem[], searchValue: string): IKeybindingItem[] { - if (/@source:\s*default/i.test(searchValue)) { - return keybindingItems.filter(k => k.source === SOURCE_DEFAULT); + if (/@source:\s*default/i.test(searchValue) || /@source:\s*system/i.test(searchValue)) { + return keybindingItems.filter(k => k.source === SOURCE_SYSTEM); } if (/@source:\s*user/i.test(searchValue)) { return keybindingItems.filter(k => k.source === SOURCE_USER); } if (/@source:\s*extension/i.test(searchValue)) { - return keybindingItems.filter(k => k.source === SOURCE_EXTENSION); + return keybindingItems.filter(k => !isString(k.source) || k.source === SOURCE_EXTENSION); } return keybindingItems; } + private filterByExtension(keybindingItems: IKeybindingItem[], extension: string): IKeybindingItem[] { + extension = extension.toLowerCase().trim(); + return keybindingItems.filter(k => !isString(k.source) && (ExtensionIdentifier.equals(k.source.identifier, extension) || k.source.displayName?.toLowerCase() === extension.toLowerCase())); + } + private filterByText(keybindingItems: IKeybindingItem[], searchValue: string): IKeybindingItemEntry[] { const quoteAtFirstChar = searchValue.charAt(0) === '"'; const quoteAtLastChar = searchValue.charAt(searchValue.length - 1) === '"'; @@ -117,7 +132,10 @@ export class KeybindingsEditorModel extends EditorModel { || keybindingMatches.commandDefaultLabelMatches || keybindingMatches.sourceMatches || keybindingMatches.whenMatches - || keybindingMatches.keybindingMatches) { + || keybindingMatches.keybindingMatches + || keybindingMatches.extensionIdMatches + || keybindingMatches.extensionLabelMatches + ) { result.push({ id: KeybindingsEditorModel.getId(keybindingItem), templateId: KEYBINDING_ENTRY_TEMPLATE_ID, @@ -127,7 +145,9 @@ export class KeybindingsEditorModel extends EditorModel { keybindingMatches: keybindingMatches.keybindingMatches || undefined, commandIdMatches: keybindingMatches.commandIdMatches || undefined, sourceMatches: keybindingMatches.sourceMatches || undefined, - whenMatches: keybindingMatches.whenMatches || undefined + whenMatches: keybindingMatches.whenMatches || undefined, + extensionIdMatches: keybindingMatches.extensionIdMatches || undefined, + extensionLabelMatches: keybindingMatches.extensionLabelMatches || undefined }); } } @@ -143,13 +163,16 @@ export class KeybindingsEditorModel extends EditorModel { } override async resolve(actionLabels = new Map()): Promise { - const workbenchActionsRegistry = Registry.as(ActionExtensions.WorkbenchActions); + const extensions = new Map(); + for (const extension of this.extensionService.extensions) { + extensions.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } this._keybindingItemsSortedByPrecedence = []; const boundCommands: Map = new Map(); for (const keybinding of this.keybindingsService.getKeybindings()) { if (keybinding.command) { // Skip keybindings without commands - this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(keybinding.command, keybinding, workbenchActionsRegistry, actionLabels)); + this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(keybinding.command, keybinding, actionLabels, extensions)); boundCommands.set(keybinding.command, true); } } @@ -157,7 +180,7 @@ export class KeybindingsEditorModel extends EditorModel { const commandsWithDefaultKeybindings = this.keybindingsService.getDefaultKeybindings().map(keybinding => keybinding.command); for (const command of getAllUnboundCommands(boundCommands)) { const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1, null, false); - this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, workbenchActionsRegistry, actionLabels)); + this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, actionLabels, extensions)); } this._keybindingItemsSortedByPrecedence = distinct(this._keybindingItemsSortedByPrecedence, keybindingItem => KeybindingsEditorModel.getId(keybindingItem)); this._keybindingItems = this._keybindingItemsSortedByPrecedence.slice(0).sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b)); @@ -166,7 +189,7 @@ export class KeybindingsEditorModel extends EditorModel { } private static getId(keybindingItem: IKeybindingItem): string { - return keybindingItem.command + (keybindingItem.keybinding ? keybindingItem.keybinding.getAriaLabel() : '') + keybindingItem.source + keybindingItem.when; + return keybindingItem.command + (keybindingItem?.keybinding?.getAriaLabel() ?? '') + keybindingItem.when + (isString(keybindingItem.source) ? keybindingItem.source : keybindingItem.source.identifier.value); } private static compareKeybindingData(a: IKeybindingItem, b: IKeybindingItem): number { @@ -193,25 +216,24 @@ export class KeybindingsEditorModel extends EditorModel { return a.command.localeCompare(b.command); } - private static toKeybindingEntry(command: string, keybindingItem: ResolvedKeybindingItem, workbenchActionsRegistry: IWorkbenchActionRegistry, actions: Map): IKeybindingItem { - const menuCommand = MenuRegistry.getCommand(command)!; - const editorActionLabel = actions.get(command)!; + private static toKeybindingEntry(command: string, keybindingItem: ResolvedKeybindingItem, actions: Map, extensions: Map): IKeybindingItem { + const menuCommand = MenuRegistry.getCommand(command); + const editorActionLabel = actions.get(command); + const extensionId = keybindingItem.extensionId ?? (keybindingItem.resolvedKeybinding ? undefined : menuCommand?.source?.id); return { keybinding: keybindingItem.resolvedKeybinding, keybindingItem, command, commandLabel: KeybindingsEditorModel.getCommandLabel(menuCommand, editorActionLabel), - commandDefaultLabel: KeybindingsEditorModel.getCommandDefaultLabel(menuCommand, workbenchActionsRegistry), + commandDefaultLabel: KeybindingsEditorModel.getCommandDefaultLabel(menuCommand), when: keybindingItem.when ? keybindingItem.when.serialize() : '', - source: ( - keybindingItem.extensionId - ? (keybindingItem.isBuiltinExtension ? SOURCE_DEFAULT : SOURCE_EXTENSION) - : (keybindingItem.isDefault ? SOURCE_DEFAULT : SOURCE_USER) - ) + source: extensionId ? extensions.get(ExtensionIdentifier.toKey(extensionId)) ?? SOURCE_EXTENSION + : keybindingItem.isDefault ? SOURCE_SYSTEM : SOURCE_USER + }; } - private static getCommandDefaultLabel(menuCommand: ICommandAction, workbenchActionsRegistry: IWorkbenchActionRegistry): string | null { + private static getCommandDefaultLabel(menuCommand: ICommandAction | undefined): string | null { if (!Language.isDefaultVariant()) { if (menuCommand && menuCommand.title && (menuCommand.title).original) { const category: string | undefined = menuCommand.category ? (menuCommand.category).original : undefined; @@ -222,7 +244,7 @@ export class KeybindingsEditorModel extends EditorModel { return null; } - private static getCommandLabel(menuCommand: ICommandAction, editorActionLabel: string): string { + private static getCommandLabel(menuCommand: ICommandAction | undefined, editorActionLabel: string | undefined): string { if (menuCommand) { const category: string | undefined = menuCommand.category ? typeof menuCommand.category === 'string' ? menuCommand.category : menuCommand.category.value : undefined; const title = typeof menuCommand.title === 'string' ? menuCommand.title : menuCommand.title.value; @@ -245,14 +267,20 @@ class KeybindingItemMatches { readonly sourceMatches: IMatch[] | null = null; readonly whenMatches: IMatch[] | null = null; readonly keybindingMatches: KeybindingMatches | null = null; + readonly extensionIdMatches: IMatch[] | null = null; + readonly extensionLabelMatches: IMatch[] | null = null; constructor(private modifierLabels: ModifierLabels, keybindingItem: IKeybindingItem, searchValue: string, words: string[], keybindingWords: string[], completeMatch: boolean) { if (!completeMatch) { this.commandIdMatches = this.matches(searchValue, keybindingItem.command, or(matchesWords, matchesCamelCase), words); this.commandLabelMatches = keybindingItem.commandLabel ? this.matches(searchValue, keybindingItem.commandLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true), words) : null; this.commandDefaultLabelMatches = keybindingItem.commandDefaultLabel ? this.matches(searchValue, keybindingItem.commandDefaultLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandDefaultLabel, true), words) : null; - this.sourceMatches = this.matches(searchValue, keybindingItem.source, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.source, true), words); this.whenMatches = keybindingItem.when ? this.matches(null, keybindingItem.when, or(matchesWords, matchesCamelCase), words) : null; + if (isString(keybindingItem.source)) { + this.sourceMatches = this.matches(searchValue, keybindingItem.source, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.source as string, true), words); + } else { + this.extensionLabelMatches = keybindingItem.source.displayName ? this.matches(searchValue, keybindingItem.source.displayName, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true), words) : null; + } } this.keybindingMatches = keybindingItem.keybinding ? this.matchesKeybinding(keybindingItem.keybinding, searchValue, keybindingWords, completeMatch) : null; } diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index ca95051d2e12d..494a558ddbd8a 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -14,6 +14,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, EditPresentationTypes, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { DEFAULT_EDITOR_ASSOCIATION, IEditorPane } from 'vs/workbench/common/editor'; @@ -267,6 +268,8 @@ export interface IKeybindingItemEntry { commandLabelMatches?: IMatch[]; commandDefaultLabelMatches?: IMatch[]; sourceMatches?: IMatch[]; + extensionIdMatches?: IMatch[]; + extensionLabelMatches?: IMatch[]; whenMatches?: IMatch[]; keybindingMatches?: KeybindingMatches; } @@ -277,7 +280,7 @@ export interface IKeybindingItem { commandLabel: string; commandDefaultLabel: string; command: string; - source: string; + source: string | IExtensionDescription; when: string; } diff --git a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts index c3a718fb8dc43..29ec76bb1df63 100644 --- a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts @@ -18,7 +18,8 @@ import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayo import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/preferences'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; interface Modifiers { metaKey?: boolean; @@ -31,13 +32,17 @@ suite('KeybindingsEditorModel', () => { let instantiationService: TestInstantiationService; let testObject: KeybindingsEditorModel; + let extensions: Partial[] = []; setup(() => { + extensions = []; instantiationService = new TestInstantiationService(); instantiationService.stub(IKeybindingService, {}); - instantiationService.stub(IExtensionService, {}, 'whenInstalledExtensionsRegistered', () => Promise.resolve(null)); - + instantiationService.stub(IExtensionService, >{ + whenInstalledExtensionsRegistered: () => Promise.resolve(true), + get extensions() { return extensions; } + }); testObject = instantiationService.createInstance(KeybindingsEditorModel, OS); CommandsRegistry.registerCommand('command_without_keybinding', () => { }); @@ -227,13 +232,13 @@ suite('KeybindingsEditorModel', () => { assert.ok(actual); }); - test('filter by default source', async () => { + test('filter by system source', async () => { const command = 'a' + uuid.generateUuid(); const expected = aResolvedKeybindingItem({ command, firstChord: { keyCode: KeyCode.Escape }, when: 'context1 && context2' }); prepareKeybindingService(expected); await testObject.resolve(new Map()); - const actual = testObject.fetch('default').filter(element => element.keybindingItem.command === command)[0]; + const actual = testObject.fetch('system').filter(element => element.keybindingItem.command === command)[0]; assert.ok(actual); }); @@ -672,11 +677,29 @@ suite('KeybindingsEditorModel', () => { assert.deepStrictEqual(actual[0].keybindingMatches!.firstPart, { altKey: true }); }); + test('filter by extension', async () => { + testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh); + const command1 = `command.${uuid.generateUuid()}`; + const command2 = `command.${uuid.generateUuid()}`; + extensions.push({ identifier: new ExtensionIdentifier('foo'), displayName: 'foo bar' }, { identifier: new ExtensionIdentifier('bar'), displayName: 'bar foo' }); + MenuRegistry.addCommand({ id: command2, title: 'title', category: 'category', source: { id: extensions[1].identifier!.value, title: extensions[1].displayName! } }); + const expected = aResolvedKeybindingItem({ command: command1, firstChord: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, isDefault: true, extensionId: extensions[0].identifier!.value }); + prepareKeybindingService(expected, aResolvedKeybindingItem({ command: command2, isDefault: true })); + + await testObject.resolve(new Map()); + let actual = testObject.fetch('@ext:foo'); + assert.strictEqual(1, actual.length); + assert.deepStrictEqual(actual[0].keybindingItem.command, command1); + + actual = testObject.fetch('@ext:"bar foo"'); + assert.strictEqual(1, actual.length); + assert.deepStrictEqual(actual[0].keybindingItem.command, command2); + }); + function prepareKeybindingService(...keybindingItems: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] { instantiationService.stub(IKeybindingService, 'getKeybindings', () => keybindingItems); instantiationService.stub(IKeybindingService, 'getDefaultKeybindings', () => keybindingItems); return keybindingItems; - } function registerCommandWithTitle(command: string, title: string): void { @@ -717,7 +740,7 @@ suite('KeybindingsEditorModel', () => { } } - function aResolvedKeybindingItem({ command, when, isDefault, firstChord, secondChord }: { command?: string; when?: string; isDefault?: boolean; firstChord?: { keyCode: KeyCode; modifiers?: Modifiers }; secondChord?: { keyCode: KeyCode; modifiers?: Modifiers } }): ResolvedKeybindingItem { + function aResolvedKeybindingItem({ command, when, isDefault, firstChord, secondChord, extensionId }: { command?: string; when?: string; isDefault?: boolean; firstChord?: { keyCode: KeyCode; modifiers?: Modifiers }; secondChord?: { keyCode: KeyCode; modifiers?: Modifiers }; extensionId?: string }): ResolvedKeybindingItem { const aSimpleKeybinding = function (chord: { keyCode: KeyCode; modifiers?: Modifiers }): KeyCodeChord { const { ctrlKey, shiftKey, altKey, metaKey } = chord.modifiers || { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false }; return new KeyCodeChord(ctrlKey!, shiftKey!, altKey!, metaKey!, chord.keyCode); @@ -730,7 +753,7 @@ suite('KeybindingsEditorModel', () => { } } const keybinding = chords.length > 0 ? new USLayoutResolvedKeybinding(chords, OS) : undefined; - return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null, false); + return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, extensionId ?? null, false); } function asResolvedKeybindingItems(keybindingEntries: IKeybindingItemEntry[], keepUnassigned: boolean = false): ResolvedKeybindingItem[] {