Skip to content

Commit

Permalink
Merge pull request #158616 from microsoft/tyriar/156179-quickinput-find
Browse files Browse the repository at this point in the history
Support toggles in the quick pick, adopt in terminal run recent command
  • Loading branch information
Tyriar committed Aug 22, 2022
2 parents 91146b5 + 2ba72b5 commit 9b507bc
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 73 deletions.
61 changes: 36 additions & 25 deletions src/vs/base/browser/ui/findinput/findInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import 'vs/css!./findInput';
import * as nls from 'vs/nls';
import { DisposableStore } from 'vs/base/common/lifecycle';


export interface IFindInputOptions extends IFindInputStyles {
Expand Down Expand Up @@ -47,12 +48,12 @@ export class FindInput extends Widget {

static readonly OPTION_CHANGE: string = 'optionChange';

private contextViewProvider: IContextViewProvider;
private placeholder: string;
private validation?: IInputValidator;
private label: string;
private fixFocusOnOptionClickEnabled = true;
private imeSessionInProgress = false;
private additionalTogglesDisposables: DisposableStore = new DisposableStore();

protected inputActiveOptionBorder?: Color;
protected inputActiveOptionForeground?: Color;
Expand Down Expand Up @@ -100,9 +101,8 @@ export class FindInput extends Widget {
private _onRegexKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onRegexKeyDown: Event<IKeyboardEvent> = this._onRegexKeyDown.event;

constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider, private readonly _showOptionButtons: boolean, options: IFindInputOptions) {
constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, private readonly _showOptionButtons: boolean, options: IFindInputOptions) {
super();
this.contextViewProvider = contextViewProvider;
this.placeholder = options.placeholder || '';
this.validation = options.validation;
this.label = options.label || NLS_DEFAULT_LABEL;
Expand Down Expand Up @@ -135,7 +135,7 @@ export class FindInput extends Widget {
this.domNode = document.createElement('div');
this.domNode.classList.add('monaco-findInput');

this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
this.inputBox = this._register(new HistoryInputBox(this.domNode, contextViewProvider, {
placeholder: this.placeholder || '',
ariaLabel: this.label || '',
validationOptions: {
Expand Down Expand Up @@ -254,27 +254,7 @@ export class FindInput extends Widget {
this.regex.domNode.style.display = 'none';
}

for (const toggle of options?.additionalToggles ?? []) {
this._register(toggle);
this.controls.appendChild(toggle.domNode);

this._register(toggle.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
}));

this.additionalToggles.push(toggle);
}

if (this.additionalToggles.length > 0) {
this.controls.style.display = 'block';
}

this.inputBox.paddingRight =
(this._showOptionButtons ? this.caseSensitive.width() + this.wholeWords.width() + this.regex.width() : 0)
+ this.additionalToggles.reduce((r, t) => r + t.width(), 0);
this.setAdditionalToggles(options?.additionalToggles);

this.domNode.appendChild(this.controls);

Expand Down Expand Up @@ -338,6 +318,37 @@ export class FindInput extends Widget {
}
}

public setAdditionalToggles(toggles: Toggle[] | undefined): void {
for (const currentToggle of this.additionalToggles) {
currentToggle.domNode.remove();
}
this.additionalToggles = [];
this.additionalTogglesDisposables.dispose();
this.additionalTogglesDisposables = new DisposableStore();

for (const toggle of toggles ?? []) {
this.additionalTogglesDisposables.add(toggle);
this.controls.appendChild(toggle.domNode);

this.additionalTogglesDisposables.add(toggle.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
}));

this.additionalToggles.push(toggle);
}

if (this.additionalToggles.length > 0) {
this.controls.style.display = 'block';
}

this.inputBox.paddingRight =
(this._showOptionButtons ? this.caseSensitive.width() + this.wholeWords.width() + this.regex.width() : 0)
+ this.additionalToggles.reduce((r, t) => r + t.width(), 0);
}

public clear(): void {
this.clearValidation();
this.setValue('');
Expand Down
11 changes: 10 additions & 1 deletion src/vs/base/parts/quickinput/browser/quickInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybi
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget';
import { IProgressBarStyles, ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
import { Action } from 'vs/base/common/actions';
import { equals } from 'vs/base/common/arrays';
import { TimeoutTimer } from 'vs/base/common/async';
Expand All @@ -28,7 +29,7 @@ import { isIOS } from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import { isString, withNullAsUndefined } from 'vs/base/common/types';
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickWillAcceptEvent, ItemActivation, NO_KEY_MODS, QuickInputHideReason, QuickPickInput } from 'vs/base/parts/quickinput/common/quickInput';
import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickWillAcceptEvent, ItemActivation, NO_KEY_MODS, QuickInputHideReason, QuickPickInput } from 'vs/base/parts/quickinput/common/quickInput';
import 'vs/css!./media/quickInput';
import { localize } from 'vs/nls';
import { QuickInputBox } from './quickInputBox';
Expand Down Expand Up @@ -740,6 +741,14 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.update();
}

set toggles(toggles: IQuickInputToggle[] | undefined) {
// HACK: Filter out toggles here that are not concrete Toggle objects. This is to workaround
// a layering issue as quick input's interface is in common but Toggle is in browser and
// it requires a HTMLElement on its interface
const concreteToggles = toggles?.filter(opts => opts instanceof Toggle) as Toggle[];
this.ui.inputBox.toggles = concreteToggles;
}

onDidChangeSelection = this.onDidChangeSelectionEmitter.event;

onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event;
Expand Down
60 changes: 33 additions & 27 deletions src/vs/base/parts/quickinput/browser/quickInputBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import * as dom from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IInputBoxStyles, InputBox, IRange, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
import { IInputBoxStyles, IRange, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import Severity from 'vs/base/common/severity';
import 'vs/css!./media/quickInput';
Expand All @@ -16,113 +18,117 @@ const $ = dom.$;
export class QuickInputBox extends Disposable {

private container: HTMLElement;
private inputBox: InputBox;
private findInput: FindInput;

constructor(
private parent: HTMLElement
) {
super();
this.container = dom.append(this.parent, $('.quick-input-box'));
this.inputBox = this._register(new InputBox(this.container, undefined));
this.findInput = this._register(new FindInput(this.container, undefined, false, { label: '' }));
}

onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
return dom.addDisposableListener(this.findInput.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
handler(new StandardKeyboardEvent(e));
});
};

onMouseDown = (handler: (event: StandardMouseEvent) => void): IDisposable => {
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
return dom.addDisposableListener(this.findInput.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
handler(new StandardMouseEvent(e));
});
};

onDidChange = (handler: (event: string) => void): IDisposable => {
return this.inputBox.onDidChange(handler);
return this.findInput.onDidChange(handler);
};

get value() {
return this.inputBox.value;
return this.findInput.getValue();
}

set value(value: string) {
this.inputBox.value = value;
this.findInput.setValue(value);
}

select(range: IRange | null = null): void {
this.inputBox.select(range);
this.findInput.inputBox.select(range);
}

isSelectionAtEnd(): boolean {
return this.inputBox.isSelectionAtEnd();
return this.findInput.inputBox.isSelectionAtEnd();
}

setPlaceholder(placeholder: string): void {
this.inputBox.setPlaceHolder(placeholder);
this.findInput.inputBox.setPlaceHolder(placeholder);
}

get placeholder() {
return this.inputBox.inputElement.getAttribute('placeholder') || '';
return this.findInput.inputBox.inputElement.getAttribute('placeholder') || '';
}

set placeholder(placeholder: string) {
this.inputBox.setPlaceHolder(placeholder);
this.findInput.inputBox.setPlaceHolder(placeholder);
}

get ariaLabel() {
return this.inputBox.getAriaLabel();
return this.findInput.inputBox.getAriaLabel();
}

set ariaLabel(ariaLabel: string) {
this.inputBox.setAriaLabel(ariaLabel);
this.findInput.inputBox.setAriaLabel(ariaLabel);
}

get password() {
return this.inputBox.inputElement.type === 'password';
return this.findInput.inputBox.inputElement.type === 'password';
}

set password(password: boolean) {
this.inputBox.inputElement.type = password ? 'password' : 'text';
this.findInput.inputBox.inputElement.type = password ? 'password' : 'text';
}

set enabled(enabled: boolean) {
this.inputBox.setEnabled(enabled);
this.findInput.setEnabled(enabled);
}

set toggles(toggles: Toggle[] | undefined) {
this.findInput.setAdditionalToggles(toggles);
}

hasFocus(): boolean {
return this.inputBox.hasFocus();
return this.findInput.inputBox.hasFocus();
}

setAttribute(name: string, value: string): void {
this.inputBox.inputElement.setAttribute(name, value);
this.findInput.inputBox.inputElement.setAttribute(name, value);
}

removeAttribute(name: string): void {
this.inputBox.inputElement.removeAttribute(name);
this.findInput.inputBox.inputElement.removeAttribute(name);
}

showDecoration(decoration: Severity): void {
if (decoration === Severity.Ignore) {
this.inputBox.hideMessage();
this.findInput.clearMessage();
} else {
this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
this.findInput.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
}
}

stylesForType(decoration: Severity) {
return this.inputBox.stylesForType(decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR);
return this.findInput.inputBox.stylesForType(decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR);
}

setFocus(): void {
this.inputBox.focus();
this.findInput.focus();
}

layout(): void {
this.inputBox.layout();
this.findInput.inputBox.layout();
}

style(styles: IInputBoxStyles): void {
this.inputBox.style(styles);
this.findInput.style(styles);
}
}
9 changes: 9 additions & 0 deletions src/vs/base/parts/quickinput/common/quickInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,15 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
hideInput: boolean;

hideCheckAll: boolean;

/**
* A set of `Toggle` objects to add to the input box.
*/
toggles: IQuickInputToggle[] | undefined;
}

export interface IQuickInputToggle {
onChange: Event<boolean /* via keyboard */>;
}

export interface IInputBox extends IQuickInput {
Expand Down
14 changes: 9 additions & 5 deletions src/vs/platform/quickinput/browser/quickPickPin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,22 @@ const buttonClasses = [pinButtonClass, pinnedButtonClass];
* Shows the quickpick once formatted.
*/
export async function showWithPinnedItems(storageService: IStorageService, storageKey: string, quickPick: IQuickPick<IQuickPickItem>, filterDuplicates?: boolean): Promise<void> {
const itemsWithoutPinned = quickPick.items;
let itemsWithPinned = _formatPinnedItems(storageKey, quickPick, storageService, undefined, filterDuplicates);
quickPick.onDidTriggerItemButton(async buttonEvent => {
const expectedButton = buttonEvent.button.iconClass && buttonClasses.includes(buttonEvent.button.iconClass);
if (expectedButton) {
quickPick.items = await _formatPinnedItems(storageKey, quickPick, storageService, buttonEvent.item, filterDuplicates);
quickPick.items = itemsWithoutPinned;
itemsWithPinned = _formatPinnedItems(storageKey, quickPick, storageService, buttonEvent.item, filterDuplicates);
quickPick.items = quickPick.value ? itemsWithoutPinned : itemsWithPinned;
}
});
quickPick.onDidChangeValue(async value => {
// don't show pinned items in the search results
quickPick.items = value ? quickPick.items.filter(i => i.type !== 'separator' && !i.buttons?.find(b => b.iconClass === pinnedButtonClass)) : quickPick.items;
// Return pinned items if there is no search value
quickPick.items = value ? itemsWithoutPinned : itemsWithPinned;
});
quickPick.items = await _formatPinnedItems(storageKey, quickPick, storageService, undefined, filterDuplicates);
await quickPick.show();
quickPick.items = quickPick.value ? itemsWithoutPinned : itemsWithPinned;
quickPick.show();
}

function _formatPinnedItems(storageKey: string, quickPick: IQuickPick<IQuickPickItem>, storageService: IStorageService, changedItem?: IQuickPickItem, filterDuplicates?: boolean): QuickPickItem[] {
Expand Down
29 changes: 14 additions & 15 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/commo
import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
import { IProcessDataEvent, IProcessPropertyMap, IReconnectionProperties, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, PosixShellType, ProcessPropertyType, ShellIntegrationStatus, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { escapeNonWindowsPath, collapseTildePath } from 'vs/platform/terminal/common/terminalEnvironment';
import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { activeContrastBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust';
Expand Down Expand Up @@ -90,6 +90,7 @@ import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks';
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
import { showWithPinnedItems } from 'vs/platform/quickinput/browser/quickPickPin';
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';

const enum Constants {
/**
Expand Down Expand Up @@ -1010,27 +1011,25 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
if (items.length === 0) {
return;
}
const fuzzySearchToggle = new Toggle({
title: 'Fuzzy search',
icon: Codicon.searchFuzzy,
isChecked: filterMode === 'fuzzy',
inputActiveOptionBorder: this._themeService.getColorTheme().getColor(inputActiveOptionBorder),
inputActiveOptionForeground: this._themeService.getColorTheme().getColor(inputActiveOptionForeground),
inputActiveOptionBackground: this._themeService.getColorTheme().getColor(inputActiveOptionBackground)
});
fuzzySearchToggle.onChange(() => {
this.runRecent(type, fuzzySearchToggle.checked ? 'fuzzy' : 'contiguous', quickPick.value);
});
const outputProvider = this._instantiationService.createInstance(TerminalOutputProvider);
const quickPick = this._quickInputService.createQuickPick<IQuickPickItem & { rawLabel: string }>();
const originalItems = items;
quickPick.items = [...originalItems];
quickPick.sortByLabel = false;
quickPick.placeholder = placeholder;
quickPick.customButton = true;
quickPick.matchOnLabelMode = filterMode || 'contiguous';
if (filterMode === 'fuzzy') {
quickPick.customLabel = nls.localize('terminal.contiguousSearch', 'Use Contiguous Search');
quickPick.onDidCustom(() => {
quickPick.hide();
this.runRecent(type, 'contiguous', quickPick.value);
});
} else {
quickPick.customLabel = nls.localize('terminal.fuzzySearch', 'Use Fuzzy Search');
quickPick.onDidCustom(() => {
quickPick.hide();
this.runRecent(type, 'fuzzy', quickPick.value);
});
}
quickPick.toggles = [fuzzySearchToggle];
quickPick.onDidTriggerItemButton(async e => {
if (e.button === removeFromCommandHistoryButton) {
if (type === 'command') {
Expand Down

0 comments on commit 9b507bc

Please sign in to comment.