diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts new file mode 100644 index 00000000000..811ccaac879 --- /dev/null +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; +import { ActionRunner, IAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { MenuId, IMenuService, SubmenuItemAction, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +export type IButtonConfigProvider = (action: IAction) => { + showIcon?: boolean; + showLabel?: boolean; +} | undefined; + +export interface IMenuWorkbenchButtonBarOptions { + telemetrySource?: string; + buttonConfigProvider?: IButtonConfigProvider; +} + +export class MenuWorkbenchButtonBar extends ButtonBar { + + private readonly _store = new DisposableStore(); + + private readonly _onDidChangeMenuItems = new Emitter(); + readonly onDidChangeMenuItems: Event = this._onDidChangeMenuItems.event; + + constructor( + container: HTMLElement, + menuId: MenuId, + options: IMenuWorkbenchButtonBarOptions | undefined, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(container); + + const menu = menuService.createMenu(menuId, contextKeyService); + this._store.add(menu); + + const actionRunner = this._store.add(new ActionRunner()); + if (options?.telemetrySource) { + actionRunner.onDidRun(e => { + telemetryService.publicLog2( + 'workbenchActionExecuted', + { id: e.action.id, from: options.telemetrySource! } + ); + }, this._store); + } + + const conifgProvider: IButtonConfigProvider = options?.buttonConfigProvider ?? (() => ({ showLabel: true })); + + const update = () => { + + this.clear(); + + const actions = menu + .getActions({ renderShortTitle: true }) + .flatMap(entry => entry[1]); + + for (let i = 0; i < actions.length; i++) { + const secondary = i > 0; + const actionOrSubmenu = actions[i]; + let action: MenuItemAction | SubmenuItemAction; + let btn: IButton; + + if (actionOrSubmenu instanceof SubmenuItemAction && actionOrSubmenu.actions.length > 0) { + const [first, ...rest] = actionOrSubmenu.actions; + action = first; + btn = this.addButtonWithDropdown({ + secondary, + actionRunner, + actions: rest, + contextMenuProvider: contextMenuService, + }); + } else { + action = actionOrSubmenu; + btn = this.addButton({ secondary }); + } + + btn.enabled = action.enabled; + if (conifgProvider(action)?.showLabel ?? true) { + btn.label = action.label; + } else { + btn.element.classList.add('monaco-text-button'); + } + if (conifgProvider(action)?.showIcon && ThemeIcon.isThemeIcon(action.item.icon)) { + btn.icon = action.item.icon; + } + const kb = keybindingService.lookupKeybinding(action.id); + if (kb) { + btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel()); + } else { + btn.element.title = action.label; + + } + btn.onDidClick(async () => { + actionRunner.run(action); + }); + } + this._onDidChangeMenuItems.fire(this); + }; + this._store.add(menu.onDidChange(update)); + update(); + } + + override dispose() { + this._onDidChangeMenuItems.dispose(); + this._store.dispose(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 0617795a65f..44c09e897bd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -199,6 +199,14 @@ padding: 0 4px; } +.monaco-editor .inline-chat .status .actions > .monaco-button.codicon { + display: flex; +} + +.monaco-editor .inline-chat .status .actions > .monaco-button.codicon::before { + align-self: center; +} + .monaco-editor .inline-chat .status .actions .monaco-text-button { padding: 2px 4px } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index ee28443c726..fd388e16bd8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -169,7 +169,7 @@ export class ReRunRequestAction extends AbstractInlineChatAction { icon: Codicon.refresh, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EMPTY.negate(), CTX_INLINE_CHAT_LAST_RESPONSE_TYPE), menu: { - id: MENU_INLINE_CHAT_WIDGET_FEEDBACK, + id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '2_feedback', order: 3, } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 916e5437212..8eb85590808 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -12,7 +12,7 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom'; import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; @@ -28,14 +28,11 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { Position } from 'vs/editor/common/core/position'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; -import { DropdownWithDefaultActionViewItem, IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { FileKind } from 'vs/platform/files/common/files'; -import { IAction } from 'vs/base/common/actions'; -import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; @@ -44,18 +41,14 @@ import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/inlineCh import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; -import { assertType } from 'vs/base/common/types'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IdleValue } from 'vs/base/common/async'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -196,9 +189,7 @@ export class InlineChatWidget { @IKeybindingService private readonly _keybindingService: IKeybindingService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IMenuService private readonly _menuService: IMenuService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { // input editor logic @@ -298,103 +289,27 @@ export class InlineChatWidget { this._progressBar = new ProgressBar(this._elements.progress); this._store.add(this._progressBar); + const workbenchMenubarOptions: IMenuWorkbenchButtonBarOptions = { + telemetrySource: 'interactiveEditorWidget-toolbar', + buttonConfigProvider: action => { + if (action.id === ACTION_REGENERATE_RESPONSE) { + return { showIcon: true, showLabel: false }; + } + return undefined; + } + }; + const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, MENU_INLINE_CHAT_WIDGET_STATUS, workbenchMenubarOptions); + this._store.add(statusButtonBar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); + this._store.add(statusButtonBar); + + const workbenchToolbarOptions = { hiddenItemStrategy: HiddenItemStrategy.NoHide, toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true - }, - actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { - - if (action instanceof SubmenuItemAction) { - return this._instantiationService.createInstance(DropdownWithDefaultActionViewItem, action, { ...options, renderKeybindingWithDefaultActionLabel: true, persistLastActionId: false }); - } - - if (action.id === ACTION_ACCEPT_CHANGES) { - const ButtonLikeActionViewItem = class extends MenuEntryActionViewItem { - - override render(container: HTMLElement): void { - this.options.icon = false; - super.render(container); - assertType(this.element); - this.element.classList.add('button-item'); - } - - protected override updateLabel(): void { - assertType(this.label); - assertType(this.action instanceof MenuItemAction); - const label = MenuItemAction.label(this.action.item, { renderShortTitle: true }); - const labelElements = renderLabelWithIcons(`$(check)${label}`); - reset(this.label, ...labelElements); - } - - protected override updateClass(): void { - // noop - } - }; - return this._instantiationService.createInstance(ButtonLikeActionViewItem, action, options); - } - - return createActionViewItem(this._instantiationService, action, options); } }; - // const statusToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, MENU_INLINE_CHAT_WIDGET_STATUS, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); - // this._store.add(statusToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - // this._store.add(statusToolbar); - - // TODO@jrieken extract this as re-usable MenuButtonBar similar to MenuWorkbenchToolBar - // add telemetry for workbench actions.... - // - const menu = this._menuService.createMenu(MENU_INLINE_CHAT_WIDGET_STATUS, this._contextKeyService); - const buttonBar = new ButtonBar(this._elements.statusToolbar); - this._store.add(menu); - this._store.add(buttonBar); - - const populateButtonBarFromMenu = () => { - - buttonBar.clear(); - - const actions = menu - .getActions({ renderShortTitle: true }) - .flatMap(entry => entry[1]); - - for (let i = 0; i < actions.length; i++) { - const action = actions[i]; - let btnAction: IAction; - let btn: IButton; - - if (action instanceof SubmenuItemAction && action.actions.length > 0) { - const [first, ...rest] = action.actions; - btnAction = first; - btn = buttonBar.addButtonWithDropdown({ - secondary: i > 0, - actions: rest, - contextMenuProvider: this._contextMenuService - }); - } else { - btnAction = action; - btn = buttonBar.addButton({ secondary: i > 0 }); - } - - btn.label = btnAction.label; - btn.enabled = btnAction.enabled; - const kb = _keybindingService.lookupKeybinding(btnAction.id); - if (kb) { - btn.element.title = localize('labelWithKeybinding', "{0} ({1})", btnAction.label, kb.getLabel()); - } - btn.onDidClick(async () => { - try { - await btnAction.run(); - } catch (error) { - onUnexpectedError(error); - } - }); - } - }; - - populateButtonBarFromMenu(); - this._store.add(menu.onDidChange(populateButtonBarFromMenu)); - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, MENU_INLINE_CHAT_WIDGET_FEEDBACK, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));