Path: blob/main/src/vs/workbench/services/actions/common/menusExtensionPoint.ts
5221 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/repositories/title',152id: MenuId.SCMSourceControlTitle,153description: localize('menus.scmSourceControlTitle', "The Source Control Repositories title menu"),154proposed: 'contribSourceControlTitleMenu'155},156{157key: 'scm/repository',158id: MenuId.SCMSourceControlInline,159description: localize('menus.scmSourceControlInline', "The Source Control repository menu"),160},161{162key: 'scm/resourceState/context',163id: MenuId.SCMResourceContext,164description: localize('menus.resourceStateContext', "The Source Control resource state context menu")165},166{167key: 'scm/resourceFolder/context',168id: MenuId.SCMResourceFolderContext,169description: localize('menus.resourceFolderContext', "The Source Control resource folder context menu")170},171{172key: 'scm/resourceGroup/context',173id: MenuId.SCMResourceGroupContext,174description: localize('menus.resourceGroupContext', "The Source Control resource group context menu")175},176{177key: 'scm/change/title',178id: MenuId.SCMChangeContext,179description: localize('menus.changeTitle', "The Source Control inline change menu")180},181{182key: 'scm/inputBox',183id: MenuId.SCMInputBox,184description: localize('menus.input', "The Source Control input box menu"),185proposed: 'contribSourceControlInputBoxMenu'186},187{188key: 'scm/history/title',189id: MenuId.SCMHistoryTitle,190description: localize('menus.scmHistoryTitle', "The Source Control History title menu"),191proposed: 'contribSourceControlHistoryTitleMenu'192},193{194key: 'scm/historyItem/context',195id: MenuId.SCMHistoryItemContext,196description: localize('menus.historyItemContext', "The Source Control history item context menu"),197proposed: 'contribSourceControlHistoryItemMenu'198},199{200key: 'scm/historyItemRef/context',201id: MenuId.SCMHistoryItemRefContext,202description: localize('menus.historyItemRefContext', "The Source Control history item reference context menu"),203proposed: 'contribSourceControlHistoryItemMenu'204},205{206key: 'scm/artifactGroup/context',207id: MenuId.SCMArtifactGroupContext,208description: localize('menus.artifactGroupContext', "The Source Control artifact group context menu"),209proposed: 'contribSourceControlArtifactGroupMenu'210},211{212key: 'scm/artifact/context',213id: MenuId.SCMArtifactContext,214description: localize('menus.artifactContext', "The Source Control artifact context menu"),215proposed: 'contribSourceControlArtifactMenu'216},217{218key: 'statusBar/remoteIndicator',219id: MenuId.StatusBarRemoteIndicatorMenu,220description: localize('menus.statusBarRemoteIndicator', "The remote indicator menu in the status bar"),221supportsSubmenus: false222},223{224key: 'terminal/context',225id: MenuId.TerminalInstanceContext,226description: localize('menus.terminalContext', "The terminal context menu")227},228{229key: 'terminal/title/context',230id: MenuId.TerminalTabContext,231description: localize('menus.terminalTabContext', "The terminal tabs context menu")232},233{234key: 'view/title',235id: MenuId.ViewTitle,236description: localize('view.viewTitle', "The contributed view title menu")237},238{239key: 'viewContainer/title',240id: MenuId.ViewContainerTitle,241description: localize('view.containerTitle', "The contributed view container title menu"),242proposed: 'contribViewContainerTitle'243},244{245key: 'view/item/context',246id: MenuId.ViewItemContext,247description: localize('view.itemContext', "The contributed view item context menu")248},249{250key: 'comments/comment/editorActions',251id: MenuId.CommentEditorActions,252description: localize('commentThread.editorActions', "The contributed comment editor actions"),253proposed: 'contribCommentEditorActionsMenu'254},255{256key: 'comments/commentThread/title',257id: MenuId.CommentThreadTitle,258description: localize('commentThread.title', "The contributed comment thread title menu")259},260{261key: 'comments/commentThread/context',262id: MenuId.CommentThreadActions,263description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"),264supportsSubmenus: false265},266{267key: 'comments/commentThread/additionalActions',268id: MenuId.CommentThreadAdditionalActions,269description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"),270supportsSubmenus: true,271proposed: 'contribCommentThreadAdditionalMenu'272},273{274key: 'comments/commentThread/title/context',275id: MenuId.CommentThreadTitleContext,276description: 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."),277proposed: 'contribCommentPeekContext'278},279{280key: 'comments/comment/title',281id: MenuId.CommentTitle,282description: localize('comment.title', "The contributed comment title menu")283},284{285key: 'comments/comment/context',286id: MenuId.CommentActions,287description: localize('comment.actions', "The contributed comment context menu, rendered as buttons below the comment editor"),288supportsSubmenus: false289},290{291key: 'comments/commentThread/comment/context',292id: MenuId.CommentThreadCommentContext,293description: 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."),294proposed: 'contribCommentPeekContext'295},296{297key: 'commentsView/commentThread/context',298id: MenuId.CommentsViewThreadActions,299description: localize('commentsView.threadActions', "The contributed comment thread context menu in the comments view"),300proposed: 'contribCommentsViewThreadMenus'301},302{303key: 'notebook/toolbar',304id: MenuId.NotebookToolbar,305description: localize('notebook.toolbar', "The contributed notebook toolbar menu")306},307{308key: 'notebook/kernelSource',309id: MenuId.NotebookKernelSource,310description: localize('notebook.kernelSource', "The contributed notebook kernel sources menu"),311proposed: 'notebookKernelSource'312},313{314key: 'notebook/cell/title',315id: MenuId.NotebookCellTitle,316description: localize('notebook.cell.title', "The contributed notebook cell title menu")317},318{319key: 'notebook/cell/execute',320id: MenuId.NotebookCellExecute,321description: localize('notebook.cell.execute', "The contributed notebook cell execution menu")322},323{324key: 'interactive/toolbar',325id: MenuId.InteractiveToolbar,326description: localize('interactive.toolbar', "The contributed interactive toolbar menu"),327},328{329key: 'interactive/cell/title',330id: MenuId.InteractiveCellTitle,331description: localize('interactive.cell.title', "The contributed interactive cell title menu"),332},333{334key: 'issue/reporter',335id: MenuId.IssueReporter,336description: localize('issue.reporter', "The contributed issue reporter menu")337},338{339key: 'testing/item/context',340id: MenuId.TestItem,341description: localize('testing.item.context', "The contributed test item menu"),342},343{344key: 'testing/item/gutter',345id: MenuId.TestItemGutter,346description: localize('testing.item.gutter.title', "The menu for a gutter decoration for a test item"),347},348{349key: 'testing/profiles/context',350id: MenuId.TestProfilesContext,351description: localize('testing.profiles.context.title', "The menu for configuring testing profiles."),352},353{354key: 'testing/item/result',355id: MenuId.TestPeekElement,356description: localize('testing.item.result.title', "The menu for an item in the Test Results view or peek."),357},358{359key: 'testing/message/context',360id: MenuId.TestMessageContext,361description: localize('testing.message.context.title', "A prominent button overlaying editor content where the message is displayed"),362},363{364key: 'testing/message/content',365id: MenuId.TestMessageContent,366description: localize('testing.message.content.title', "Context menu for the message in the results tree"),367},368{369key: 'extension/context',370id: MenuId.ExtensionContext,371description: localize('menus.extensionContext', "The extension context menu")372},373{374key: 'timeline/title',375id: MenuId.TimelineTitle,376description: localize('view.timelineTitle', "The Timeline view title menu")377},378{379key: 'timeline/item/context',380id: MenuId.TimelineItemContext,381description: localize('view.timelineContext', "The Timeline view item context menu")382},383{384key: 'ports/item/context',385id: MenuId.TunnelContext,386description: localize('view.tunnelContext', "The Ports view item context menu")387},388{389key: 'ports/item/origin/inline',390id: MenuId.TunnelOriginInline,391description: localize('view.tunnelOriginInline', "The Ports view item origin inline menu")392},393{394key: 'ports/item/port/inline',395id: MenuId.TunnelPortInline,396description: localize('view.tunnelPortInline', "The Ports view item port inline menu")397},398{399key: 'file/newFile',400id: MenuId.NewFile,401description: localize('file.newFile', "The 'New File...' quick pick, shown on welcome page and File menu."),402supportsSubmenus: false,403},404{405key: 'webview/context',406id: MenuId.WebviewContext,407description: localize('webview.context', "The webview context menu")408},409{410key: 'file/share',411id: MenuId.MenubarShare,412description: localize('menus.share', "Share submenu shown in the top level File menu."),413proposed: 'contribShareMenu'414},415{416key: 'editor/inlineCompletions/actions',417id: MenuId.InlineCompletionsActions,418description: localize('inlineCompletions.actions', "The actions shown when hovering on an inline completion"),419supportsSubmenus: false,420proposed: 'inlineCompletionsAdditions'421},422{423key: 'editor/content',424id: MenuId.EditorContent,425description: localize('merge.toolbar', "The prominent button in an editor, overlays its content"),426proposed: 'contribEditorContentMenu'427},428{429key: 'editor/lineNumber/context',430id: MenuId.EditorLineNumberContext,431description: localize('editorLineNumberContext', "The contributed editor line number context menu")432},433{434key: 'mergeEditor/result/title',435id: MenuId.MergeInputResultToolbar,436description: localize('menus.mergeEditorResult', "The result toolbar of the merge editor"),437proposed: 'contribMergeEditorMenus'438},439{440key: 'multiDiffEditor/content',441id: MenuId.MultiDiffEditorContent,442description: localize('menus.multiDiffEditorContent', "A prominent button overlaying the multi diff editor"),443proposed: 'contribEditorContentMenu'444},445{446key: 'multiDiffEditor/resource/title',447id: MenuId.MultiDiffEditorFileToolbar,448description: localize('menus.multiDiffEditorResource', "The resource toolbar in the multi diff editor"),449proposed: 'contribMultiDiffEditorMenus'450},451{452key: 'diffEditor/gutter/hunk',453id: MenuId.DiffEditorHunkToolbar,454description: localize('menus.diffEditorGutterToolBarMenus', "The gutter toolbar in the diff editor"),455proposed: 'contribDiffEditorGutterToolBarMenus'456},457{458key: 'diffEditor/gutter/selection',459id: MenuId.DiffEditorSelectionToolbar,460description: localize('menus.diffEditorGutterToolBarMenus', "The gutter toolbar in the diff editor"),461proposed: 'contribDiffEditorGutterToolBarMenus'462},463{464key: 'searchPanel/aiResults/commands',465id: MenuId.SearchActionMenu,466description: localize('searchPanel.aiResultsCommands', "The commands that will contribute to the menu rendered as buttons next to the AI search title"),467},468{469key: 'editor/context/chat',470id: MenuId.ChatTextEditorMenu,471description: localize('menus.chatTextEditor', "The Chat submenu in the text editor context menu."),472supportsSubmenus: false,473proposed: 'chatParticipantPrivate'474},475{476key: 'chat/input/editing/sessionToolbar',477id: MenuId.ChatEditingSessionChangesToolbar,478description: localize('menus.chatEditingSessionChangesToolbar', "The Chat Editing widget toolbar menu for session changes."),479proposed: 'chatSessionsProvider'480},481{482// TODO: rename this to something like: `chatSessions/item/inline`483key: 'chat/chatSessions',484id: MenuId.AgentSessionsContext,485description: localize('menus.chatSessions', "The Chat Sessions menu."),486supportsSubmenus: false,487proposed: 'chatSessionsProvider'488},489{490key: 'chatSessions/newSession',491id: MenuId.AgentSessionsCreateSubMenu,492description: localize('menus.chatSessionsNewSession', "Menu for new chat sessions."),493supportsSubmenus: false,494proposed: 'chatSessionsProvider'495},496{497key: 'chat/multiDiff/context',498id: MenuId.ChatMultiDiffContext,499description: localize('menus.chatMultiDiffContext', "The Chat Multi-Diff context menu."),500supportsSubmenus: false,501proposed: 'chatSessionsProvider',502},503{504key: 'chat/editor/inlineGutter',505id: MenuId.ChatEditorInlineGutter,506description: localize('menus.chatEditorInlineGutter', "The inline gutter menu in the chat editor."),507supportsSubmenus: false,508proposed: 'contribChatEditorInlineGutterMenu',509},510{511key: 'chat/contextUsage/actions',512id: MenuId.ChatContextUsageActions,513description: localize('menus.chatContextUsageActions', "Actions in the chat context usage details popup."),514proposed: 'chatParticipantAdditions'515},516];517518namespace schema {519520// --- menus, submenus contribution point521522export interface IUserFriendlyMenuItem {523command: string;524alt?: string;525when?: string;526group?: string;527}528529export interface IUserFriendlySubmenuItem {530submenu: string;531when?: string;532group?: string;533}534535export interface IUserFriendlySubmenu {536id: string;537label: string;538icon?: IUserFriendlyIcon;539}540541export function isMenuItem(item: IUserFriendlyMenuItem | IUserFriendlySubmenuItem): item is IUserFriendlyMenuItem {542return typeof (item as IUserFriendlyMenuItem).command === 'string';543}544545export function isValidMenuItem(item: IUserFriendlyMenuItem, collector: ExtensionMessageCollector): boolean {546if (typeof item.command !== 'string') {547collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));548return false;549}550if (item.alt && typeof item.alt !== 'string') {551collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'alt'));552return false;553}554if (item.when && typeof item.when !== 'string') {555collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));556return false;557}558if (item.group && typeof item.group !== 'string') {559collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group'));560return false;561}562563return true;564}565566export function isValidSubmenuItem(item: IUserFriendlySubmenuItem, collector: ExtensionMessageCollector): boolean {567if (typeof item.submenu !== 'string') {568collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'submenu'));569return false;570}571if (item.when && typeof item.when !== 'string') {572collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));573return false;574}575if (item.group && typeof item.group !== 'string') {576collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group'));577return false;578}579580return true;581}582583export function isValidItems(items: (IUserFriendlyMenuItem | IUserFriendlySubmenuItem)[], collector: ExtensionMessageCollector): boolean {584if (!Array.isArray(items)) {585collector.error(localize('requirearray', "submenu items must be an array"));586return false;587}588589for (const item of items) {590if (isMenuItem(item)) {591if (!isValidMenuItem(item, collector)) {592return false;593}594} else {595if (!isValidSubmenuItem(item, collector)) {596return false;597}598}599}600601return true;602}603604export function isValidSubmenu(submenu: IUserFriendlySubmenu, collector: ExtensionMessageCollector): boolean {605if (typeof submenu !== 'object') {606collector.error(localize('require', "submenu items must be an object"));607return false;608}609610if (typeof submenu.id !== 'string') {611collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id'));612return false;613}614if (typeof submenu.label !== 'string') {615collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'label'));616return false;617}618619return true;620}621622const menuItem: IJSONSchema = {623type: 'object',624required: ['command'],625properties: {626command: {627description: localize('vscode.extension.contributes.menuItem.command', 'Identifier of the command to execute. The command must be declared in the \'commands\'-section'),628type: 'string'629},630alt: {631description: localize('vscode.extension.contributes.menuItem.alt', 'Identifier of an alternative command to execute. The command must be declared in the \'commands\'-section'),632type: 'string'633},634when: {635description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'),636type: 'string'637},638group: {639description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'),640type: 'string'641}642}643};644645const submenuItem: IJSONSchema = {646type: 'object',647required: ['submenu'],648properties: {649submenu: {650description: localize('vscode.extension.contributes.menuItem.submenu', 'Identifier of the submenu to display in this item.'),651type: 'string'652},653when: {654description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'),655type: 'string'656},657group: {658description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'),659type: 'string'660}661}662};663664const submenu: IJSONSchema = {665type: 'object',666required: ['id', 'label'],667properties: {668id: {669description: localize('vscode.extension.contributes.submenu.id', 'Identifier of the menu to display as a submenu.'),670type: 'string'671},672label: {673description: localize('vscode.extension.contributes.submenu.label', 'The label of the menu item which leads to this submenu.'),674type: 'string'675},676icon: {677description: 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)"'),678anyOf: [{679type: 'string'680},681{682type: 'object',683properties: {684light: {685description: localize('vscode.extension.contributes.submenu.icon.light', 'Icon path when a light theme is used'),686type: 'string'687},688dark: {689description: localize('vscode.extension.contributes.submenu.icon.dark', 'Icon path when a dark theme is used'),690type: 'string'691}692}693}]694}695}696};697698export const menusContribution: IJSONSchema = {699description: localize('vscode.extension.contributes.menus', "Contributes menu items to the editor"),700type: 'object',701properties: index(apiMenus, menu => menu.key, menu => ({702markdownDescription: menu.proposed ? localize('proposed', "Proposed API, requires `enabledApiProposal: [\"{0}\"]` - {1}", menu.proposed, menu.description) : menu.description,703type: 'array',704items: menu.supportsSubmenus === false ? menuItem : { oneOf: [menuItem, submenuItem] }705})),706additionalProperties: {707description: 'Submenu',708type: 'array',709items: { oneOf: [menuItem, submenuItem] }710}711};712713export const submenusContribution: IJSONSchema = {714description: localize('vscode.extension.contributes.submenus', "Contributes submenu items to the editor"),715type: 'array',716items: submenu717};718719// --- commands contribution point720721export interface IUserFriendlyCommand {722command: string;723title: string | ILocalizedString;724shortTitle?: string | ILocalizedString;725enablement?: string;726category?: string | ILocalizedString;727icon?: IUserFriendlyIcon;728}729730export type IUserFriendlyIcon = string | { light: string; dark: string };731732export function isValidCommand(command: IUserFriendlyCommand, collector: ExtensionMessageCollector): boolean {733if (!command) {734collector.error(localize('nonempty', "expected non-empty value."));735return false;736}737if (isFalsyOrWhitespace(command.command)) {738collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));739return false;740}741if (!isValidLocalizedString(command.title, collector, 'title')) {742return false;743}744if (command.shortTitle && !isValidLocalizedString(command.shortTitle, collector, 'shortTitle')) {745return false;746}747if (command.enablement && typeof command.enablement !== 'string') {748collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'precondition'));749return false;750}751if (command.category && !isValidLocalizedString(command.category, collector, 'category')) {752return false;753}754if (!isValidIcon(command.icon, collector)) {755return false;756}757return true;758}759760function isValidIcon(icon: IUserFriendlyIcon | undefined, collector: ExtensionMessageCollector): boolean {761if (typeof icon === 'undefined') {762return true;763}764if (typeof icon === 'string') {765return true;766} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {767return true;768}769collector.error(localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));770return false;771}772773function isValidLocalizedString(localized: string | ILocalizedString, collector: ExtensionMessageCollector, propertyName: string): boolean {774if (typeof localized === 'undefined') {775collector.error(localize('requireStringOrObject', "property `{0}` is mandatory and must be of type `string` or `object`", propertyName));776return false;777} else if (typeof localized === 'string' && isFalsyOrWhitespace(localized)) {778collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", propertyName));779return false;780} else if (typeof localized !== 'string' && (isFalsyOrWhitespace(localized.original) || isFalsyOrWhitespace(localized.value))) {781collector.error(localize('requirestrings', "properties `{0}` and `{1}` are mandatory and must be of type `string`", `${propertyName}.value`, `${propertyName}.original`));782return false;783}784785return true;786}787788const commandType: IJSONSchema = {789type: 'object',790required: ['command', 'title'],791properties: {792command: {793description: localize('vscode.extension.contributes.commandType.command', 'Identifier of the command to execute'),794type: 'string'795},796title: {797description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'),798type: 'string'799},800shortTitle: {801markdownDescription: 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.'),802type: 'string'803},804category: {805description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by which the command is grouped in the UI'),806type: 'string'807},808enablement: {809description: 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.'),810type: 'string'811},812icon: {813description: 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)"'),814anyOf: [{815type: 'string'816},817{818type: 'object',819properties: {820light: {821description: localize('vscode.extension.contributes.commandType.icon.light', 'Icon path when a light theme is used'),822type: 'string'823},824dark: {825description: localize('vscode.extension.contributes.commandType.icon.dark', 'Icon path when a dark theme is used'),826type: 'string'827}828}829}]830}831}832};833834export const commandsContribution: IJSONSchema = {835description: localize('vscode.extension.contributes.commands', "Contributes commands to the command palette."),836oneOf: [837commandType,838{839type: 'array',840items: commandType841}842]843};844}845846const _commandRegistrations = new DisposableStore();847848export const commandsExtensionPoint = ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlyCommand | schema.IUserFriendlyCommand[]>({849extensionPoint: 'commands',850jsonSchema: schema.commandsContribution,851activationEventsGenerator: function* (contribs: readonly schema.IUserFriendlyCommand[]) {852for (const contrib of contribs) {853if (contrib.command) {854yield `onCommand:${contrib.command}`;855}856}857}858});859860commandsExtensionPoint.setHandler(extensions => {861862function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser<unknown>) {863864if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) {865return;866}867868const { icon, enablement, category, title, shortTitle, command } = userFriendlyCommand;869870let absoluteIcon: { dark: URI; light?: URI } | ThemeIcon | undefined;871if (icon) {872if (typeof icon === 'string') {873absoluteIcon = ThemeIcon.fromString(icon) ?? { dark: resources.joinPath(extension.description.extensionLocation, icon), light: resources.joinPath(extension.description.extensionLocation, icon) };874875} else {876absoluteIcon = {877dark: resources.joinPath(extension.description.extensionLocation, icon.dark),878light: resources.joinPath(extension.description.extensionLocation, icon.light)879};880}881}882883const existingCmd = MenuRegistry.getCommand(command);884if (existingCmd) {885if (existingCmd.source) {886extension.collector.info(localize('dup1', "Command `{0}` already registered by {1} ({2})", userFriendlyCommand.command, existingCmd.source.title, existingCmd.source.id));887} else {888extension.collector.info(localize('dup0', "Command `{0}` already registered", userFriendlyCommand.command));889}890}891_commandRegistrations.add(MenuRegistry.addCommand({892id: command,893title,894source: { id: extension.description.identifier.value, title: extension.description.displayName ?? extension.description.name },895shortTitle,896tooltip: title,897category,898precondition: ContextKeyExpr.deserialize(enablement),899icon: absoluteIcon900}));901}902903// remove all previous command registrations904_commandRegistrations.clear();905906for (const extension of extensions) {907const { value } = extension;908if (Array.isArray(value)) {909for (const command of value) {910handleCommand(command, extension);911}912} else {913handleCommand(value, extension);914}915}916});917918interface IRegisteredSubmenu {919readonly id: MenuId;920readonly label: string;921readonly icon?: { dark: URI; light?: URI } | ThemeIcon;922}923924const _submenus = new Map<string, IRegisteredSubmenu>();925926const submenusExtensionPoint = ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlySubmenu[]>({927extensionPoint: 'submenus',928jsonSchema: schema.submenusContribution929});930931submenusExtensionPoint.setHandler(extensions => {932933_submenus.clear();934935for (const extension of extensions) {936const { value, collector } = extension;937938for (const [, submenuInfo] of Object.entries(value)) {939940if (!schema.isValidSubmenu(submenuInfo, collector)) {941continue;942}943944if (!submenuInfo.id) {945collector.warn(localize('submenuId.invalid.id', "`{0}` is not a valid submenu identifier", submenuInfo.id));946continue;947}948if (_submenus.has(submenuInfo.id)) {949collector.info(localize('submenuId.duplicate.id', "The `{0}` submenu was already previously registered.", submenuInfo.id));950continue;951}952if (!submenuInfo.label) {953collector.warn(localize('submenuId.invalid.label', "`{0}` is not a valid submenu label", submenuInfo.label));954continue;955}956957let absoluteIcon: { dark: URI; light?: URI } | ThemeIcon | undefined;958if (submenuInfo.icon) {959if (typeof submenuInfo.icon === 'string') {960absoluteIcon = ThemeIcon.fromString(submenuInfo.icon) || { dark: resources.joinPath(extension.description.extensionLocation, submenuInfo.icon) };961} else {962absoluteIcon = {963dark: resources.joinPath(extension.description.extensionLocation, submenuInfo.icon.dark),964light: resources.joinPath(extension.description.extensionLocation, submenuInfo.icon.light)965};966}967}968969const item: IRegisteredSubmenu = {970id: MenuId.for(`api:${submenuInfo.id}`),971label: submenuInfo.label,972icon: absoluteIcon973};974975_submenus.set(submenuInfo.id, item);976}977}978});979980const _apiMenusByKey = new Map(apiMenus.map(menu => ([menu.key, menu])));981const _menuRegistrations = new DisposableStore();982const _submenuMenuItems = new Map<string /* menu id */, Set<string /* submenu id */>>();983984const menusExtensionPoint = ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: (schema.IUserFriendlyMenuItem | schema.IUserFriendlySubmenuItem)[] }>({985extensionPoint: 'menus',986jsonSchema: schema.menusContribution,987deps: [submenusExtensionPoint]988});989990menusExtensionPoint.setHandler(extensions => {991992// remove all previous menu registrations993_menuRegistrations.clear();994_submenuMenuItems.clear();995996for (const extension of extensions) {997const { value, collector } = extension;998999for (const entry of Object.entries(value)) {1000if (!schema.isValidItems(entry[1], collector)) {1001continue;1002}10031004let menu = _apiMenusByKey.get(entry[0]);10051006if (!menu) {1007const submenu = _submenus.get(entry[0]);10081009if (submenu) {1010menu = {1011key: entry[0],1012id: submenu.id,1013description: ''1014};1015}1016}10171018if (!menu) {1019continue;1020}10211022if (menu.proposed && !isProposedApiEnabled(extension.description, menu.proposed)) {1023collector.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));1024continue;1025}10261027for (const menuItem of entry[1]) {1028let item: IMenuItem | ISubmenuItem;10291030if (schema.isMenuItem(menuItem)) {1031const command = MenuRegistry.getCommand(menuItem.command);1032const alt = menuItem.alt && MenuRegistry.getCommand(menuItem.alt) || undefined;10331034if (!command) {1035collector.error(localize('missing.command', "Menu item references a command `{0}` which is not defined in the 'commands' section.", menuItem.command));1036continue;1037}1038if (menuItem.alt && !alt) {1039collector.warn(localize('missing.altCommand', "Menu item references an alt-command `{0}` which is not defined in the 'commands' section.", menuItem.alt));1040}1041if (menuItem.command === menuItem.alt) {1042collector.info(localize('dupe.command', "Menu item references the same command as default and alt-command"));1043}10441045item = { command, alt, group: undefined, order: undefined, when: undefined };1046} else {1047if (menu.supportsSubmenus === false) {1048collector.error(localize('unsupported.submenureference', "Menu item references a submenu for a menu which doesn't have submenu support."));1049continue;1050}10511052const submenu = _submenus.get(menuItem.submenu);10531054if (!submenu) {1055collector.error(localize('missing.submenu', "Menu item references a submenu `{0}` which is not defined in the 'submenus' section.", menuItem.submenu));1056continue;1057}10581059let submenuRegistrations = _submenuMenuItems.get(menu.id.id);10601061if (!submenuRegistrations) {1062submenuRegistrations = new Set();1063_submenuMenuItems.set(menu.id.id, submenuRegistrations);1064}10651066if (submenuRegistrations.has(submenu.id.id)) {1067collector.warn(localize('submenuItem.duplicate', "The `{0}` submenu was already contributed to the `{1}` menu.", menuItem.submenu, entry[0]));1068continue;1069}10701071submenuRegistrations.add(submenu.id.id);10721073item = { submenu: submenu.id, icon: submenu.icon, title: submenu.label, group: undefined, order: undefined, when: undefined };1074}10751076if (menuItem.group) {1077const idx = menuItem.group.lastIndexOf('@');1078if (idx > 0) {1079item.group = menuItem.group.substr(0, idx);1080item.order = Number(menuItem.group.substr(idx + 1)) || undefined;1081} else {1082item.group = menuItem.group;1083}1084}10851086if (menu.id === MenuId.ViewContainerTitle && !menuItem.when?.includes('viewContainer == workbench.view.debug')) {1087// Not a perfect check but enough to communicate that this proposed extension point is currently only for the debug view container1088collector.error(localize('viewContainerTitle.when', "The {0} menu contribution must check {1} in its {2} clause.", '`viewContainer/title`', '`viewContainer == workbench.view.debug`', '"when"'));1089continue;1090}10911092item.when = ContextKeyExpr.deserialize(menuItem.when);1093_menuRegistrations.add(MenuRegistry.appendMenuItem(menu.id, item));1094}1095}1096}1097});10981099class CommandsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {11001101readonly type = 'table';11021103constructor(1104@IKeybindingService private readonly _keybindingService: IKeybindingService1105) { super(); }11061107shouldRender(manifest: IExtensionManifest): boolean {1108return !!manifest.contributes?.commands;1109}11101111render(manifest: IExtensionManifest): IRenderedData<ITableData> {1112const rawCommands = manifest.contributes?.commands || [];1113const commands = rawCommands.map(c => ({1114id: c.command,1115title: c.title,1116keybindings: [] as ResolvedKeybinding[],1117menus: [] as string[]1118}));11191120const byId = index(commands, c => c.id);11211122const menus = manifest.contributes?.menus || {};11231124// Add to commandPalette array any commands not explicitly contributed to it1125const implicitlyOnCommandPalette = index(commands, c => c.id);1126if (menus['commandPalette']) {1127for (const command of menus['commandPalette']) {1128delete implicitlyOnCommandPalette[command.command];1129}1130}11311132if (Object.keys(implicitlyOnCommandPalette).length) {1133if (!menus['commandPalette']) {1134menus['commandPalette'] = [];1135}1136for (const command in implicitlyOnCommandPalette) {1137menus['commandPalette'].push({ command });1138}1139}11401141for (const context in menus) {1142for (const menu of menus[context]) {11431144// This typically happens for the commandPalette context1145if (menu.when === 'false') {1146continue;1147}1148if (menu.command) {1149let command = byId[menu.command];1150if (command) {1151if (!command.menus.includes(context)) {1152command.menus.push(context);1153}1154} else {1155command = { id: menu.command, title: '', keybindings: [], menus: [context] };1156byId[command.id] = command;1157commands.push(command);1158}1159}1160}1161}11621163const rawKeybindings = manifest.contributes?.keybindings ? (Array.isArray(manifest.contributes.keybindings) ? manifest.contributes.keybindings : [manifest.contributes.keybindings]) : [];11641165rawKeybindings.forEach(rawKeybinding => {1166const keybinding = this.resolveKeybinding(rawKeybinding);11671168if (!keybinding) {1169return;1170}11711172let command = byId[rawKeybinding.command];11731174if (command) {1175command.keybindings.push(keybinding);1176} else {1177command = { id: rawKeybinding.command, title: '', keybindings: [keybinding], menus: [] };1178byId[command.id] = command;1179commands.push(command);1180}1181});11821183if (!commands.length) {1184return { data: { headers: [], rows: [] }, dispose: () => { } };1185}11861187const headers = [1188localize('command name', "ID"),1189localize('command title', "Title"),1190localize('keyboard shortcuts', "Keyboard Shortcuts"),1191localize('menuContexts', "Menu Contexts")1192];11931194const rows: IRowData[][] = commands.sort((a, b) => a.id.localeCompare(b.id))1195.map(command => {1196return [1197new MarkdownString().appendMarkdown(`\`${command.id}\``),1198typeof command.title === 'string' ? command.title : command.title.value,1199command.keybindings,1200new MarkdownString().appendMarkdown(`${command.menus.sort((a, b) => a.localeCompare(b)).map(menu => `\`${menu}\``).join(' ')}`),1201];1202});12031204return {1205data: {1206headers,1207rows1208},1209dispose: () => { }1210};1211}12121213private resolveKeybinding(rawKeyBinding: IKeyBinding): ResolvedKeybinding | undefined {1214let key: string | undefined;12151216switch (platform) {1217case 'win32': key = rawKeyBinding.win; break;1218case 'linux': key = rawKeyBinding.linux; break;1219case 'darwin': key = rawKeyBinding.mac; break;1220}12211222return this._keybindingService.resolveUserBinding(key ?? rawKeyBinding.key)[0];1223}12241225}12261227Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({1228id: 'commands',1229label: localize('commands', "Commands"),1230access: {1231canToggle: false,1232},1233renderer: new SyncDescriptor(CommandsTableRenderer),1234});123512361237