Path: blob/main/src/vs/workbench/services/actions/common/menusExtensionPoint.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';7import * as resources from '../../../../base/common/resources.js';8import { IJSONSchema } from '../../../../base/common/jsonSchema.js';9import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';10import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';11import { MenuId, MenuRegistry, IMenuItem, ISubmenuItem } from '../../../../platform/actions/common/actions.js';12import { URI } from '../../../../base/common/uri.js';13import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';14import { ThemeIcon } from '../../../../base/common/themables.js';15import { index } from '../../../../base/common/arrays.js';16import { isProposedApiEnabled } from '../../extensions/common/extensions.js';17import { ILocalizedString } from '../../../../platform/action/common/action.js';18import { IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData, Extensions as ExtensionFeaturesExtensions } from '../../extensionManagement/common/extensionFeatures.js';19import { IExtensionManifest, IKeyBinding } from '../../../../platform/extensions/common/extensions.js';20import { Registry } from '../../../../platform/registry/common/platform.js';21import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';22import { platform } from '../../../../base/common/process.js';23import { MarkdownString } from '../../../../base/common/htmlContent.js';24import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';25import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';26import { ApiProposalName } from '../../../../platform/extensions/common/extensionsApiProposals.js';2728interface IAPIMenu {29readonly key: string;30readonly id: MenuId;31readonly description: string;32readonly proposed?: ApiProposalName;33readonly supportsSubmenus?: boolean; // defaults to true34}3536const apiMenus: IAPIMenu[] = [37{38key: 'commandPalette',39id: MenuId.CommandPalette,40description: localize('menus.commandPalette', "The Command Palette"),41supportsSubmenus: false42},43{44key: 'touchBar',45id: MenuId.TouchBarContext,46description: localize('menus.touchBar', "The touch bar (macOS only)"),47supportsSubmenus: false48},49{50key: 'editor/title',51id: MenuId.EditorTitle,52description: localize('menus.editorTitle', "The editor title menu")53},54{55key: 'editor/title/run',56id: MenuId.EditorTitleRun,57description: localize('menus.editorTitleRun', "Run submenu inside the editor title menu")58},59{60key: 'editor/context',61id: MenuId.EditorContext,62description: localize('menus.editorContext', "The editor context menu")63},64{65key: 'editor/context/copy',66id: MenuId.EditorContextCopy,67description: localize('menus.editorContextCopyAs', "'Copy as' submenu in the editor context menu")68},69{70key: 'editor/context/share',71id: MenuId.EditorContextShare,72description: localize('menus.editorContextShare', "'Share' submenu in the editor context menu"),73proposed: 'contribShareMenu'74},75{76key: 'explorer/context',77id: MenuId.ExplorerContext,78description: localize('menus.explorerContext', "The file explorer context menu")79},80{81key: 'explorer/context/share',82id: MenuId.ExplorerContextShare,83description: localize('menus.explorerContextShare', "'Share' submenu in the file explorer context menu"),84proposed: 'contribShareMenu'85},86{87key: 'editor/title/context',88id: MenuId.EditorTitleContext,89description: localize('menus.editorTabContext', "The editor tabs context menu")90},91{92key: 'editor/title/context/share',93id: MenuId.EditorTitleContextShare,94description: localize('menus.editorTitleContextShare', "'Share' submenu inside the editor title context menu"),95proposed: 'contribShareMenu'96},97{98key: 'debug/callstack/context',99id: MenuId.DebugCallStackContext,100description: localize('menus.debugCallstackContext', "The debug callstack view context menu")101},102{103key: 'debug/variables/context',104id: MenuId.DebugVariablesContext,105description: localize('menus.debugVariablesContext', "The debug variables view context menu")106},107{108key: 'debug/watch/context',109id: MenuId.DebugWatchContext,110description: localize('menus.debugWatchContext', "The debug watch view context menu")111},112{113key: 'debug/toolBar',114id: MenuId.DebugToolBar,115description: localize('menus.debugToolBar', "The debug toolbar menu")116},117{118key: 'debug/createConfiguration',119id: MenuId.DebugCreateConfiguration,120proposed: 'contribDebugCreateConfiguration',121description: localize('menus.debugCreateConfiguation', "The debug create configuration menu")122},123{124key: 'notebook/variables/context',125id: MenuId.NotebookVariablesContext,126description: localize('menus.notebookVariablesContext', "The notebook variables view context menu")127},128{129key: 'menuBar/home',130id: MenuId.MenubarHomeMenu,131description: localize('menus.home', "The home indicator context menu (web only)"),132proposed: 'contribMenuBarHome',133supportsSubmenus: false134},135{136key: 'menuBar/edit/copy',137id: MenuId.MenubarCopy,138description: localize('menus.opy', "'Copy as' submenu in the top level Edit menu")139},140{141key: 'scm/title',142id: MenuId.SCMTitle,143description: localize('menus.scmTitle', "The Source Control title menu")144},145{146key: 'scm/sourceControl',147id: MenuId.SCMSourceControl,148description: localize('menus.scmSourceControl', "The Source Control menu")149},150{151key: 'scm/sourceControl/title',152id: MenuId.SCMSourceControlTitle,153description: localize('menus.scmSourceControlTitle', "The Source Control title menu"),154proposed: 'contribSourceControlTitleMenu'155},156{157key: 'scm/resourceState/context',158id: MenuId.SCMResourceContext,159description: localize('menus.resourceStateContext', "The Source Control resource state context menu")160},161{162key: 'scm/resourceFolder/context',163id: MenuId.SCMResourceFolderContext,164description: localize('menus.resourceFolderContext', "The Source Control resource folder context menu")165},166{167key: 'scm/resourceGroup/context',168id: MenuId.SCMResourceGroupContext,169description: localize('menus.resourceGroupContext', "The Source Control resource group context menu")170},171{172key: 'scm/change/title',173id: MenuId.SCMChangeContext,174description: localize('menus.changeTitle', "The Source Control inline change menu")175},176{177key: 'scm/inputBox',178id: MenuId.SCMInputBox,179description: localize('menus.input', "The Source Control input box menu"),180proposed: 'contribSourceControlInputBoxMenu'181},182{183key: 'scm/history/title',184id: MenuId.SCMHistoryTitle,185description: localize('menus.scmHistoryTitle', "The Source Control History title menu"),186proposed: 'contribSourceControlHistoryTitleMenu'187},188{189key: 'scm/historyItem/context',190id: MenuId.SCMHistoryItemContext,191description: localize('menus.historyItemContext', "The Source Control history item context menu"),192proposed: 'contribSourceControlHistoryItemMenu'193},194{195key: 'scm/historyItem/hover',196id: MenuId.SCMHistoryItemHover,197description: localize('menus.historyItemHover', "The Source Control history item hover menu"),198proposed: 'contribSourceControlHistoryItemMenu'199},200{201key: 'scm/historyItemRef/context',202id: MenuId.SCMHistoryItemRefContext,203description: localize('menus.historyItemRefContext', "The Source Control history item reference context menu"),204proposed: 'contribSourceControlHistoryItemMenu'205},206{207key: 'statusBar/remoteIndicator',208id: MenuId.StatusBarRemoteIndicatorMenu,209description: localize('menus.statusBarRemoteIndicator', "The remote indicator menu in the status bar"),210supportsSubmenus: false211},212{213key: 'terminal/context',214id: MenuId.TerminalInstanceContext,215description: localize('menus.terminalContext', "The terminal context menu")216},217{218key: 'terminal/title/context',219id: MenuId.TerminalTabContext,220description: localize('menus.terminalTabContext', "The terminal tabs context menu")221},222{223key: 'view/title',224id: MenuId.ViewTitle,225description: localize('view.viewTitle', "The contributed view title menu")226},227{228key: 'viewContainer/title',229id: MenuId.ViewContainerTitle,230description: localize('view.containerTitle', "The contributed view container title menu"),231proposed: 'contribViewContainerTitle'232},233{234key: 'view/item/context',235id: MenuId.ViewItemContext,236description: localize('view.itemContext', "The contributed view item context menu")237},238{239key: 'comments/comment/editorActions',240id: MenuId.CommentEditorActions,241description: localize('commentThread.editorActions', "The contributed comment editor actions"),242proposed: 'contribCommentEditorActionsMenu'243},244{245key: 'comments/commentThread/title',246id: MenuId.CommentThreadTitle,247description: localize('commentThread.title', "The contributed comment thread title menu")248},249{250key: 'comments/commentThread/context',251id: MenuId.CommentThreadActions,252description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"),253supportsSubmenus: false254},255{256key: 'comments/commentThread/additionalActions',257id: MenuId.CommentThreadAdditionalActions,258description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"),259supportsSubmenus: true,260proposed: 'contribCommentThreadAdditionalMenu'261},262{263key: 'comments/commentThread/title/context',264id: MenuId.CommentThreadTitleContext,265description: localize('commentThread.titleContext', "The contributed comment thread title's peek context menu, rendered as a right click menu on the comment thread's peek title."),266proposed: 'contribCommentPeekContext'267},268{269key: 'comments/comment/title',270id: MenuId.CommentTitle,271description: localize('comment.title', "The contributed comment title menu")272},273{274key: 'comments/comment/context',275id: MenuId.CommentActions,276description: localize('comment.actions', "The contributed comment context menu, rendered as buttons below the comment editor"),277supportsSubmenus: false278},279{280key: 'comments/commentThread/comment/context',281id: MenuId.CommentThreadCommentContext,282description: localize('comment.commentContext', "The contributed comment context menu, rendered as a right click menu on the an individual comment in the comment thread's peek view."),283proposed: 'contribCommentPeekContext'284},285{286key: 'commentsView/commentThread/context',287id: MenuId.CommentsViewThreadActions,288description: localize('commentsView.threadActions', "The contributed comment thread context menu in the comments view"),289proposed: 'contribCommentsViewThreadMenus'290},291{292key: 'notebook/toolbar',293id: MenuId.NotebookToolbar,294description: localize('notebook.toolbar', "The contributed notebook toolbar menu")295},296{297key: 'notebook/kernelSource',298id: MenuId.NotebookKernelSource,299description: localize('notebook.kernelSource', "The contributed notebook kernel sources menu"),300proposed: 'notebookKernelSource'301},302{303key: 'notebook/cell/title',304id: MenuId.NotebookCellTitle,305description: localize('notebook.cell.title', "The contributed notebook cell title menu")306},307{308key: 'notebook/cell/execute',309id: MenuId.NotebookCellExecute,310description: localize('notebook.cell.execute', "The contributed notebook cell execution menu")311},312{313key: 'interactive/toolbar',314id: MenuId.InteractiveToolbar,315description: localize('interactive.toolbar', "The contributed interactive toolbar menu"),316},317{318key: 'interactive/cell/title',319id: MenuId.InteractiveCellTitle,320description: localize('interactive.cell.title', "The contributed interactive cell title menu"),321},322{323key: 'issue/reporter',324id: MenuId.IssueReporter,325description: localize('issue.reporter', "The contributed issue reporter menu")326},327{328key: 'testing/item/context',329id: MenuId.TestItem,330description: localize('testing.item.context', "The contributed test item menu"),331},332{333key: 'testing/item/gutter',334id: MenuId.TestItemGutter,335description: localize('testing.item.gutter.title', "The menu for a gutter decoration for a test item"),336},337{338key: 'testing/profiles/context',339id: MenuId.TestProfilesContext,340description: localize('testing.profiles.context.title', "The menu for configuring testing profiles."),341},342{343key: 'testing/item/result',344id: MenuId.TestPeekElement,345description: localize('testing.item.result.title', "The menu for an item in the Test Results view or peek."),346},347{348key: 'testing/message/context',349id: MenuId.TestMessageContext,350description: localize('testing.message.context.title', "A prominent button overlaying editor content where the message is displayed"),351},352{353key: 'testing/message/content',354id: MenuId.TestMessageContent,355description: localize('testing.message.content.title', "Context menu for the message in the results tree"),356},357{358key: 'extension/context',359id: MenuId.ExtensionContext,360description: localize('menus.extensionContext', "The extension context menu")361},362{363key: 'timeline/title',364id: MenuId.TimelineTitle,365description: localize('view.timelineTitle', "The Timeline view title menu")366},367{368key: 'timeline/item/context',369id: MenuId.TimelineItemContext,370description: localize('view.timelineContext', "The Timeline view item context menu")371},372{373key: 'ports/item/context',374id: MenuId.TunnelContext,375description: localize('view.tunnelContext', "The Ports view item context menu")376},377{378key: 'ports/item/origin/inline',379id: MenuId.TunnelOriginInline,380description: localize('view.tunnelOriginInline', "The Ports view item origin inline menu")381},382{383key: 'ports/item/port/inline',384id: MenuId.TunnelPortInline,385description: localize('view.tunnelPortInline', "The Ports view item port inline menu")386},387{388key: 'file/newFile',389id: MenuId.NewFile,390description: localize('file.newFile', "The 'New File...' quick pick, shown on welcome page and File menu."),391supportsSubmenus: false,392},393{394key: 'webview/context',395id: MenuId.WebviewContext,396description: localize('webview.context', "The webview context menu")397},398{399key: 'file/share',400id: MenuId.MenubarShare,401description: localize('menus.share', "Share submenu shown in the top level File menu."),402proposed: 'contribShareMenu'403},404{405key: 'editor/inlineCompletions/actions',406id: MenuId.InlineCompletionsActions,407description: localize('inlineCompletions.actions', "The actions shown when hovering on an inline completion"),408supportsSubmenus: false,409proposed: 'inlineCompletionsAdditions'410},411{412key: 'editor/content',413id: MenuId.EditorContent,414description: localize('merge.toolbar', "The prominent button in an editor, overlays its content"),415proposed: 'contribEditorContentMenu'416},417{418key: 'editor/lineNumber/context',419id: MenuId.EditorLineNumberContext,420description: localize('editorLineNumberContext', "The contributed editor line number context menu")421},422{423key: 'mergeEditor/result/title',424id: MenuId.MergeInputResultToolbar,425description: localize('menus.mergeEditorResult', "The result toolbar of the merge editor"),426proposed: 'contribMergeEditorMenus'427},428{429key: 'multiDiffEditor/resource/title',430id: MenuId.MultiDiffEditorFileToolbar,431description: localize('menus.multiDiffEditorResource', "The resource toolbar in the multi diff editor"),432proposed: 'contribMultiDiffEditorMenus'433},434{435key: 'diffEditor/gutter/hunk',436id: MenuId.DiffEditorHunkToolbar,437description: localize('menus.diffEditorGutterToolBarMenus', "The gutter toolbar in the diff editor"),438proposed: 'contribDiffEditorGutterToolBarMenus'439},440{441key: 'diffEditor/gutter/selection',442id: MenuId.DiffEditorSelectionToolbar,443description: localize('menus.diffEditorGutterToolBarMenus', "The gutter toolbar in the diff editor"),444proposed: 'contribDiffEditorGutterToolBarMenus'445},446{447key: 'searchPanel/aiResults/commands',448id: MenuId.SearchActionMenu,449description: localize('searchPanel.aiResultsCommands', "The commands that will contribute to the menu rendered as buttons next to the AI search title"),450},451{452key: 'editor/context/chat',453id: MenuId.ChatTextEditorMenu,454description: localize('menus.chatTextEditor', "The Chat submenu in the text editor context menu."),455supportsSubmenus: false,456proposed: 'chatParticipantPrivate'457},458{459key: 'chat/chatSessions',460id: MenuId.ChatSessionsMenu,461description: localize('menus.chatSessions', "The Chat Sessions menu."),462supportsSubmenus: false,463proposed: 'chatSessionsProvider'464},465{466key: 'chat/multiDiff/context',467id: MenuId.ChatMultiDiffContext,468description: localize('menus.chatMultiDiffContext', "The Chat Multi-Diff context menu."),469supportsSubmenus: false,470proposed: 'chatSessionsProvider',471},472];473474namespace schema {475476// --- menus, submenus contribution point477478export interface IUserFriendlyMenuItem {479command: string;480alt?: string;481when?: string;482group?: string;483}484485export interface IUserFriendlySubmenuItem {486submenu: string;487when?: string;488group?: string;489}490491export interface IUserFriendlySubmenu {492id: string;493label: string;494icon?: IUserFriendlyIcon;495}496497export function isMenuItem(item: IUserFriendlyMenuItem | IUserFriendlySubmenuItem): item is IUserFriendlyMenuItem {498return typeof (item as IUserFriendlyMenuItem).command === 'string';499}500501export function isValidMenuItem(item: IUserFriendlyMenuItem, collector: ExtensionMessageCollector): boolean {502if (typeof item.command !== 'string') {503collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));504return false;505}506if (item.alt && typeof item.alt !== 'string') {507collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'alt'));508return false;509}510if (item.when && typeof item.when !== 'string') {511collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));512return false;513}514if (item.group && typeof item.group !== 'string') {515collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group'));516return false;517}518519return true;520}521522export function isValidSubmenuItem(item: IUserFriendlySubmenuItem, collector: ExtensionMessageCollector): boolean {523if (typeof item.submenu !== 'string') {524collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'submenu'));525return false;526}527if (item.when && typeof item.when !== 'string') {528collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));529return false;530}531if (item.group && typeof item.group !== 'string') {532collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group'));533return false;534}535536return true;537}538539export function isValidItems(items: (IUserFriendlyMenuItem | IUserFriendlySubmenuItem)[], collector: ExtensionMessageCollector): boolean {540if (!Array.isArray(items)) {541collector.error(localize('requirearray', "submenu items must be an array"));542return false;543}544545for (const item of items) {546if (isMenuItem(item)) {547if (!isValidMenuItem(item, collector)) {548return false;549}550} else {551if (!isValidSubmenuItem(item, collector)) {552return false;553}554}555}556557return true;558}559560export function isValidSubmenu(submenu: IUserFriendlySubmenu, collector: ExtensionMessageCollector): boolean {561if (typeof submenu !== 'object') {562collector.error(localize('require', "submenu items must be an object"));563return false;564}565566if (typeof submenu.id !== 'string') {567collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id'));568return false;569}570if (typeof submenu.label !== 'string') {571collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'label'));572return false;573}574575return true;576}577578const menuItem: IJSONSchema = {579type: 'object',580required: ['command'],581properties: {582command: {583description: localize('vscode.extension.contributes.menuItem.command', 'Identifier of the command to execute. The command must be declared in the \'commands\'-section'),584type: 'string'585},586alt: {587description: localize('vscode.extension.contributes.menuItem.alt', 'Identifier of an alternative command to execute. The command must be declared in the \'commands\'-section'),588type: 'string'589},590when: {591description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'),592type: 'string'593},594group: {595description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'),596type: 'string'597}598}599};600601const submenuItem: IJSONSchema = {602type: 'object',603required: ['submenu'],604properties: {605submenu: {606description: localize('vscode.extension.contributes.menuItem.submenu', 'Identifier of the submenu to display in this item.'),607type: 'string'608},609when: {610description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'),611type: 'string'612},613group: {614description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'),615type: 'string'616}617}618};619620const submenu: IJSONSchema = {621type: 'object',622required: ['id', 'label'],623properties: {624id: {625description: localize('vscode.extension.contributes.submenu.id', 'Identifier of the menu to display as a submenu.'),626type: 'string'627},628label: {629description: localize('vscode.extension.contributes.submenu.label', 'The label of the menu item which leads to this submenu.'),630type: 'string'631},632icon: {633description: localize({ key: 'vscode.extension.contributes.submenu.icon', comment: ['do not translate or change "\\$(zap)", \\ in front of $ is important.'] }, '(Optional) Icon which is used to represent the submenu in the UI. Either a file path, an object with file paths for dark and light themes, or a theme icon references, like "\\$(zap)"'),634anyOf: [{635type: 'string'636},637{638type: 'object',639properties: {640light: {641description: localize('vscode.extension.contributes.submenu.icon.light', 'Icon path when a light theme is used'),642type: 'string'643},644dark: {645description: localize('vscode.extension.contributes.submenu.icon.dark', 'Icon path when a dark theme is used'),646type: 'string'647}648}649}]650}651}652};653654export const menusContribution: IJSONSchema = {655description: localize('vscode.extension.contributes.menus', "Contributes menu items to the editor"),656type: 'object',657properties: index(apiMenus, menu => menu.key, menu => ({658markdownDescription: menu.proposed ? localize('proposed', "Proposed API, requires `enabledApiProposal: [\"{0}\"]` - {1}", menu.proposed, menu.description) : menu.description,659type: 'array',660items: menu.supportsSubmenus === false ? menuItem : { oneOf: [menuItem, submenuItem] }661})),662additionalProperties: {663description: 'Submenu',664type: 'array',665items: { oneOf: [menuItem, submenuItem] }666}667};668669export const submenusContribution: IJSONSchema = {670description: localize('vscode.extension.contributes.submenus', "Contributes submenu items to the editor"),671type: 'array',672items: submenu673};674675// --- commands contribution point676677export interface IUserFriendlyCommand {678command: string;679title: string | ILocalizedString;680shortTitle?: string | ILocalizedString;681enablement?: string;682category?: string | ILocalizedString;683icon?: IUserFriendlyIcon;684}685686export type IUserFriendlyIcon = string | { light: string; dark: string };687688export function isValidCommand(command: IUserFriendlyCommand, collector: ExtensionMessageCollector): boolean {689if (!command) {690collector.error(localize('nonempty', "expected non-empty value."));691return false;692}693if (isFalsyOrWhitespace(command.command)) {694collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));695return false;696}697if (!isValidLocalizedString(command.title, collector, 'title')) {698return false;699}700if (command.shortTitle && !isValidLocalizedString(command.shortTitle, collector, 'shortTitle')) {701return false;702}703if (command.enablement && typeof command.enablement !== 'string') {704collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'precondition'));705return false;706}707if (command.category && !isValidLocalizedString(command.category, collector, 'category')) {708return false;709}710if (!isValidIcon(command.icon, collector)) {711return false;712}713return true;714}715716function isValidIcon(icon: IUserFriendlyIcon | undefined, collector: ExtensionMessageCollector): boolean {717if (typeof icon === 'undefined') {718return true;719}720if (typeof icon === 'string') {721return true;722} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {723return true;724}725collector.error(localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));726return false;727}728729function isValidLocalizedString(localized: string | ILocalizedString, collector: ExtensionMessageCollector, propertyName: string): boolean {730if (typeof localized === 'undefined') {731collector.error(localize('requireStringOrObject', "property `{0}` is mandatory and must be of type `string` or `object`", propertyName));732return false;733} else if (typeof localized === 'string' && isFalsyOrWhitespace(localized)) {734collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", propertyName));735return false;736} else if (typeof localized !== 'string' && (isFalsyOrWhitespace(localized.original) || isFalsyOrWhitespace(localized.value))) {737collector.error(localize('requirestrings', "properties `{0}` and `{1}` are mandatory and must be of type `string`", `${propertyName}.value`, `${propertyName}.original`));738return false;739}740741return true;742}743744const commandType: IJSONSchema = {745type: 'object',746required: ['command', 'title'],747properties: {748command: {749description: localize('vscode.extension.contributes.commandType.command', 'Identifier of the command to execute'),750type: 'string'751},752title: {753description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'),754type: 'string'755},756shortTitle: {757markdownDescription: localize('vscode.extension.contributes.commandType.shortTitle', '(Optional) Short title by which the command is represented in the UI. Menus pick either `title` or `shortTitle` depending on the context in which they show commands.'),758type: 'string'759},760category: {761description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by which the command is grouped in the UI'),762type: 'string'763},764enablement: {765description: localize('vscode.extension.contributes.commandType.precondition', '(Optional) Condition which must be true to enable the command in the UI (menu and keybindings). Does not prevent executing the command by other means, like the `executeCommand`-api.'),766type: 'string'767},768icon: {769description: localize({ key: 'vscode.extension.contributes.commandType.icon', comment: ['do not translate or change "\\$(zap)", \\ in front of $ is important.'] }, '(Optional) Icon which is used to represent the command in the UI. Either a file path, an object with file paths for dark and light themes, or a theme icon references, like "\\$(zap)"'),770anyOf: [{771type: 'string'772},773{774type: 'object',775properties: {776light: {777description: localize('vscode.extension.contributes.commandType.icon.light', 'Icon path when a light theme is used'),778type: 'string'779},780dark: {781description: localize('vscode.extension.contributes.commandType.icon.dark', 'Icon path when a dark theme is used'),782type: 'string'783}784}785}]786}787}788};789790export const commandsContribution: IJSONSchema = {791description: localize('vscode.extension.contributes.commands', "Contributes commands to the command palette."),792oneOf: [793commandType,794{795type: 'array',796items: commandType797}798]799};800}801802const _commandRegistrations = new DisposableStore();803804export const commandsExtensionPoint = ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlyCommand | schema.IUserFriendlyCommand[]>({805extensionPoint: 'commands',806jsonSchema: schema.commandsContribution,807activationEventsGenerator: (contribs: schema.IUserFriendlyCommand[], result: { push(item: string): void }) => {808for (const contrib of contribs) {809if (contrib.command) {810result.push(`onCommand:${contrib.command}`);811}812}813}814});815816commandsExtensionPoint.setHandler(extensions => {817818function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser<any>) {819820if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) {821return;822}823824const { icon, enablement, category, title, shortTitle, command } = userFriendlyCommand;825826let absoluteIcon: { dark: URI; light?: URI } | ThemeIcon | undefined;827if (icon) {828if (typeof icon === 'string') {829absoluteIcon = ThemeIcon.fromString(icon) ?? { dark: resources.joinPath(extension.description.extensionLocation, icon), light: resources.joinPath(extension.description.extensionLocation, icon) };830831} else {832absoluteIcon = {833dark: resources.joinPath(extension.description.extensionLocation, icon.dark),834light: resources.joinPath(extension.description.extensionLocation, icon.light)835};836}837}838839const existingCmd = MenuRegistry.getCommand(command);840if (existingCmd) {841if (existingCmd.source) {842extension.collector.info(localize('dup1', "Command `{0}` already registered by {1} ({2})", userFriendlyCommand.command, existingCmd.source.title, existingCmd.source.id));843} else {844extension.collector.info(localize('dup0', "Command `{0}` already registered", userFriendlyCommand.command));845}846}847_commandRegistrations.add(MenuRegistry.addCommand({848id: command,849title,850source: { id: extension.description.identifier.value, title: extension.description.displayName ?? extension.description.name },851shortTitle,852tooltip: title,853category,854precondition: ContextKeyExpr.deserialize(enablement),855icon: absoluteIcon856}));857}858859// remove all previous command registrations860_commandRegistrations.clear();861862for (const extension of extensions) {863const { value } = extension;864if (Array.isArray(value)) {865for (const command of value) {866handleCommand(command, extension);867}868} else {869handleCommand(value, extension);870}871}872});873874interface IRegisteredSubmenu {875readonly id: MenuId;876readonly label: string;877readonly icon?: { dark: URI; light?: URI } | ThemeIcon;878}879880const _submenus = new Map<string, IRegisteredSubmenu>();881882const submenusExtensionPoint = ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlySubmenu[]>({883extensionPoint: 'submenus',884jsonSchema: schema.submenusContribution885});886887submenusExtensionPoint.setHandler(extensions => {888889_submenus.clear();890891for (const extension of extensions) {892const { value, collector } = extension;893894for (const [, submenuInfo] of Object.entries(value)) {895896if (!schema.isValidSubmenu(submenuInfo, collector)) {897continue;898}899900if (!submenuInfo.id) {901collector.warn(localize('submenuId.invalid.id', "`{0}` is not a valid submenu identifier", submenuInfo.id));902continue;903}904if (_submenus.has(submenuInfo.id)) {905collector.info(localize('submenuId.duplicate.id', "The `{0}` submenu was already previously registered.", submenuInfo.id));906continue;907}908if (!submenuInfo.label) {909collector.warn(localize('submenuId.invalid.label', "`{0}` is not a valid submenu label", submenuInfo.label));910continue;911}912913let absoluteIcon: { dark: URI; light?: URI } | ThemeIcon | undefined;914if (submenuInfo.icon) {915if (typeof submenuInfo.icon === 'string') {916absoluteIcon = ThemeIcon.fromString(submenuInfo.icon) || { dark: resources.joinPath(extension.description.extensionLocation, submenuInfo.icon) };917} else {918absoluteIcon = {919dark: resources.joinPath(extension.description.extensionLocation, submenuInfo.icon.dark),920light: resources.joinPath(extension.description.extensionLocation, submenuInfo.icon.light)921};922}923}924925const item: IRegisteredSubmenu = {926id: MenuId.for(`api:${submenuInfo.id}`),927label: submenuInfo.label,928icon: absoluteIcon929};930931_submenus.set(submenuInfo.id, item);932}933}934});935936const _apiMenusByKey = new Map(apiMenus.map(menu => ([menu.key, menu])));937const _menuRegistrations = new DisposableStore();938const _submenuMenuItems = new Map<string /* menu id */, Set<string /* submenu id */>>();939940const menusExtensionPoint = ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: (schema.IUserFriendlyMenuItem | schema.IUserFriendlySubmenuItem)[] }>({941extensionPoint: 'menus',942jsonSchema: schema.menusContribution,943deps: [submenusExtensionPoint]944});945946menusExtensionPoint.setHandler(extensions => {947948// remove all previous menu registrations949_menuRegistrations.clear();950_submenuMenuItems.clear();951952for (const extension of extensions) {953const { value, collector } = extension;954955for (const entry of Object.entries(value)) {956if (!schema.isValidItems(entry[1], collector)) {957continue;958}959960let menu = _apiMenusByKey.get(entry[0]);961962if (!menu) {963const submenu = _submenus.get(entry[0]);964965if (submenu) {966menu = {967key: entry[0],968id: submenu.id,969description: ''970};971}972}973974if (!menu) {975continue;976}977978if (menu.proposed && !isProposedApiEnabled(extension.description, menu.proposed)) {979collector.error(localize('proposedAPI.invalid', "{0} is a proposed menu identifier. It requires 'package.json#enabledApiProposals: [\"{1}\"]' and is only available when running out of dev or with the following command line switch: --enable-proposed-api {2}", entry[0], menu.proposed, extension.description.identifier.value));980continue;981}982983for (const menuItem of entry[1]) {984let item: IMenuItem | ISubmenuItem;985986if (schema.isMenuItem(menuItem)) {987const command = MenuRegistry.getCommand(menuItem.command);988const alt = menuItem.alt && MenuRegistry.getCommand(menuItem.alt) || undefined;989990if (!command) {991collector.error(localize('missing.command', "Menu item references a command `{0}` which is not defined in the 'commands' section.", menuItem.command));992continue;993}994if (menuItem.alt && !alt) {995collector.warn(localize('missing.altCommand', "Menu item references an alt-command `{0}` which is not defined in the 'commands' section.", menuItem.alt));996}997if (menuItem.command === menuItem.alt) {998collector.info(localize('dupe.command', "Menu item references the same command as default and alt-command"));999}10001001item = { command, alt, group: undefined, order: undefined, when: undefined };1002} else {1003if (menu.supportsSubmenus === false) {1004collector.error(localize('unsupported.submenureference', "Menu item references a submenu for a menu which doesn't have submenu support."));1005continue;1006}10071008const submenu = _submenus.get(menuItem.submenu);10091010if (!submenu) {1011collector.error(localize('missing.submenu', "Menu item references a submenu `{0}` which is not defined in the 'submenus' section.", menuItem.submenu));1012continue;1013}10141015let submenuRegistrations = _submenuMenuItems.get(menu.id.id);10161017if (!submenuRegistrations) {1018submenuRegistrations = new Set();1019_submenuMenuItems.set(menu.id.id, submenuRegistrations);1020}10211022if (submenuRegistrations.has(submenu.id.id)) {1023collector.warn(localize('submenuItem.duplicate', "The `{0}` submenu was already contributed to the `{1}` menu.", menuItem.submenu, entry[0]));1024continue;1025}10261027submenuRegistrations.add(submenu.id.id);10281029item = { submenu: submenu.id, icon: submenu.icon, title: submenu.label, group: undefined, order: undefined, when: undefined };1030}10311032if (menuItem.group) {1033const idx = menuItem.group.lastIndexOf('@');1034if (idx > 0) {1035item.group = menuItem.group.substr(0, idx);1036item.order = Number(menuItem.group.substr(idx + 1)) || undefined;1037} else {1038item.group = menuItem.group;1039}1040}10411042if (menu.id === MenuId.ViewContainerTitle && !menuItem.when?.includes('viewContainer == workbench.view.debug')) {1043// Not a perfect check but enough to communicate that this proposed extension point is currently only for the debug view container1044collector.error(localize('viewContainerTitle.when', "The {0} menu contribution must check {1} in its {2} clause.", '`viewContainer/title`', '`viewContainer == workbench.view.debug`', '"when"'));1045continue;1046}10471048item.when = ContextKeyExpr.deserialize(menuItem.when);1049_menuRegistrations.add(MenuRegistry.appendMenuItem(menu.id, item));1050}1051}1052}1053});10541055class CommandsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {10561057readonly type = 'table';10581059constructor(1060@IKeybindingService private readonly _keybindingService: IKeybindingService1061) { super(); }10621063shouldRender(manifest: IExtensionManifest): boolean {1064return !!manifest.contributes?.commands;1065}10661067render(manifest: IExtensionManifest): IRenderedData<ITableData> {1068const rawCommands = manifest.contributes?.commands || [];1069const commands = rawCommands.map(c => ({1070id: c.command,1071title: c.title,1072keybindings: [] as ResolvedKeybinding[],1073menus: [] as string[]1074}));10751076const byId = index(commands, c => c.id);10771078const menus = manifest.contributes?.menus || {};10791080// Add to commandPalette array any commands not explicitly contributed to it1081const implicitlyOnCommandPalette = index(commands, c => c.id);1082if (menus['commandPalette']) {1083for (const command of menus['commandPalette']) {1084delete implicitlyOnCommandPalette[command.command];1085}1086}10871088if (Object.keys(implicitlyOnCommandPalette).length) {1089if (!menus['commandPalette']) {1090menus['commandPalette'] = [];1091}1092for (const command in implicitlyOnCommandPalette) {1093menus['commandPalette'].push({ command });1094}1095}10961097for (const context in menus) {1098for (const menu of menus[context]) {10991100// This typically happens for the commandPalette context1101if (menu.when === 'false') {1102continue;1103}1104if (menu.command) {1105let command = byId[menu.command];1106if (command) {1107if (!command.menus.includes(context)) {1108command.menus.push(context);1109}1110} else {1111command = { id: menu.command, title: '', keybindings: [], menus: [context] };1112byId[command.id] = command;1113commands.push(command);1114}1115}1116}1117}11181119const rawKeybindings = manifest.contributes?.keybindings ? (Array.isArray(manifest.contributes.keybindings) ? manifest.contributes.keybindings : [manifest.contributes.keybindings]) : [];11201121rawKeybindings.forEach(rawKeybinding => {1122const keybinding = this.resolveKeybinding(rawKeybinding);11231124if (!keybinding) {1125return;1126}11271128let command = byId[rawKeybinding.command];11291130if (command) {1131command.keybindings.push(keybinding);1132} else {1133command = { id: rawKeybinding.command, title: '', keybindings: [keybinding], menus: [] };1134byId[command.id] = command;1135commands.push(command);1136}1137});11381139if (!commands.length) {1140return { data: { headers: [], rows: [] }, dispose: () => { } };1141}11421143const headers = [1144localize('command name', "ID"),1145localize('command title', "Title"),1146localize('keyboard shortcuts', "Keyboard Shortcuts"),1147localize('menuContexts', "Menu Contexts")1148];11491150const rows: IRowData[][] = commands.sort((a, b) => a.id.localeCompare(b.id))1151.map(command => {1152return [1153new MarkdownString().appendMarkdown(`\`${command.id}\``),1154typeof command.title === 'string' ? command.title : command.title.value,1155command.keybindings,1156new MarkdownString().appendMarkdown(`${command.menus.sort((a, b) => a.localeCompare(b)).map(menu => `\`${menu}\``).join(' ')}`),1157];1158});11591160return {1161data: {1162headers,1163rows1164},1165dispose: () => { }1166};1167}11681169private resolveKeybinding(rawKeyBinding: IKeyBinding): ResolvedKeybinding | undefined {1170let key: string | undefined;11711172switch (platform) {1173case 'win32': key = rawKeyBinding.win; break;1174case 'linux': key = rawKeyBinding.linux; break;1175case 'darwin': key = rawKeyBinding.mac; break;1176}11771178return this._keybindingService.resolveUserBinding(key ?? rawKeyBinding.key)[0];1179}11801181}11821183Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({1184id: 'commands',1185label: localize('commands', "Commands"),1186access: {1187canToggle: false,1188},1189renderer: new SyncDescriptor(CommandsTableRenderer),1190});119111921193