Skip to content
12 changes: 11 additions & 1 deletion src/vs/base/browser/ui/findinput/findInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +36,8 @@ export interface IFindInputOptions {
readonly appendWholeWordsLabel?: string;
readonly appendRegexLabel?: string;
readonly additionalToggles?: Toggle[];
readonly actions?: ReadonlyArray<IAction>;
readonly actionViewItemProvider?: IActionViewItemProvider;
readonly showHistoryHint?: () => boolean;
readonly toggleStyles: IToggleStyles;
readonly inputBoxStyles: IInputBoxStyles;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -307,6 +313,10 @@ export class FindInput extends Widget {
this.updateInputBoxPadding();
}

public setActions(actions: ReadonlyArray<IAction> | undefined, actionViewItemProvider?: IActionViewItemProvider): void {
this.inputBox.setActions(actions, actionViewItemProvider);
}

private updateInputBoxPadding(controlsHidden = false) {
if (controlsHidden) {
this.inputBox.paddingRight = 0;
Expand Down
21 changes: 19 additions & 2 deletions src/vs/base/browser/ui/inputbox/inputBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,6 +37,7 @@ export interface IInputOptions {
readonly flexibleWidth?: boolean;
readonly flexibleMaxHeight?: number;
readonly actions?: ReadonlyArray<IAction>;
readonly actionViewItemProvider?: IActionViewItemProvider;
readonly inputBoxStyles: IInputBoxStyles;
readonly history?: IHistory<string>;
}
Expand Down Expand Up @@ -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<IAction> | 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) {
Expand Down
19 changes: 19 additions & 0 deletions src/vs/base/browser/ui/toggle/toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -496,3 +497,21 @@ 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;
};
}
108 changes: 108 additions & 0 deletions src/vs/base/test/browser/actionbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand Down Expand Up @@ -60,4 +62,110 @@ 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 view item was created
const toggleViewItem = actionbar.viewItems[0] as ToggleActionViewItem;
assert(toggleViewItem instanceof ToggleActionViewItem, 'Toggle view item should exist');

// Change the action's checked state
toggleAction.checked = true;
// 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',
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');
});
});
});
37 changes: 16 additions & 21 deletions src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = [{
Expand Down Expand Up @@ -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()));
Expand Down
Loading
Loading