Skip to content

Commit be12637

Browse files
committed
extract MenuWorkbenchButtonBar as a thing, use for main buttons in inline chat
1 parent aa94277 commit be12637

File tree

4 files changed

+147
-103
lines changed

4 files changed

+147
-103
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button';
7+
import { ActionRunner, IAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
8+
import { Emitter, Event } from 'vs/base/common/event';
9+
import { DisposableStore } from 'vs/base/common/lifecycle';
10+
import { ThemeIcon } from 'vs/base/common/themables';
11+
import { localize } from 'vs/nls';
12+
import { MenuId, IMenuService, SubmenuItemAction, MenuItemAction } from 'vs/platform/actions/common/actions';
13+
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
14+
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
15+
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
16+
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
17+
18+
export type IButtonConfigProvider = (action: IAction) => {
19+
showIcon?: boolean;
20+
showLabel?: boolean;
21+
} | undefined;
22+
23+
export interface IMenuWorkbenchButtonBarOptions {
24+
telemetrySource?: string;
25+
buttonConfigProvider?: IButtonConfigProvider;
26+
}
27+
28+
export class MenuWorkbenchButtonBar extends ButtonBar {
29+
30+
private readonly _store = new DisposableStore();
31+
32+
private readonly _onDidChangeMenuItems = new Emitter<this>();
33+
readonly onDidChangeMenuItems: Event<this> = this._onDidChangeMenuItems.event;
34+
35+
constructor(
36+
container: HTMLElement,
37+
menuId: MenuId,
38+
options: IMenuWorkbenchButtonBarOptions | undefined,
39+
@IMenuService menuService: IMenuService,
40+
@IContextKeyService contextKeyService: IContextKeyService,
41+
@IContextMenuService contextMenuService: IContextMenuService,
42+
@IKeybindingService keybindingService: IKeybindingService,
43+
@ITelemetryService telemetryService: ITelemetryService,
44+
) {
45+
super(container);
46+
47+
const menu = menuService.createMenu(menuId, contextKeyService);
48+
this._store.add(menu);
49+
50+
const actionRunner = this._store.add(new ActionRunner());
51+
if (options?.telemetrySource) {
52+
actionRunner.onDidRun(e => {
53+
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>(
54+
'workbenchActionExecuted',
55+
{ id: e.action.id, from: options.telemetrySource! }
56+
);
57+
}, this._store);
58+
}
59+
60+
const conifgProvider: IButtonConfigProvider = options?.buttonConfigProvider ?? (() => ({ showLabel: true }));
61+
62+
const update = () => {
63+
64+
this.clear();
65+
66+
const actions = menu
67+
.getActions({ renderShortTitle: true })
68+
.flatMap(entry => entry[1]);
69+
70+
for (let i = 0; i < actions.length; i++) {
71+
const secondary = i > 0;
72+
const actionOrSubmenu = actions[i];
73+
let action: MenuItemAction | SubmenuItemAction;
74+
let btn: IButton;
75+
76+
if (actionOrSubmenu instanceof SubmenuItemAction && actionOrSubmenu.actions.length > 0) {
77+
const [first, ...rest] = actionOrSubmenu.actions;
78+
action = <MenuItemAction>first;
79+
btn = this.addButtonWithDropdown({
80+
secondary,
81+
actionRunner,
82+
actions: rest,
83+
contextMenuProvider: contextMenuService,
84+
});
85+
} else {
86+
action = actionOrSubmenu;
87+
btn = this.addButton({ secondary });
88+
}
89+
90+
btn.enabled = action.enabled;
91+
if (conifgProvider(action)?.showLabel ?? true) {
92+
btn.label = action.label;
93+
} else {
94+
btn.element.classList.add('monaco-text-button');
95+
}
96+
if (conifgProvider(action)?.showIcon && ThemeIcon.isThemeIcon(action.item.icon)) {
97+
btn.icon = action.item.icon;
98+
}
99+
const kb = keybindingService.lookupKeybinding(action.id);
100+
if (kb) {
101+
btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel());
102+
} else {
103+
btn.element.title = action.label;
104+
105+
}
106+
btn.onDidClick(async () => {
107+
actionRunner.run(action);
108+
});
109+
}
110+
this._onDidChangeMenuItems.fire(this);
111+
};
112+
this._store.add(menu.onDidChange(update));
113+
update();
114+
}
115+
116+
override dispose() {
117+
this._onDidChangeMenuItems.dispose();
118+
this._store.dispose();
119+
super.dispose();
120+
}
121+
}

src/vs/workbench/contrib/inlineChat/browser/inlineChat.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@
199199
padding: 0 4px;
200200
}
201201

202+
.monaco-editor .inline-chat .status .actions > .monaco-button.codicon {
203+
display: flex;
204+
}
205+
206+
.monaco-editor .inline-chat .status .actions > .monaco-button.codicon::before {
207+
align-self: center;
208+
}
209+
202210
.monaco-editor .inline-chat .status .actions .monaco-text-button {
203211
padding: 2px 4px
204212
}

src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export class ReRunRequestAction extends AbstractInlineChatAction {
169169
icon: Codicon.refresh,
170170
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EMPTY.negate(), CTX_INLINE_CHAT_LAST_RESPONSE_TYPE),
171171
menu: {
172-
id: MENU_INLINE_CHAT_WIDGET_FEEDBACK,
172+
id: MENU_INLINE_CHAT_WIDGET_STATUS,
173173
group: '2_feedback',
174174
order: 3,
175175
}

src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts

Lines changed: 17 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { localize } from 'vs/nls';
1212
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1313
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1414
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
15-
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';
15+
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';
1616
import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
1717
import { Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
1818
import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event';
@@ -28,14 +28,11 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
2828
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
2929
import { Position } from 'vs/editor/common/core/position';
3030
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
31-
import { DropdownWithDefaultActionViewItem, IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
3231
import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages';
3332
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
3433
import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language';
3534
import { ResourceLabel } from 'vs/workbench/browser/labels';
3635
import { FileKind } from 'vs/platform/files/common/files';
37-
import { IAction } from 'vs/base/common/actions';
38-
import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
3936
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
4037
import { LanguageSelector } from 'vs/editor/common/languageSelector';
4138
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
@@ -44,18 +41,14 @@ import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/inlineCh
4441
import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
4542
import { LineRange } from 'vs/editor/common/core/lineRange';
4643
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
47-
import { IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
4844
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
4945
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
5046
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution';
51-
import { assertType } from 'vs/base/common/types';
5247
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
5348
import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
5449
import { IdleValue } from 'vs/base/common/async';
5550
import * as aria from 'vs/base/browser/ui/aria/aria';
56-
import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button';
57-
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
58-
import { onUnexpectedError } from 'vs/base/common/errors';
51+
import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar';
5952

6053
const defaultAriaLabel = localize('aria-label', "Inline Chat Input");
6154

@@ -196,9 +189,7 @@ export class InlineChatWidget {
196189
@IKeybindingService private readonly _keybindingService: IKeybindingService,
197190
@IInstantiationService private readonly _instantiationService: IInstantiationService,
198191
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
199-
@IConfigurationService private readonly _configurationService: IConfigurationService,
200-
@IMenuService private readonly _menuService: IMenuService,
201-
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
192+
@IConfigurationService private readonly _configurationService: IConfigurationService
202193
) {
203194

204195
// input editor logic
@@ -298,104 +289,28 @@ export class InlineChatWidget {
298289
this._progressBar = new ProgressBar(this._elements.progress);
299290
this._store.add(this._progressBar);
300291

292+
const workbenchMenubarOptions: IMenuWorkbenchButtonBarOptions = {
293+
telemetrySource: 'interactiveEditorWidget-toolbar',
294+
buttonConfigProvider: action => {
295+
if (action.id === ACTION_REGENERATE_RESPONSE) {
296+
return { showIcon: true, showLabel: false };
297+
}
298+
return undefined;
299+
}
300+
};
301+
const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, MENU_INLINE_CHAT_WIDGET_STATUS, workbenchMenubarOptions);
302+
this._store.add(statusButtonBar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
303+
this._store.add(statusButtonBar);
304+
305+
301306
const workbenchToolbarOptions = {
302307
hiddenItemStrategy: HiddenItemStrategy.NoHide,
303308
toolbarOptions: {
304309
primaryGroup: () => true,
305310
useSeparatorsInPrimaryActions: true
306-
},
307-
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
308-
309-
if (action instanceof SubmenuItemAction) {
310-
return this._instantiationService.createInstance(DropdownWithDefaultActionViewItem, action, { ...options, renderKeybindingWithDefaultActionLabel: true, persistLastActionId: false });
311-
}
312-
313-
if (action.id === ACTION_ACCEPT_CHANGES) {
314-
const ButtonLikeActionViewItem = class extends MenuEntryActionViewItem {
315-
316-
override render(container: HTMLElement): void {
317-
this.options.icon = false;
318-
super.render(container);
319-
assertType(this.element);
320-
this.element.classList.add('button-item');
321-
}
322-
323-
protected override updateLabel(): void {
324-
assertType(this.label);
325-
assertType(this.action instanceof MenuItemAction);
326-
const label = MenuItemAction.label(this.action.item, { renderShortTitle: true });
327-
const labelElements = renderLabelWithIcons(`$(check)${label}`);
328-
reset(this.label, ...labelElements);
329-
}
330-
331-
protected override updateClass(): void {
332-
// noop
333-
}
334-
};
335-
return this._instantiationService.createInstance(ButtonLikeActionViewItem, <MenuItemAction>action, <IMenuEntryActionViewItemOptions>options);
336-
}
337-
338-
return createActionViewItem(this._instantiationService, action, options);
339-
}
340-
};
341-
// const statusToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, MENU_INLINE_CHAT_WIDGET_STATUS, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore });
342-
// this._store.add(statusToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
343-
// this._store.add(statusToolbar);
344-
345-
// TODO@jrieken extract this as re-usable MenuButtonBar similar to MenuWorkbenchToolBar
346-
// add telemetry for workbench actions....
347-
//
348-
const menu = this._menuService.createMenu(MENU_INLINE_CHAT_WIDGET_STATUS, this._contextKeyService);
349-
const buttonBar = new ButtonBar(this._elements.statusToolbar);
350-
this._store.add(menu);
351-
this._store.add(buttonBar);
352-
353-
const populateButtonBarFromMenu = () => {
354-
355-
buttonBar.clear();
356-
357-
const actions = menu
358-
.getActions({ renderShortTitle: true })
359-
.flatMap(entry => entry[1]);
360-
361-
for (let i = 0; i < actions.length; i++) {
362-
const action = actions[i];
363-
let btnAction: IAction;
364-
let btn: IButton;
365-
366-
if (action instanceof SubmenuItemAction && action.actions.length > 0) {
367-
const [first, ...rest] = action.actions;
368-
btnAction = first;
369-
btn = buttonBar.addButtonWithDropdown({
370-
secondary: i > 0,
371-
actions: rest,
372-
contextMenuProvider: this._contextMenuService
373-
});
374-
} else {
375-
btnAction = action;
376-
btn = buttonBar.addButton({ secondary: i > 0 });
377-
}
378-
379-
btn.label = btnAction.label;
380-
btn.enabled = btnAction.enabled;
381-
const kb = _keybindingService.lookupKeybinding(btnAction.id);
382-
if (kb) {
383-
btn.element.title = localize('labelWithKeybinding', "{0} ({1})", btnAction.label, kb.getLabel());
384-
}
385-
btn.onDidClick(async () => {
386-
try {
387-
await btnAction.run();
388-
} catch (error) {
389-
onUnexpectedError(error);
390-
}
391-
});
392311
}
393312
};
394313

395-
populateButtonBarFromMenu();
396-
this._store.add(menu.onDidChange(populateButtonBarFromMenu));
397-
398-
399314
const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, MENU_INLINE_CHAT_WIDGET_FEEDBACK, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore });
400315
this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
401316
this._store.add(feedbackToolbar);

0 commit comments

Comments
 (0)