From a6d380f5a9c0284d47ca6d2d6eb41ce2b0cf8f40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 00:49:00 +0000 Subject: [PATCH 01/11] Initial plan From 7a7a711f21835d3d8cf1509fac8aa496d584cc97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:05:59 +0000 Subject: [PATCH 02/11] Add createToggleActionViewItemProvider function and comprehensive tests Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --- src/vs/base/browser/ui/toggle/toggle.ts | 20 ++++++ src/vs/base/test/browser/actionbar.test.ts | 82 ++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index e490c9820d68c..5dac4b6331b32 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -11,6 +11,7 @@ import { ThemeIcon } from '../../../common/themables.js'; import { $, addDisposableListener, EventType, isActiveElement } from '../../dom.js'; import { IKeyboardEvent } from '../../keyboardEvent.js'; import { BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js'; +import { IActionViewItemProvider } from '../actionbar/actionbar.js'; import { HoverStyle, IHoverLifecycleOptions } from '../hover/hover.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; import { Widget } from '../widget.js'; @@ -496,3 +497,22 @@ export class CheckboxActionViewItem extends BaseActionViewItem { } } + +/** + * Creates an action view item provider that renders toggles for actions with a checked state + * and falls back to default button rendering for regular actions. + * + * @param toggleStyles - Optional styles to apply to toggle items + * @returns An IActionViewItemProvider that can be used with ActionBar + */ +export function createToggleActionViewItemProvider(toggleStyles?: IToggleStyles): IActionViewItemProvider { + return (action: IAction, options: IActionViewItemOptions) => { + // Only render as a toggle if the action has a checked property + if (action.checked !== undefined) { + return new ToggleActionViewItem(null, action, { ...options, toggleStyles }); + } + // Return undefined to fall back to default button rendering + return undefined; + }; +} + diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index 7ec25b1bf11ef..0f6f086696c2b 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -7,6 +7,8 @@ import assert from 'assert'; import { ActionBar, prepareActions } from '../../browser/ui/actionbar/actionbar.js'; import { Action, Separator } from '../../common/actions.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; +import { createToggleActionViewItemProvider, ToggleActionViewItem, unthemedToggleStyles } from '../../browser/ui/toggle/toggle.js'; +import { ActionViewItem } from '../../browser/ui/actionbar/actionViewItems.js'; suite('Actionbar', () => { @@ -60,4 +62,84 @@ suite('Actionbar', () => { actionbar.clear(); assert.strictEqual(actionbar.hasAction(a1), false); }); + + suite('ToggleActionViewItemProvider', () => { + + test('renders toggle for actions with checked state', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const toggleAction = store.add(new Action('toggle', 'Toggle', undefined, true, undefined)); + toggleAction.checked = true; + + actionbar.push(toggleAction); + + // Verify that the action was rendered as a toggle + assert.strictEqual(actionbar.viewItems.length, 1); + assert(actionbar.viewItems[0] instanceof ToggleActionViewItem, 'Action with checked state should render as ToggleActionViewItem'); + }); + + test('renders button for actions without checked state', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const buttonAction = store.add(new Action('button', 'Button')); + + actionbar.push(buttonAction); + + // Verify that the action was rendered as a regular button (ActionViewItem) + assert.strictEqual(actionbar.viewItems.length, 1); + assert(actionbar.viewItems[0] instanceof ActionViewItem, 'Action without checked state should render as ActionViewItem'); + assert(!(actionbar.viewItems[0] instanceof ToggleActionViewItem), 'Action without checked state should not render as ToggleActionViewItem'); + }); + + test('handles mixed actions (toggles and buttons)', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const toggleAction = store.add(new Action('toggle', 'Toggle')); + toggleAction.checked = false; + const buttonAction = store.add(new Action('button', 'Button')); + + actionbar.push([toggleAction, buttonAction]); + + // Verify that we have both types of items + assert.strictEqual(actionbar.viewItems.length, 2); + assert(actionbar.viewItems[0] instanceof ToggleActionViewItem, 'First action should be a toggle'); + assert(actionbar.viewItems[1] instanceof ActionViewItem, 'Second action should be a button'); + assert(!(actionbar.viewItems[1] instanceof ToggleActionViewItem), 'Second action should not be a toggle'); + }); + + test('toggle state changes when action checked changes', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const toggleAction = store.add(new Action('toggle', 'Toggle')); + toggleAction.checked = false; + + actionbar.push(toggleAction); + + // Verify the toggle element has the correct aria-checked attribute + const toggleViewItem = actionbar.viewItems[0] as ToggleActionViewItem; + const toggleElement = toggleViewItem.element?.querySelector('[role="checkbox"]') as HTMLElement; + assert(toggleElement, 'Toggle element should exist'); + assert.strictEqual(toggleElement.getAttribute('aria-checked'), 'false', 'Initial checked state should be false'); + + // Change the action's checked state + toggleAction.checked = true; + assert.strictEqual(toggleElement.getAttribute('aria-checked'), 'true', 'Toggle should update when action checked changes'); + }); + }); }); From 84f3df48d28f4b8fa070131b162a4d43c2ed3f43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 07:14:06 +0000 Subject: [PATCH 03/11] Apply createToggleActionViewItemProvider to quick input ActionBars Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --- .../browser/quickInputController.ts | 19 ++++++++++++++----- .../quickinput/browser/quickInputList.ts | 9 ++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 4a8f8166eb693..46ad8f9d09e18 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -33,8 +33,8 @@ import { IConfigurationService } from '../../configuration/common/configuration. import { Platform, platform, setTimeout0 } from '../../../base/common/platform.js'; import { getWindowControlsStyle, WindowControlsStyle } from '../../window/common/window.js'; import { getZoomFactor } from '../../../base/browser/browser.js'; -import { TriStateCheckbox } from '../../../base/browser/ui/toggle/toggle.js'; -import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; +import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../base/browser/ui/toggle/toggle.js'; +import { defaultCheckboxStyles, defaultToggleStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; @@ -146,12 +146,18 @@ export class QuickInputController extends Disposable { const titleBar = dom.append(container, $('.quick-input-titlebar')); - const leftActionBar = this._register(new ActionBar(titleBar, { hoverDelegate: this.options.hoverDelegate })); + const leftActionBar = this._register(new ActionBar(titleBar, { + hoverDelegate: this.options.hoverDelegate, + actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + })); leftActionBar.domNode.classList.add('quick-input-left-action-bar'); const title = dom.append(titleBar, $('.quick-input-title')); - const rightActionBar = this._register(new ActionBar(titleBar, { hoverDelegate: this.options.hoverDelegate })); + const rightActionBar = this._register(new ActionBar(titleBar, { + hoverDelegate: this.options.hoverDelegate, + actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + })); rightActionBar.domNode.classList.add('quick-input-right-action-bar'); const headerContainer = dom.append(container, $('.quick-input-header')); @@ -184,7 +190,10 @@ export class QuickInputController extends Disposable { countContainer.setAttribute('aria-live', 'polite'); const count = this._register(new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge)); - const inlineActionBar = this._register(new ActionBar(headerContainer, { hoverDelegate: this.options.hoverDelegate })); + const inlineActionBar = this._register(new ActionBar(headerContainer, { + hoverDelegate: this.options.hoverDelegate, + actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + })); inlineActionBar.domNode.classList.add('quick-input-inline-action-bar'); const okContainer = dom.append(headerContainer, $('.quick-input-action')); diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index f0b00be81d29f..30b5552198bd2 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -15,7 +15,7 @@ import { IIconLabelValueOptions, IconLabel } from '../../../base/browser/ui/icon import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IListVirtualDelegate } from '../../../base/browser/ui/list/list.js'; import { IListAccessibilityProvider, IListStyles } from '../../../base/browser/ui/list/listWidget.js'; -import { Checkbox } from '../../../base/browser/ui/toggle/toggle.js'; +import { Checkbox, createToggleActionViewItemProvider } from '../../../base/browser/ui/toggle/toggle.js'; import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from '../../../base/browser/ui/tree/tree.js'; import { equals } from '../../../base/common/arrays.js'; @@ -38,7 +38,7 @@ import { localize } from '../../../nls.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { WorkbenchObjectTree } from '../../list/browser/listService.js'; -import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; +import { defaultCheckboxStyles, defaultToggleStyles } from '../../theme/browser/defaultStyles.js'; import { isDark } from '../../theme/common/theme.js'; import { IThemeService } from '../../theme/common/themeService.js'; import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickFocus, QuickPickItem } from '../common/quickInput.js'; @@ -371,7 +371,10 @@ abstract class BaseQuickInputListRenderer implement data.separator = dom.append(data.entry, $('.quick-input-list-separator')); // Actions - data.actionBar = new ActionBar(data.entry, this.hoverDelegate ? { hoverDelegate: this.hoverDelegate } : undefined); + data.actionBar = new ActionBar(data.entry, { + ...(this.hoverDelegate ? { hoverDelegate: this.hoverDelegate } : undefined), + actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + }); data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar'); data.toDisposeTemplate.add(data.actionBar); From de46c730ac30ced9f09307a62c1aa11fcacb02a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:58:15 +0000 Subject: [PATCH 04/11] Add toggle property to IQuickInputButton and update quickInputButtonToAction Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --- src/vs/base/test/browser/actionbar.test.ts | 27 +++++++++++++++++++ .../quickinput/browser/quickInputUtils.ts | 9 ++++++- .../platform/quickinput/common/quickInput.ts | 8 ++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index 0f6f086696c2b..b0d64c868a694 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -141,5 +141,32 @@ suite('Actionbar', () => { toggleAction.checked = true; assert.strictEqual(toggleElement.getAttribute('aria-checked'), 'true', 'Toggle should update when action checked changes'); }); + + test('quick input button with toggle property creates action with checked state', async function () { + const { quickInputButtonToAction } = await import('../../../platform/quickinput/browser/quickInputUtils.js'); + + // Create a button with toggle property + const toggleButton = { + iconClass: 'test-icon', + tooltip: 'Toggle Button', + toggle: { checked: true } + }; + + const action = quickInputButtonToAction(toggleButton, 'test-id', () => { }); + + // Verify the action has checked property set + assert.strictEqual(action.checked, true, 'Action should have checked property set to true'); + + // Create a button without toggle property + const regularButton = { + iconClass: 'test-icon', + tooltip: 'Regular Button' + }; + + const regularAction = quickInputButtonToAction(regularButton, 'test-id-2', () => { }); + + // Verify the action doesn't have checked property + assert.strictEqual(regularAction.checked, undefined, 'Regular action should not have checked property'); + }); }); }); diff --git a/src/vs/platform/quickinput/browser/quickInputUtils.ts b/src/vs/platform/quickinput/browser/quickInputUtils.ts index 6f968ed4d9693..4ec04cd565c99 100644 --- a/src/vs/platform/quickinput/browser/quickInputUtils.ts +++ b/src/vs/platform/quickinput/browser/quickInputUtils.ts @@ -49,7 +49,7 @@ export function quickInputButtonToAction(button: IQuickInputButton, id: string, cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible'; } - return { + const action: IAction = { id, label: '', tooltip: button.tooltip || '', @@ -57,6 +57,13 @@ export function quickInputButtonToAction(button: IQuickInputButton, id: string, enabled: true, run }; + + // If the button has a toggle property, set the checked state on the action + if (button.toggle) { + action.checked = button.toggle.checked; + } + + return action; } export function renderQuickInputDescription(description: string, container: HTMLElement, actionHandler: { callback: (content: string) => void; disposables: DisposableStore }) { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 70829b21a0ac6..9a387f3f23761 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -831,6 +831,14 @@ export interface IQuickInputButton { * @note This property is ignored if the button was added to a QuickPickItem. */ location?: QuickInputButtonLocation; + /** + * When present, indicates that the button is a toggle button that can be checked or unchecked. + * + * @note This property is currently only applicable to buttons with {@link QuickInputButtonLocation.Input} location. + * It must be set for such buttons, and the state will be updated when the button is toggled. + * It cannot be set for buttons with other location values. + */ + readonly toggle?: { checked: boolean }; } /** From 381d44e0ca1fe0d63cd23b7ecb169f05e8ddff1d Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 16 Dec 2025 16:00:28 -0800 Subject: [PATCH 05/11] treat toggles like buttons --- src/vs/base/browser/ui/findinput/findInput.ts | 12 +++- src/vs/base/browser/ui/inputbox/inputBox.ts | 21 ++++++- src/vs/base/browser/ui/toggle/toggle.ts | 1 - .../browser/gotoLineQuickAccess.ts | 37 ++++++------ .../quickinput/browser/media/quickInput.css | 30 ++++++++-- .../platform/quickinput/browser/quickInput.ts | 40 +++++++++++-- .../quickinput/browser/quickInputBox.ts | 22 ++++++- .../browser/quickInputController.ts | 1 + .../quickinput/browser/quickInputUtils.ts | 57 ++++++++++++++----- .../browser/tree/quickInputTreeRenderer.ts | 7 ++- 10 files changed, 177 insertions(+), 51 deletions(-) diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 2c9c47f4d5e13..2518cf4250d23 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -13,6 +13,8 @@ import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBox import { Widget } from '../widget.js'; import { Emitter, Event } from '../../../common/event.js'; import { KeyCode } from '../../../common/keyCodes.js'; +import { IAction } from '../../../common/actions.js'; +import type { IActionViewItemProvider } from '../actionbar/actionbar.js'; import './findInput.css'; import * as nls from '../../../../nls.js'; import { DisposableStore, MutableDisposable } from '../../../common/lifecycle.js'; @@ -34,6 +36,8 @@ export interface IFindInputOptions { readonly appendWholeWordsLabel?: string; readonly appendRegexLabel?: string; readonly additionalToggles?: Toggle[]; + readonly actions?: ReadonlyArray; + readonly actionViewItemProvider?: IActionViewItemProvider; readonly showHistoryHint?: () => boolean; readonly toggleStyles: IToggleStyles; readonly inputBoxStyles: IInputBoxStyles; @@ -112,7 +116,9 @@ export class FindInput extends Widget { flexibleWidth, flexibleMaxHeight, inputBoxStyles: options.inputBoxStyles, - history: options.history + history: options.history, + actions: options.actions, + actionViewItemProvider: options.actionViewItemProvider })); if (this.showCommonFindToggles) { @@ -307,6 +313,10 @@ export class FindInput extends Widget { this.updateInputBoxPadding(); } + public setActions(actions: ReadonlyArray | undefined, actionViewItemProvider?: IActionViewItemProvider): void { + this.inputBox.setActions(actions, actionViewItemProvider); + } + private updateInputBoxPadding(controlsHidden = false) { if (controlsHidden) { this.inputBox.paddingRight = 0; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index df93d742649f7..3438890dd233b 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -8,7 +8,7 @@ import * as cssJs from '../../cssValue.js'; import { DomEmitter } from '../../event.js'; import { renderFormattedText, renderText } from '../../formattedTextRenderer.js'; import { IHistoryNavigationWidget } from '../../history.js'; -import { ActionBar } from '../actionbar/actionbar.js'; +import { ActionBar, IActionViewItemProvider } from '../actionbar/actionbar.js'; import * as aria from '../aria/aria.js'; import { AnchorAlignment, IContextViewProvider } from '../contextview/contextview.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; @@ -37,6 +37,7 @@ export interface IInputOptions { readonly flexibleWidth?: boolean; readonly flexibleMaxHeight?: number; readonly actions?: ReadonlyArray; + readonly actionViewItemProvider?: IActionViewItemProvider; readonly inputBoxStyles: IInputBoxStyles; readonly history?: IHistory; } @@ -206,13 +207,29 @@ export class InputBox extends Widget { // Support actions if (this.options.actions) { - this.actionbar = this._register(new ActionBar(this.element)); + this.actionbar = this._register(new ActionBar(this.element, { + actionViewItemProvider: this.options.actionViewItemProvider + })); this.actionbar.push(this.options.actions, { icon: true, label: false }); } this.applyStyles(); } + public setActions(actions: ReadonlyArray | undefined, actionViewItemProvider?: IActionViewItemProvider): void { + if (this.actionbar) { + this.actionbar.clear(); + if (actions) { + this.actionbar.push(actions, { icon: true, label: false }); + } + } else if (actions) { + this.actionbar = this._register(new ActionBar(this.element, { + actionViewItemProvider: actionViewItemProvider ?? this.options.actionViewItemProvider + })); + this.actionbar.push(actions, { icon: true, label: false }); + } + } + protected onBlur(): void { this._hideMessage(); if (this.options.showPlaceholderOnFocus) { diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index 5dac4b6331b32..4944052c84434 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -515,4 +515,3 @@ export function createToggleActionViewItemProvider(toggleStyles?: IToggleStyles) return undefined; }; } - diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts index f178eb3c8b330..5a96c2d69872a 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts @@ -3,15 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; -import { IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputButton, IQuickPick, IQuickPickItem, QuickInputButtonLocation } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from '../../../../platform/theme/common/colors/inputColors.js'; -import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { getCodeEditor } from '../../../browser/editorBrowser.js'; import { EditorOption, RenderLineNumbersType } from '../../../common/config/editorOptions.js'; import { IPosition } from '../../../common/core/position.js'; @@ -77,13 +75,21 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor } })); + // Add a toggle to switch between 1- and 0-based offsets. + const offsetButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.indexZero), + tooltip: localize('gotoLineToggleButton', "Toggle Zero-Based Offset"), + location: QuickInputButtonLocation.Input, + toggle: { checked: this.useZeroBasedOffset } + }; + // React to picker changes const updatePickerAndEditor = () => { const inputText = picker.value.trim().substring(AbstractGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX.length); const { inOffsetMode, lineNumber, column, label } = this.parsePosition(editor, inputText); // Show toggle only when input text starts with '::'. - toggle.visible = !!inOffsetMode; + picker.buttons = inOffsetMode ? [offsetButton] : []; // Picker picker.items = [{ @@ -116,23 +122,12 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor this.addDecorations(editor, range); }; - // Add a toggle to switch between 1- and 0-based offsets. - const toggle = new Toggle({ - title: localize('gotoLineToggle', "Use Zero-Based Offset"), - icon: Codicon.indexZero, - isChecked: this.useZeroBasedOffset, - inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), - inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), - inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground) - }); - - disposables.add( - toggle.onChange(() => { - this.useZeroBasedOffset = !this.useZeroBasedOffset; + disposables.add(picker.onDidTriggerButton(button => { + if (button === offsetButton) { + this.useZeroBasedOffset = button.toggle?.checked ?? !this.useZeroBasedOffset; updatePickerAndEditor(); - })); - - picker.toggles = [toggle]; + } + })); updatePickerAndEditor(); disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor())); diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 7d6e69a3d7003..8d733d69c3564 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -20,6 +20,16 @@ border-top-left-radius: 5px; } +.quick-input-widget .monaco-inputbox .monaco-action-bar { + top: 0; +} + +.quick-input-widget .monaco-custom-toggle { + padding: 3px; + border-radius: 5px; + box-sizing: content-box; +} + .quick-input-left-action-bar { display: flex; margin-left: 4px; @@ -301,7 +311,8 @@ overflow: visible; } -.quick-input-list .quick-input-list-entry-action-bar .action-label { +.quick-input-list .quick-input-list-entry-action-bar .action-label, +.quick-input-list .quick-input-list-entry-action-bar .monaco-custom-toggle { /* * By default, actions in the quick input action bar are hidden * until hovered over them or selected. @@ -327,7 +338,12 @@ .quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar .action-label, .quick-input-list .quick-input-list-entry.focus-inside .quick-input-list-entry-action-bar .action-label, .quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label, -.quick-input-list .monaco-list-row.passive-focused .quick-input-list-entry-action-bar .action-label { +.quick-input-list .monaco-list-row.passive-focused .quick-input-list-entry-action-bar .action-label, +.quick-input-list .quick-input-list-entry .quick-input-list-entry-action-bar .monaco-custom-toggle.always-visible, +.quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar .monaco-custom-toggle, +.quick-input-list .quick-input-list-entry.focus-inside .quick-input-list-entry-action-bar .monaco-custom-toggle, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .monaco-custom-toggle, +.quick-input-list .monaco-list-row.passive-focused .quick-input-list-entry-action-bar .monaco-custom-toggle { display: flex; } @@ -456,7 +472,8 @@ overflow: visible; } -.quick-input-tree .quick-input-tree-entry-action-bar .action-label { +.quick-input-tree .quick-input-tree-entry-action-bar .action-label, +.quick-input-tree .quick-input-tree-entry-action-bar .monaco-custom-toggle { /* * By default, actions in the quick input action bar are hidden * until hovered over them or selected. @@ -482,7 +499,12 @@ .quick-input-tree .quick-input-tree-entry:hover .quick-input-tree-entry-action-bar .action-label, .quick-input-tree .quick-input-tree-entry.focus-inside .quick-input-tree-entry-action-bar .action-label, .quick-input-tree .monaco-list-row.focused .quick-input-tree-entry-action-bar .action-label, -.quick-input-tree .monaco-list-row.passive-focused .quick-input-tree-entry-action-bar .action-label { +.quick-input-tree .monaco-list-row.passive-focused .quick-input-tree-entry-action-bar .action-label, +.quick-input-tree .quick-input-tree-entry .quick-input-tree-entry-action-bar .monaco-custom-toggle.always-visible, +.quick-input-tree .quick-input-tree-entry:hover .quick-input-tree-entry-action-bar .monaco-custom-toggle, +.quick-input-tree .quick-input-tree-entry.focus-inside .quick-input-tree-entry-action-bar .monaco-custom-toggle, +.quick-input-tree .monaco-list-row.focused .quick-input-tree-entry-action-bar .monaco-custom-toggle, +.quick-input-tree .monaco-list-row.passive-focused .quick-input-tree-entry-action-bar .monaco-custom-toggle { display: flex; } diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 199fbc3da617c..8cb30e034bf3b 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -167,6 +167,7 @@ export abstract class QuickInput extends Disposable implements IQuickInput { private _leftButtons: IQuickInputButton[] = []; private _rightButtons: IQuickInputButton[] = []; private _inlineButtons: IQuickInputButton[] = []; + private _inputButtons: IQuickInputButton[] = []; private buttonsUpdated = false; private _toggles: IQuickInputToggle[] = []; private togglesUpdated = false; @@ -296,14 +297,17 @@ export abstract class QuickInput extends Disposable implements IQuickInput { return [ ...this._leftButtons, ...this._rightButtons, - ...this._inlineButtons + ...this._inlineButtons, + ...this._inputButtons ]; } set buttons(buttons: IQuickInputButton[]) { + // TODO: use a switch this._leftButtons = buttons.filter(b => b === backButton); - this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline); + this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline && b.location !== QuickInputButtonLocation.Input); this._inlineButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Inline); + this._inputButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Input); this.buttonsUpdated = true; this.update(); } @@ -438,7 +442,12 @@ export abstract class QuickInput extends Disposable implements IQuickInput { .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => this.onDidTriggerButtonEmitter.fire(button) + async () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + this.onDidTriggerButtonEmitter.fire(button); + } )); this.ui.leftActionBar.push(leftButtons, { icon: true, label: false }); this.ui.rightActionBar.clear(); @@ -446,7 +455,12 @@ export abstract class QuickInput extends Disposable implements IQuickInput { .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => this.onDidTriggerButtonEmitter.fire(button) + async () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + this.onDidTriggerButtonEmitter.fire(button); + } )); this.ui.rightActionBar.push(rightButtons, { icon: true, label: false }); this.ui.inlineActionBar.clear(); @@ -454,9 +468,25 @@ export abstract class QuickInput extends Disposable implements IQuickInput { .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => this.onDidTriggerButtonEmitter.fire(button) + async () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + this.onDidTriggerButtonEmitter.fire(button); + } )); this.ui.inlineActionBar.push(inlineButtons, { icon: true, label: false }); + this.ui.inputBox.actions = this._inputButtons + .map((button, index) => quickInputButtonToAction( + button, + `id-${index}`, + async () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + this.onDidTriggerButtonEmitter.fire(button); + } + )); } if (this.togglesUpdated) { this.togglesUpdated = false; diff --git a/src/vs/platform/quickinput/browser/quickInputBox.ts b/src/vs/platform/quickinput/browser/quickInputBox.ts index 09032647c1f33..e3d9f673a322b 100644 --- a/src/vs/platform/quickinput/browser/quickInputBox.ts +++ b/src/vs/platform/quickinput/browser/quickInputBox.ts @@ -6,7 +6,9 @@ import * as dom from '../../../base/browser/dom.js'; import { FindInput } from '../../../base/browser/ui/findinput/findInput.js'; import { IInputBoxStyles, IRange, MessageType } from '../../../base/browser/ui/inputbox/inputBox.js'; -import { IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js'; +import { createToggleActionViewItemProvider, IToggleStyles, Toggle, unthemedToggleStyles } from '../../../base/browser/ui/toggle/toggle.js'; +import { IAction } from '../../../base/common/actions.js'; +import { IActionViewItemProvider } from '../../../base/browser/ui/actionbar/actionbar.js'; import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import Severity from '../../../base/common/severity.js'; import './media/quickInput.css'; @@ -25,7 +27,15 @@ export class QuickInputBox extends Disposable { ) { super(); this.container = dom.append(this.parent, $('.quick-input-box')); - this.findInput = this._register(new FindInput(this.container, undefined, { label: '', inputBoxStyles, toggleStyles })); + this.findInput = this._register(new FindInput( + this.container, + undefined, + { + label: '', + inputBoxStyles, + toggleStyles, + actionViewItemProvider: createToggleActionViewItemProvider(unthemedToggleStyles) + })); const input = this.findInput.inputBox.inputElement; input.role = 'textbox'; input.ariaHasPopup = 'menu'; @@ -100,6 +110,14 @@ export class QuickInputBox extends Disposable { this.findInput.setAdditionalToggles(toggles); } + set actions(actions: ReadonlyArray | undefined) { + this.setActions(actions); + } + + setActions(actions: ReadonlyArray | undefined, actionViewItemProvider?: IActionViewItemProvider): void { + this.findInput.setActions(actions, actionViewItemProvider); + } + get ariaLabel(): string { return this.findInput.inputBox.inputElement.getAttribute('aria-label') || ''; } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 46ad8f9d09e18..701a4140d30d3 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -688,6 +688,7 @@ export class QuickInputController extends Disposable { ui.tree.sortByLabel = true; ui.ignoreFocusOut = false; ui.inputBox.toggles = undefined; + ui.inputBox.actions = undefined; const backKeybindingLabel = this.options.backKeybindingLabel(); backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); diff --git a/src/vs/platform/quickinput/browser/quickInputUtils.ts b/src/vs/platform/quickinput/browser/quickInputUtils.ts index 4ec04cd565c99..826f736df7d9f 100644 --- a/src/vs/platform/quickinput/browser/quickInputUtils.ts +++ b/src/vs/platform/quickinput/browser/quickInputUtils.ts @@ -43,25 +43,56 @@ function getIconClass(iconPath: { dark: URI; light?: URI } | undefined): string return iconClass; } +class QuickInputToggleButtonAction implements IAction { + class: string | undefined; + + constructor( + public readonly id: string, + public label: string, + public tooltip: string, + className: string | undefined, + public enabled: boolean, + private _checked: boolean, + public run: () => unknown + ) { + this.class = className; + } + + get checked(): boolean { + return this._checked; + } + + set checked(value: boolean) { + this._checked = value; + // Toggles behave like buttons. When clicked, they run... the only difference is that their checked state also changes. + this.run(); + } +} + export function quickInputButtonToAction(button: IQuickInputButton, id: string, run: () => unknown): IAction { let cssClasses = button.iconClass || getIconClass(button.iconPath); if (button.alwaysVisible) { cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible'; } - const action: IAction = { - id, - label: '', - tooltip: button.tooltip || '', - class: cssClasses, - enabled: true, - run - }; - - // If the button has a toggle property, set the checked state on the action - if (button.toggle) { - action.checked = button.toggle.checked; - } + const action = button.toggle + ? new QuickInputToggleButtonAction( + id, + '', + button.tooltip || '', + cssClasses, + true, + button.toggle.checked, + run + ) + : { + id, + label: '', + tooltip: button.tooltip || '', + class: cssClasses, + enabled: true, + run, + }; return action; } diff --git a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts index ba52d2bdea658..0ca0c39bbddaf 100644 --- a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts +++ b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts @@ -9,7 +9,7 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; -import { TriStateCheckbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { createToggleActionViewItemProvider, TriStateCheckbox, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js'; import { ITreeElementRenderDetails, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -76,7 +76,10 @@ export class QuickInputTreeRenderer extends Disposable supportIcons: true, hoverDelegate: this._hoverDelegate })); - const actionBar = store.add(new ActionBar(entry, this._hoverDelegate ? { hoverDelegate: this._hoverDelegate } : undefined)); + const actionBar = store.add(new ActionBar(entry, { + actionViewItemProvider: createToggleActionViewItemProvider(unthemedToggleStyles), + hoverDelegate: this._hoverDelegate + })); actionBar.domNode.classList.add('quick-input-tree-entry-action-bar'); return { toDisposeTemplate: store, From e9e16e85e02a8829051ec70665ce0b061c47f0b8 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 17 Dec 2025 08:58:35 -0800 Subject: [PATCH 06/11] Styles --- .../platform/quickinput/browser/quickInputBox.ts | 4 ++-- .../quickinput/browser/quickInputController.ts | 13 +++++++------ .../quickinput/browser/quickInputList.ts | 16 ++++++++++------ .../browser/tree/quickInputTreeController.ts | 3 +++ .../browser/tree/quickInputTreeRenderer.ts | 5 +++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputBox.ts b/src/vs/platform/quickinput/browser/quickInputBox.ts index e3d9f673a322b..0ecb0371795e2 100644 --- a/src/vs/platform/quickinput/browser/quickInputBox.ts +++ b/src/vs/platform/quickinput/browser/quickInputBox.ts @@ -6,7 +6,7 @@ import * as dom from '../../../base/browser/dom.js'; import { FindInput } from '../../../base/browser/ui/findinput/findInput.js'; import { IInputBoxStyles, IRange, MessageType } from '../../../base/browser/ui/inputbox/inputBox.js'; -import { createToggleActionViewItemProvider, IToggleStyles, Toggle, unthemedToggleStyles } from '../../../base/browser/ui/toggle/toggle.js'; +import { createToggleActionViewItemProvider, IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js'; import { IAction } from '../../../base/common/actions.js'; import { IActionViewItemProvider } from '../../../base/browser/ui/actionbar/actionbar.js'; import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; @@ -34,7 +34,7 @@ export class QuickInputBox extends Disposable { label: '', inputBoxStyles, toggleStyles, - actionViewItemProvider: createToggleActionViewItemProvider(unthemedToggleStyles) + actionViewItemProvider: createToggleActionViewItemProvider(toggleStyles) })); const input = this.findInput.inputBox.inputElement; input.role = 'textbox'; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 701a4140d30d3..648a477c3309c 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -34,7 +34,7 @@ import { Platform, platform, setTimeout0 } from '../../../base/common/platform.j import { getWindowControlsStyle, WindowControlsStyle } from '../../window/common/window.js'; import { getZoomFactor } from '../../../base/browser/browser.js'; import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../base/browser/ui/toggle/toggle.js'; -import { defaultCheckboxStyles, defaultToggleStyles } from '../../theme/browser/defaultStyles.js'; +import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; @@ -148,7 +148,7 @@ export class QuickInputController extends Disposable { const leftActionBar = this._register(new ActionBar(titleBar, { hoverDelegate: this.options.hoverDelegate, - actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + actionViewItemProvider: createToggleActionViewItemProvider(this.styles.toggle) })); leftActionBar.domNode.classList.add('quick-input-left-action-bar'); @@ -156,7 +156,7 @@ export class QuickInputController extends Disposable { const rightActionBar = this._register(new ActionBar(titleBar, { hoverDelegate: this.options.hoverDelegate, - actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + actionViewItemProvider: createToggleActionViewItemProvider(this.styles.toggle) })); rightActionBar.domNode.classList.add('quick-input-right-action-bar'); @@ -192,7 +192,7 @@ export class QuickInputController extends Disposable { const inlineActionBar = this._register(new ActionBar(headerContainer, { hoverDelegate: this.options.hoverDelegate, - actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + actionViewItemProvider: createToggleActionViewItemProvider(this.styles.toggle) })); inlineActionBar.domNode.classList.add('quick-input-inline-action-bar'); @@ -222,7 +222,7 @@ export class QuickInputController extends Disposable { // List const listId = this.idPrefix + 'list'; - const list = this._register(this.instantiationService.createInstance(QuickInputList, container, this.options.hoverDelegate, this.options.linkOpenerDelegate, listId)); + const list = this._register(this.instantiationService.createInstance(QuickInputList, container, this.options.hoverDelegate, this.options.linkOpenerDelegate, listId, this.styles)); inputBox.setAttribute('aria-controls', listId); this._register(list.onDidChangeFocus(() => { if (inputBox.hasFocus()) { @@ -265,7 +265,8 @@ export class QuickInputController extends Disposable { const tree = this._register(this.instantiationService.createInstance( QuickInputTreeController, container, - this.options.hoverDelegate + this.options.hoverDelegate, + this.styles )); this._register(tree.tree.onDidChangeFocus(() => { if (inputBox.hasFocus()) { diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index 30b5552198bd2..734f9f3075b52 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -15,7 +15,7 @@ import { IIconLabelValueOptions, IconLabel } from '../../../base/browser/ui/icon import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IListVirtualDelegate } from '../../../base/browser/ui/list/list.js'; import { IListAccessibilityProvider, IListStyles } from '../../../base/browser/ui/list/listWidget.js'; -import { Checkbox, createToggleActionViewItemProvider } from '../../../base/browser/ui/toggle/toggle.js'; +import { Checkbox, createToggleActionViewItemProvider, IToggleStyles } from '../../../base/browser/ui/toggle/toggle.js'; import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from '../../../base/browser/ui/tree/tree.js'; import { equals } from '../../../base/common/arrays.js'; @@ -38,10 +38,11 @@ import { localize } from '../../../nls.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { WorkbenchObjectTree } from '../../list/browser/listService.js'; -import { defaultCheckboxStyles, defaultToggleStyles } from '../../theme/browser/defaultStyles.js'; +import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { isDark } from '../../theme/common/theme.js'; import { IThemeService } from '../../theme/common/themeService.js'; import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickFocus, QuickPickItem } from '../common/quickInput.js'; +import { IQuickInputStyles } from './quickInput.js'; import { quickInputButtonToAction } from './quickInputUtils.js'; const $ = dom.$; @@ -325,6 +326,7 @@ abstract class BaseQuickInputListRenderer implement constructor( private readonly hoverDelegate: IHoverDelegate | undefined, + private readonly toggleStyles: IToggleStyles ) { } // TODO: only do the common stuff here and have a subclass handle their specific stuff @@ -373,7 +375,7 @@ abstract class BaseQuickInputListRenderer implement // Actions data.actionBar = new ActionBar(data.entry, { ...(this.hoverDelegate ? { hoverDelegate: this.hoverDelegate } : undefined), - actionViewItemProvider: createToggleActionViewItemProvider(defaultToggleStyles) + actionViewItemProvider: createToggleActionViewItemProvider(this.toggleStyles) }); data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar'); data.toDisposeTemplate.add(data.actionBar); @@ -403,9 +405,10 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer void, id: string, + private styles: IQuickInputStyles, @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); this._container = dom.append(this.parent, $('.quick-input-list')); - this._separatorRenderer = new QuickPickSeparatorElementRenderer(hoverDelegate); - this._itemRenderer = instantiationService.createInstance(QuickPickItemElementRenderer, hoverDelegate); + this._separatorRenderer = new QuickPickSeparatorElementRenderer(hoverDelegate, this.styles.toggle); + this._itemRenderer = instantiationService.createInstance(QuickPickItemElementRenderer, hoverDelegate, this.styles.toggle); this._tree = this._register(instantiationService.createInstance( WorkbenchObjectTree, 'QuickInput', diff --git a/src/vs/platform/quickinput/browser/tree/quickInputTreeController.ts b/src/vs/platform/quickinput/browser/tree/quickInputTreeController.ts index 452ba2a5c97ce..b8342cf028fec 100644 --- a/src/vs/platform/quickinput/browser/tree/quickInputTreeController.ts +++ b/src/vs/platform/quickinput/browser/tree/quickInputTreeController.ts @@ -20,6 +20,7 @@ import { QuickInputTreeFilter } from './quickInputTreeFilter.js'; import { QuickInputCheckboxStateHandler, QuickInputTreeRenderer } from './quickInputTreeRenderer.js'; import { QuickInputTreeSorter } from './quickInputTreeSorter.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { IQuickInputStyles } from '../quickInput.js'; const $ = dom.$; const flatHierarchyClass = 'quick-input-tree-flat'; @@ -78,6 +79,7 @@ export class QuickInputTreeController extends Disposable { constructor( container: HTMLElement, hoverDelegate: IHoverDelegate | undefined, + styles: IQuickInputStyles, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -89,6 +91,7 @@ export class QuickInputTreeController extends Disposable { this._onDidTriggerButton, this.onDidChangeCheckboxState, this._checkboxStateHandler, + styles.toggle )); this._filter = this.instantiationService.createInstance(QuickInputTreeFilter); this._sorter = this._register(new QuickInputTreeSorter()); diff --git a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts index 0ca0c39bbddaf..a4db0d9481c72 100644 --- a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts +++ b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts @@ -9,7 +9,7 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; -import { createToggleActionViewItemProvider, TriStateCheckbox, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js'; +import { createToggleActionViewItemProvider, IToggleStyles, TriStateCheckbox } from '../../../../base/browser/ui/toggle/toggle.js'; import { ITreeElementRenderDetails, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -52,6 +52,7 @@ export class QuickInputTreeRenderer extends Disposable private readonly _buttonTriggeredEmitter: Emitter>, private readonly onCheckedEvent: Event>, private readonly _checkboxStateHandler: QuickInputCheckboxStateHandler, + private readonly _toggleStyles: IToggleStyles, @IThemeService private readonly _themeService: IThemeService, ) { super(); @@ -77,7 +78,7 @@ export class QuickInputTreeRenderer extends Disposable hoverDelegate: this._hoverDelegate })); const actionBar = store.add(new ActionBar(entry, { - actionViewItemProvider: createToggleActionViewItemProvider(unthemedToggleStyles), + actionViewItemProvider: createToggleActionViewItemProvider(this._toggleStyles), hoverDelegate: this._hoverDelegate })); actionBar.domNode.classList.add('quick-input-tree-entry-action-bar'); From b73657eb10d7c8db3bb100f72289bbc9d739aa1a Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 17 Dec 2025 10:12:37 -0800 Subject: [PATCH 07/11] fix ci --- src/vs/base/test/browser/actionbar.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index b0d64c868a694..60f9902bc3f62 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -131,20 +131,19 @@ suite('Actionbar', () => { actionbar.push(toggleAction); - // Verify the toggle element has the correct aria-checked attribute + // Verify the toggle view item was created const toggleViewItem = actionbar.viewItems[0] as ToggleActionViewItem; - const toggleElement = toggleViewItem.element?.querySelector('[role="checkbox"]') as HTMLElement; - assert(toggleElement, 'Toggle element should exist'); - assert.strictEqual(toggleElement.getAttribute('aria-checked'), 'false', 'Initial checked state should be false'); + assert(toggleViewItem instanceof ToggleActionViewItem, 'Toggle view item should exist'); // Change the action's checked state toggleAction.checked = true; - assert.strictEqual(toggleElement.getAttribute('aria-checked'), 'true', 'Toggle should update when action checked changes'); + // The view item should reflect the updated checked state + assert.strictEqual(toggleAction.checked, true, 'Toggle action should update checked state'); }); test('quick input button with toggle property creates action with checked state', async function () { const { quickInputButtonToAction } = await import('../../../platform/quickinput/browser/quickInputUtils.js'); - + // Create a button with toggle property const toggleButton = { iconClass: 'test-icon', From 3568c99846311b6ed91834735d8a933820e28bc5 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 17 Dec 2025 10:59:16 -0800 Subject: [PATCH 08/11] light up checked state change for the list buttons & fix CSS --- .../quickinput/browser/media/quickInput.css | 8 ++++++-- .../platform/quickinput/browser/quickInputList.ts | 14 ++++++++++++-- .../browser/tree/quickInputTreeRenderer.ts | 7 ++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 8d733d69c3564..f97d707cc0b42 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -24,8 +24,8 @@ top: 0; } -.quick-input-widget .monaco-custom-toggle { - padding: 3px; +.quick-input-widget .monaco-action-bar .monaco-custom-toggle { + margin-left: 0; border-radius: 5px; box-sizing: content-box; } @@ -67,6 +67,10 @@ margin-left: 4px; } +.quick-input-inline-action-bar > .actions-container > .action-item { + margin-left: 4px; +} + .quick-input-titlebar .monaco-action-bar .action-label.codicon { background-position: center; background-repeat: no-repeat; diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index 734f9f3075b52..ace2fe0ebeb98 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -537,7 +537,12 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer quickInputButtonToAction( button, `id-${index}`, - () => element.fireButtonTriggered({ button, item: element.item }) + () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + element.fireButtonTriggered({ button, item: element.item }); + } )), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { @@ -634,7 +639,12 @@ class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer quickInputButtonToAction( button, `id-${index}`, - () => element.fireSeparatorButtonTriggered({ button, separator: element.separator }) + () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + element.fireSeparatorButtonTriggered({ button, separator: element.separator }); + } )), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { diff --git a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts index a4db0d9481c72..ff30bb3d10381 100644 --- a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts +++ b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts @@ -157,7 +157,12 @@ export class QuickInputTreeRenderer extends Disposable templateData.actionBar.push(buttons.map((button, index) => quickInputButtonToAction( button, `tree-${index}`, - () => this._buttonTriggeredEmitter.fire({ item: quickTreeItem, button }) + () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + this._buttonTriggeredEmitter.fire({ item: quickTreeItem, button }); + } )), { icon: true, label: false }); templateData.entry.classList.add('has-actions'); } else { From 4a81a9d14af2c6e2f026c28cfec7437694fcfac6 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 17 Dec 2025 13:52:40 -0800 Subject: [PATCH 09/11] encapsulate the checked change in the util --- .../platform/quickinput/browser/quickInput.ts | 60 ++++++++++--------- .../quickinput/browser/quickInputList.ts | 14 +---- .../quickinput/browser/quickInputUtils.ts | 11 +++- .../browser/tree/quickInputTreeRenderer.ts | 7 +-- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 8cb30e034bf3b..26f0d491ce2e5 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -303,11 +303,33 @@ export abstract class QuickInput extends Disposable implements IQuickInput { } set buttons(buttons: IQuickInputButton[]) { - // TODO: use a switch - this._leftButtons = buttons.filter(b => b === backButton); - this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline && b.location !== QuickInputButtonLocation.Input); - this._inlineButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Inline); - this._inputButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Input); + const leftButtons: IQuickInputButton[] = []; + const rightButtons: IQuickInputButton[] = []; + const inlineButtons: IQuickInputButton[] = []; + const inputButtons: IQuickInputButton[] = []; + + for (const button of buttons) { + if (button === backButton) { + leftButtons.push(button); + } else { + switch (button.location) { + case QuickInputButtonLocation.Inline: + inlineButtons.push(button); + break; + case QuickInputButtonLocation.Input: + inputButtons.push(button); + break; + default: + rightButtons.push(button); + break; + } + } + } + + this._leftButtons = leftButtons; + this._rightButtons = rightButtons; + this._inlineButtons = inlineButtons; + this._inputButtons = inputButtons; this.buttonsUpdated = true; this.update(); } @@ -442,12 +464,7 @@ export abstract class QuickInput extends Disposable implements IQuickInput { .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - this.onDidTriggerButtonEmitter.fire(button); - } + async () => this.onDidTriggerButtonEmitter.fire(button) )); this.ui.leftActionBar.push(leftButtons, { icon: true, label: false }); this.ui.rightActionBar.clear(); @@ -455,12 +472,7 @@ export abstract class QuickInput extends Disposable implements IQuickInput { .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - this.onDidTriggerButtonEmitter.fire(button); - } + async () => this.onDidTriggerButtonEmitter.fire(button) )); this.ui.rightActionBar.push(rightButtons, { icon: true, label: false }); this.ui.inlineActionBar.clear(); @@ -468,24 +480,14 @@ export abstract class QuickInput extends Disposable implements IQuickInput { .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - this.onDidTriggerButtonEmitter.fire(button); - } + async () => this.onDidTriggerButtonEmitter.fire(button) )); this.ui.inlineActionBar.push(inlineButtons, { icon: true, label: false }); this.ui.inputBox.actions = this._inputButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, - async () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - this.onDidTriggerButtonEmitter.fire(button); - } + async () => this.onDidTriggerButtonEmitter.fire(button) )); } if (this.togglesUpdated) { diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index ace2fe0ebeb98..734f9f3075b52 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -537,12 +537,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer quickInputButtonToAction( button, `id-${index}`, - () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - element.fireButtonTriggered({ button, item: element.item }); - } + () => element.fireButtonTriggered({ button, item: element.item }) )), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { @@ -639,12 +634,7 @@ class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer quickInputButtonToAction( button, `id-${index}`, - () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - element.fireSeparatorButtonTriggered({ button, separator: element.separator }); - } + () => element.fireSeparatorButtonTriggered({ button, separator: element.separator }) )), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { diff --git a/src/vs/platform/quickinput/browser/quickInputUtils.ts b/src/vs/platform/quickinput/browser/quickInputUtils.ts index 826f736df7d9f..12794f59b172e 100644 --- a/src/vs/platform/quickinput/browser/quickInputUtils.ts +++ b/src/vs/platform/quickinput/browser/quickInputUtils.ts @@ -75,6 +75,13 @@ export function quickInputButtonToAction(button: IQuickInputButton, id: string, cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible'; } + const handler = () => { + if (button.toggle) { + button.toggle.checked = !button.toggle.checked; + } + return run(); + }; + const action = button.toggle ? new QuickInputToggleButtonAction( id, @@ -83,7 +90,7 @@ export function quickInputButtonToAction(button: IQuickInputButton, id: string, cssClasses, true, button.toggle.checked, - run + handler ) : { id, @@ -91,7 +98,7 @@ export function quickInputButtonToAction(button: IQuickInputButton, id: string, tooltip: button.tooltip || '', class: cssClasses, enabled: true, - run, + run: handler, }; return action; diff --git a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts index ff30bb3d10381..a4db0d9481c72 100644 --- a/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts +++ b/src/vs/platform/quickinput/browser/tree/quickInputTreeRenderer.ts @@ -157,12 +157,7 @@ export class QuickInputTreeRenderer extends Disposable templateData.actionBar.push(buttons.map((button, index) => quickInputButtonToAction( button, `tree-${index}`, - () => { - if (button.toggle) { - button.toggle.checked = !button.toggle.checked; - } - this._buttonTriggeredEmitter.fire({ item: quickTreeItem, button }); - } + () => this._buttonTriggeredEmitter.fire({ item: quickTreeItem, button }) )), { icon: true, label: false }); templateData.entry.classList.add('has-actions'); } else { From 7a007e156665a985f520991ae8fae4d3cfcc9bc2 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 17 Dec 2025 13:55:41 -0800 Subject: [PATCH 10/11] another css fix --- src/vs/platform/quickinput/browser/media/quickInput.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index f97d707cc0b42..a6a820fb09d5c 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -329,6 +329,10 @@ padding: 2px; } +.quick-input-list .quick-input-list-entry-action-bar .monaco-custom-toggle.codicon { + margin-right: 4px; +} + .quick-input-list .quick-input-list-entry-action-bar { margin-top: 1px; } @@ -490,6 +494,10 @@ padding: 2px; } +.quick-input-list .quick-input-tree-entry-action-bar .monaco-custom-toggle.codicon { + margin-right: 4px; +} + .quick-input-tree .quick-input-tree-entry-action-bar { margin-top: 1px; } From 1f83a78650b34ec821c093bc993e9457502cbda2 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 17 Dec 2025 14:12:48 -0800 Subject: [PATCH 11/11] fixes --- src/vs/platform/quickinput/browser/media/quickInput.css | 2 +- src/vs/platform/quickinput/common/quickInput.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index a6a820fb09d5c..3ad607b410e91 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -494,7 +494,7 @@ padding: 2px; } -.quick-input-list .quick-input-tree-entry-action-bar .monaco-custom-toggle.codicon { +.quick-input-tree .quick-input-tree-entry-action-bar .monaco-custom-toggle.codicon { margin-right: 4px; } diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 9a387f3f23761..7ab9cd08644ef 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -833,10 +833,8 @@ export interface IQuickInputButton { location?: QuickInputButtonLocation; /** * When present, indicates that the button is a toggle button that can be checked or unchecked. - * - * @note This property is currently only applicable to buttons with {@link QuickInputButtonLocation.Input} location. - * It must be set for such buttons, and the state will be updated when the button is toggled. - * It cannot be set for buttons with other location values. + * The `checked` property indicates the current state of the toggle and will be updated + * when the button is clicked. */ readonly toggle?: { checked: boolean }; }