diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 36beeb00478..dff0019cfee 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -69,6 +69,11 @@ .monaco-button-dropdown > .monaco-button.monaco-dropdown-button { border-left-width: 0 !important; + border-radius: 0 2px 2px 0; +} + +.monaco-button-dropdown > .monaco-button.monaco-text-button { + border-radius: 2px 0 0 2px; } .monaco-description-button { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 0d28e607144..45d213b4781 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -257,9 +257,10 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.separatorContainer.style.borderTop = '1px solid ' + border; this.separatorContainer.style.borderBottom = '1px solid ' + border; } - this.separatorContainer.style.backgroundColor = options.buttonBackground ?? ''; - this.separator.style.backgroundColor = options.buttonSeparator ?? ''; + const buttonBackground = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground; + this.separatorContainer.style.backgroundColor = buttonBackground ?? ''; + this.separator.style.backgroundColor = options.buttonSeparator ?? ''; this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...'); diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index 4387c42cc67..abd7698ed92 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -12,6 +12,7 @@ export const enum MarshalledId { ScmProvider, CommentController, CommentThread, + CommentThreadInstance, CommentThreadReply, CommentNode, CommentThreadNode, diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index a39a46fd6c5..762b9754744 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -128,6 +128,7 @@ export class MenuId { static readonly ViewTitleContext = new MenuId('ViewTitleContext'); static readonly CommentThreadTitle = new MenuId('CommentThreadTitle'); static readonly CommentThreadActions = new MenuId('CommentThreadActions'); + static readonly CommentThreadAdditionalActions = new MenuId('CommentThreadAdditionalActions'); static readonly CommentThreadTitleContext = new MenuId('CommentThreadTitleContext'); static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext'); static readonly CommentTitle = new MenuId('CommentTitle'); diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index e66c60290f9..2e2f0e0835c 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -66,7 +66,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } return commentThread.value; - } else if (arg && arg.$mid === MarshalledId.CommentThreadReply) { + } else if (arg && (arg.$mid === MarshalledId.CommentThreadReply || arg.$mid === MarshalledId.CommentThreadInstance)) { const commentController = this._commentControllers.get(arg.thread.commentControlHandle); if (!commentController) { @@ -79,6 +79,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return arg; } + if (arg.$mid === MarshalledId.CommentThreadInstance) { + return commentThread.value; + } + return { thread: commentThread.value, text: arg.text diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index 8e5bed5afac..e4fb1b79e31 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Button } from 'vs/base/browser/ui/button/button'; +import { Button, ButtonWithDropdown, IButton } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IMenu } from 'vs/platform/actions/common/actions'; +import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; export class CommentFormActions implements IDisposable { private _buttonElements: HTMLElement[] = []; @@ -17,29 +18,49 @@ export class CommentFormActions implements IDisposable { constructor( private container: HTMLElement, private actionHandler: (action: IAction) => void, + private contextMenuService?: IContextMenuService ) { } - setActions(menu: IMenu) { + setActions(menu: IMenu, hasOnlySecondaryActions: boolean = false) { this._toDispose.clear(); this._buttonElements.forEach(b => b.remove()); const groups = menu.getActions({ shouldForwardArgs: true }); - let isPrimary: boolean = true; + let isPrimary: boolean = !hasOnlySecondaryActions; for (const group of groups) { const [, actions] = group; this._actions = actions; for (const action of actions) { - const button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles }); + const submenuAction = action as SubmenuItemAction; + + // Use the first action from the submenu as the primary button. + const appliedAction: IAction = submenuAction.actions?.length > 0 ? submenuAction.actions[0] : action; + let button: IButton | undefined; + + // Use dropdown only if submenu contains more than 1 action. + if (submenuAction.actions?.length > 1 && this.contextMenuService) { + button = new ButtonWithDropdown(this.container, + { + contextMenuProvider: this.contextMenuService, + actions: submenuAction.actions.slice(1), + addPrimaryActionToDropdown: false, + secondary: !isPrimary, + ...defaultButtonStyles + }); + } else { + button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles }); + } + isPrimary = false; this._buttonElements.push(button.element); this._toDispose.add(button); - this._toDispose.add(button.onDidClick(() => this.actionHandler(action))); + this._toDispose.add(button.onDidClick(() => this.actionHandler(appliedAction))); - button.enabled = action.enabled; - button.label = action.label; + button.enabled = appliedAction.enabled; + button.label = appliedAction.label; } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts index dae9f44956f..479e5156b12 100644 --- a/src/vs/workbench/contrib/comments/browser/commentMenus.ts +++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts @@ -23,6 +23,10 @@ export class CommentMenus implements IDisposable { return this.getMenu(MenuId.CommentThreadActions, contextKeyService); } + getCommentThreadAdditionalActions(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentThreadAdditionalActions, contextKeyService); + } + getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu { return this.getMenu(MenuId.CommentTitle, contextKeyService); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadAdditionalActions.ts b/src/vs/workbench/contrib/comments/browser/commentThreadAdditionalActions.ts new file mode 100644 index 00000000000..0e3a9870d29 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadAdditionalActions.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; + +import { IAction } from 'vs/base/common/actions'; +import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IRange } from 'vs/editor/common/core/range'; +import * as languages from 'vs/editor/common/languages'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; + +export class CommentThreadAdditionalActions extends Disposable { + private _container: HTMLElement | null; + private _buttonBar: HTMLElement | null; + private _commentFormActions!: CommentFormActions; + + constructor( + container: HTMLElement, + private _commentThread: languages.CommentThread, + private _contextKeyService: IContextKeyService, + private _commentMenus: CommentMenus, + private _actionRunDelegate: (() => void) | null, + @IContextMenuService private contextMenuService: IContextMenuService, + ) { + super(); + + this._container = dom.append(container, dom.$('.comment-additional-actions')); + dom.append(this._container, dom.$('.section-separator')); + + this._buttonBar = dom.append(this._container, dom.$('.button-bar')); + this._createAdditionalActions(this._buttonBar); + } + + private _showMenu() { + this._container?.classList.remove('hidden'); + } + + private _hideMenu() { + this._container?.classList.add('hidden'); + } + + private _enableDisableMenu(menu: IMenu) { + const groups = menu.getActions({ shouldForwardArgs: true }); + + // Show the menu if at least one action is enabled. + for (const group of groups) { + const [, actions] = group; + for (const action of actions) { + if (action.enabled) { + this._showMenu(); + return; + } + + for (const subAction of (action as SubmenuItemAction).actions ?? []) { + if (subAction.enabled) { + this._showMenu(); + return; + } + } + } + } + + this._hideMenu(); + } + + + private _createAdditionalActions(container: HTMLElement) { + const menu = this._commentMenus.getCommentThreadAdditionalActions(this._contextKeyService); + this._register(menu); + this._register(menu.onDidChange(() => { + this._commentFormActions.setActions(menu); + this._enableDisableMenu(menu); + })); + + this._commentFormActions = new CommentFormActions(container, async (action: IAction) => { + this._actionRunDelegate?.(); + + action.run({ + thread: this._commentThread, + $mid: MarshalledId.CommentThreadInstance + }); + + }, this.contextMenuService); + + this._register(this._commentFormActions); + this._commentFormActions.setActions(menu, /*hasOnlySecondaryActions*/ true); + this._enableDisableMenu(menu); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index d7af5c13258..8c07cd661fe 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -17,6 +17,7 @@ import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody'; import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader'; +import { CommentThreadAdditionalActions } from 'vs/workbench/contrib/comments/browser/commentThreadAdditionalActions'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; @@ -35,6 +36,7 @@ export class CommentThreadWidget extends private _header!: CommentThreadHeader; private _body!: CommentThreadBody; private _commentReply?: CommentReply; + private _additionalActions?: CommentThreadAdditionalActions; private _commentMenus: CommentMenus; private _commentThreadDisposables: IDisposable[] = []; private _threadIsEmpty: IContextKey; @@ -177,6 +179,7 @@ export class CommentThreadWidget extends if (this._commentThread.canReply) { this._createCommentForm(); } + this._createAdditionalActions(); this._register(this._body.onDidResize(dimension => { this._refresh(dimension); @@ -239,6 +242,19 @@ export class CommentThreadWidget extends this._register(this._commentReply); } + private _createAdditionalActions() { + this._additionalActions = this._scopedInstatiationService.createInstance( + CommentThreadAdditionalActions, + this._body.container, + this._commentThread, + this._contextKeyService, + this._commentMenus, + this._containerDelegate.actionRunner, + ); + + this._register(this._additionalActions); + } + getCommentCoords(commentUniqueId: number) { return this._body.getCommentCoords(commentUniqueId); } diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index d26c07a67c4..ef33e562093 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -246,6 +246,42 @@ word-wrap: break-word; } + +.review-widget .body .comment-additional-actions { + margin: 10px 20px; +} + +.review-widget .body .comment-additional-actions .section-separator { + border-top: 1px solid var(--vscode-menu-separatorBackground); + margin: 14px 0; +} + +.review-widget .body .comment-additional-actions .button-bar { + display: flex; + white-space: nowrap; +} + +.review-widget .body .comment-additional-actions .monaco-button, +.review-widget .body .comment-additional-actions .monaco-text-button, +.review-widget .body .comment-additional-actions .monaco-button-dropdown { + display: flex; + width: auto; +} + +.review-widget .body .comment-additional-actions .button-bar>.monaco-text-button, +.review-widget .body .comment-additional-actions .button-bar>.monaco-button-dropdown { + margin: 0 10px 0 0; +} + +.review-widget .body .comment-additional-actions .button-bar .monaco-text-button { + padding: 4px 10px; +} + + +.review-widget .body .comment-additional-actions .codicon-drop-down-button { + align-items: center; +} + .review-widget .body .comment-form.expand .review-thread-reply-button { display: none; } @@ -295,7 +331,7 @@ .review-widget .body .comment-form .form-actions, .review-widget .body .edit-container .form-actions { overflow: auto; - padding: 10px 0; + margin: 10px 0; } .review-widget .body .edit-textarea { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 6a08158a58a..37a9d5a75e2 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -287,13 +287,8 @@ padding: 0 4px; } -/* split commit button */ -.scm-view .button-container > .monaco-button-dropdown > .monaco-dropdown-button.codicon-drop-down-button { - border-radius: 0 2px 2px 0; -} .scm-view .button-container > .monaco-button-dropdown > .monaco-button.monaco-text-button { - border-radius: 2px 0 0 2px; min-width: 0; } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 80a2cb98aa2..cc2ba477dfe 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -159,6 +159,13 @@ const apiMenus: IAPIMenu[] = [ description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"), supportsSubmenus: false }, + { + key: 'comments/commentThread/additionalActions', + id: MenuId.CommentThreadAdditionalActions, + description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"), + supportsSubmenus: true, + proposed: 'contribCommentThreadAdditionalMenu' + }, { key: 'comments/commentThread/title/context', id: MenuId.CommentThreadTitleContext, diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index d9336a794c4..88ee287f9c5 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -10,6 +10,7 @@ export const allApiProposals = Object.freeze({ codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', + contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', diff --git a/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts b/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts new file mode 100644 index 00000000000..2e71d90a25a --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder for comment thread additional menus + +// https://github.com/microsoft/vscode/issues/163281