Skip to content

Commit

Permalink
Append hint text to placeholder if input supports history (#129324)
Browse files Browse the repository at this point in the history
* Append hint text to placeholder if input supports history

* Avoid adding history hint to tooltip

* only add history hint when focused and history exists

* don't break showPlaceholderOnFocus

* remove parens from includes/excludes placeholders

* make `Clear Search History` clear Replace field history too

* terser indication of field history

* update placeholder history hint sooner when clearing field

* revise hint text after feedback

* fix typo in comment

* suppress hint suffix if history keybindings customized

* fix bad merge
  • Loading branch information
gjsjohnmurray authored Sep 17, 2021
1 parent dcb7d85 commit bff1cf8
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 18 deletions.
2 changes: 2 additions & 0 deletions src/vs/base/browser/ui/findinput/findInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface IFindInputOptions extends IFindInputStyles {
readonly appendWholeWordsLabel?: string;
readonly appendRegexLabel?: string;
readonly history?: string[];
readonly showHistoryHint?: () => boolean;
}

export interface IFindInputStyles extends IInputBoxStyles {
Expand Down Expand Up @@ -150,6 +151,7 @@ export class FindInput extends Widget {
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history,
showHistoryHint: options.showHistoryHint,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight
Expand Down
2 changes: 2 additions & 0 deletions src/vs/base/browser/ui/findinput/replaceInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface IReplaceInputOptions extends IReplaceInputStyles {

readonly appendPreserveCaseLabel?: string;
readonly history?: string[];
readonly showHistoryHint?: () => boolean;
}

export interface IReplaceInputStyles extends IInputBoxStyles {
Expand Down Expand Up @@ -157,6 +158,7 @@ export class ReplaceInput extends Widget {
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history,
showHistoryHint: options.showHistoryHint,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight
Expand Down
64 changes: 62 additions & 2 deletions src/vs/base/browser/ui/inputbox/inputBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ const defaultOpts = {
export class InputBox extends Widget {
private contextViewProvider?: IContextViewProvider;
element: HTMLElement;
private input: HTMLInputElement;
protected input: HTMLInputElement;
private actionbar?: ActionBar;
private options: IInputOptions;
private message: IMessage | null;
private placeholder: string;
protected placeholder: string;
private tooltip: string;
private ariaLabel: string;
private validation?: IInputValidator;
Expand Down Expand Up @@ -634,15 +634,75 @@ export class InputBox extends Widget {

export interface IHistoryInputOptions extends IInputOptions {
history: string[];
readonly showHistoryHint?: () => boolean;
}

export class HistoryInputBox extends InputBox implements IHistoryNavigationWidget {

private readonly history: HistoryNavigator<string>;
private observer: MutationObserver | undefined;

constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IHistoryInputOptions) {
const NLS_PLACEHOLDER_HISTORY_HINT = nls.localize({ key: 'history.inputbox.hint', comment: ['Text will be prefixed with \u21C5 plus a single space, then used as a hint where input field keeps history'] }, "for history");
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX = ` or \u21C5 ${NLS_PLACEHOLDER_HISTORY_HINT}`;
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS = ` (\u21C5 ${NLS_PLACEHOLDER_HISTORY_HINT})`;
super(container, contextViewProvider, options);
this.history = new HistoryNavigator<string>(options.history, 100);

// Function to append the history suffix to the placeholder if necessary
const addSuffix = () => {
if (options.showHistoryHint && options.showHistoryHint() && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX) && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS) && this.history.getHistory().length) {
const suffix = this.placeholder.endsWith(')') ? NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX : NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS;
const suffixedPlaceholder = this.placeholder + suffix;
if (options.showPlaceholderOnFocus && document.activeElement !== this.input) {
this.placeholder = suffixedPlaceholder;
}
else {
this.setPlaceHolder(suffixedPlaceholder);
}
}
};

// Spot the change to the textarea class attribute which occurs when it changes between non-empty and empty,
// and add the history suffix to the placeholder if not yet present
this.observer = new MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
mutationList.forEach((mutation: MutationRecord) => {
if (!mutation.target.textContent) {
addSuffix();
}
});
});
this.observer.observe(this.input, { attributeFilter: ['class'] });

this.onfocus(this.input, () => addSuffix());
this.onblur(this.input, () => {
const resetPlaceholder = (historyHint: string) => {
if (!this.placeholder.endsWith(historyHint)) {
return false;
}
else {
const revertedPlaceholder = this.placeholder.slice(0, this.placeholder.length - historyHint.length);
if (options.showPlaceholderOnFocus) {
this.placeholder = revertedPlaceholder;
}
else {
this.setPlaceHolder(revertedPlaceholder);
}
return true;
}
};
if (!resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS)) {
resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX);
}
});
}

override dispose() {
super.dispose();
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}

public addToHistory(): void {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/contrib/debug/browser/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
return this.instantiationService.createInstance(SelectReplActionViewItem, action, session);
} else if (action.id === FILTER_ACTION_ID) {
const filterHistory = JSON.parse(this.storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[];
const showHistoryHint = () => { return this.keybindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && this.keybindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; };
this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action,
localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude)"), this.filterState, filterHistory);
localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude)"), this.filterState, filterHistory, showHistoryHint);
return this.filterActionViewItem;
}

Expand Down
4 changes: 3 additions & 1 deletion src/vs/workbench/contrib/debug/browser/replFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class ReplFilterActionViewItem extends BaseActionViewItem {
private placeholder: string,
private filters: ReplFilterState,
private history: string[],
private showHistoryHint: () => boolean,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IContextViewService private readonly contextViewService: IContextViewService) {
Expand Down Expand Up @@ -188,7 +189,8 @@ export class ReplFilterActionViewItem extends BaseActionViewItem {
private createInput(container: HTMLElement): void {
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: this.placeholder,
history: this.history
history: this.history,
showHistoryHint: this.showHistoryHint
}));
this._register(attachInputBoxStyler(this.filterInputBox, this.themeService));
this.filterInputBox.value = this.filters.filterText;
Expand Down
10 changes: 8 additions & 2 deletions src/vs/workbench/contrib/markers/browser/markersViewActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { BaseActionViewItem, ActionViewItem } from 'vs/base/browser/ui/actionbar
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';

export interface IMarkersFiltersChangeEvent {
filterText?: boolean;
Expand Down Expand Up @@ -245,16 +246,19 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem {
private focusContextKey: IContextKey<boolean>;
private readonly filtersAction: IAction;
private actionbar: ActionBar | null = null;
private keybindingService;

constructor(
action: IAction,
private markersView: IMarkersView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService
) {
super(null, action);
this.keybindingService = keybindingService;
this.focusContextKey = Constants.MarkerViewFilterFocusContextKey.bindTo(contextKeyService);
this.delayedFilterUpdate = new Delayer<void>(400);
this._register(toDisposable(() => this.delayedFilterUpdate.cancel()));
Expand Down Expand Up @@ -316,10 +320,12 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem {
}

private createInput(container: HTMLElement): void {
const showHistoryHint = () => { return this.keybindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && this.keybindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; };
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER,
ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL,
history: this.markersView.filters.filterHistory
history: this.markersView.filters.filterHistory,
showHistoryHint
}));
this._register(attachInputBoxStyler(this.filterInputBox, this.themeService));
this.filterInputBox.value = this.markersView.filters.filterText;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export class KeybindingsSearchWidget extends SearchWidget {

constructor(parent: HTMLElement, options: KeybindingsSearchOptions,
@IContextViewService contextViewService: IContextViewService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService,
) {
super(parent, options, contextViewService, instantiationService, themeService, contextKeyService);
super(parent, options, contextViewService, instantiationService, themeService, contextKeyService, keybindingService);
this._register(attachInputBoxStyler(this.inputBox, themeService));
this._register(toDisposable(() => this.stopRecordingKeys()));
this._firstPart = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILabelService } from 'vs/platform/label/common/label';
import { activeContrastBorder, badgeBackground, badgeForeground, contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
Expand Down Expand Up @@ -370,7 +371,8 @@ export class SearchWidget extends Widget {
@IContextViewService private readonly contextViewService: IContextViewService,
@IInstantiationService protected instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IKeybindingService protected readonly keybindingService: IKeybindingService
) {
super();
this.create(parent);
Expand Down Expand Up @@ -420,7 +422,8 @@ export class SearchWidget extends Widget {
}

protected createInputBox(parent: HTMLElement): HistoryInputBox {
const box = this._register(new ContextScopedHistoryInputBox(parent, this.contextViewService, this.options, this.contextKeyService));
const showHistoryHint = () => { return this.keybindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && this.keybindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; };
const box = this._register(new ContextScopedHistoryInputBox(parent, this.contextViewService, { ...this.options, showHistoryHint }, this.contextKeyService));
this._register(attachInputBoxStyler(box, this.themeService));

return box;
Expand Down
14 changes: 10 additions & 4 deletions src/vs/workbench/contrib/search/browser/patternInputWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as nls from 'vs/nls';
import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { attachCheckboxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';

Expand Down Expand Up @@ -49,7 +50,8 @@ export class PatternInputWidget extends Widget implements IThemable {
constructor(parent: HTMLElement, private contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null),
@IThemeService protected themeService: IThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService protected readonly configurationService: IConfigurationService
@IConfigurationService protected readonly configurationService: IConfigurationService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
) {
super();
options = {
Expand Down Expand Up @@ -143,6 +145,7 @@ export class PatternInputWidget extends Widget implements IThemable {
this.domNode.style.width = this.width + 'px';
this.domNode.classList.add('monaco-findInput');

const showHistoryHint = () => { return this.keybindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && this.keybindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; };
this.inputBox = new ContextScopedHistoryInputBox(this.domNode, this.contextViewProvider, {
placeholder: options.placeholder,
showPlaceholderOnFocus: options.showPlaceholderOnFocus,
Expand All @@ -151,7 +154,8 @@ export class PatternInputWidget extends Widget implements IThemable {
validationOptions: {
validation: undefined
},
history: options.history || []
history: options.history || [],
showHistoryHint
}, this.contextKeyService);
this._register(attachInputBoxStyler(this.inputBox, this.themeService));
this._register(this.inputBox.onDidChange(() => this._onSubmit.fire(true)));
Expand Down Expand Up @@ -192,8 +196,9 @@ export class IncludePatternInputWidget extends PatternInputWidget {
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService,
) {
super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService);
super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService);
}

private useSearchInEditorsBox!: Checkbox;
Expand Down Expand Up @@ -243,8 +248,9 @@ export class ExcludePatternInputWidget extends PatternInputWidget {
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService,
) {
super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService);
super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService);
}

private useExcludesAndIgnoreFilesBox!: Checkbox;
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/search/browser/searchView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ export class SearchView extends ViewPane {

this.inputPatternIncludes = this._register(this.instantiationService.createInstance(IncludePatternInputWidget, folderIncludesList, this.contextViewService, {
ariaLabel: filesToIncludeTitle,
placeholder: nls.localize('placeholder.includes', "(e.g. *.ts, src/**/include)"),
placeholder: nls.localize('placeholder.includes', "e.g. *.ts, src/**/include"),
showPlaceholderOnFocus: true,
history: patternIncludesHistory,
}));
Expand All @@ -339,7 +339,7 @@ export class SearchView extends ViewPane {
dom.append(excludesList, $('h4', undefined, excludesTitle));
this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, {
ariaLabel: excludesTitle,
placeholder: nls.localize('placeholder.excludes', "(e.g. *.ts, src/**/exclude)"),
placeholder: nls.localize('placeholder.excludes', "e.g. *.ts, src/**/exclude"),
showPlaceholderOnFocus: true,
history: patternExclusionsHistory,
}));
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/search/browser/searchWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export class SearchWidget extends Widget {

clearHistory(): void {
this.searchInput.inputBox.clearHistory();
this.replaceInput.inputBox.clearHistory();
}

showNextSearchTerm() {
Expand Down Expand Up @@ -302,6 +303,7 @@ export class SearchWidget extends Widget {
}

private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
const showHistoryHint = () => { return this.keyBindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && this.keyBindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; };
const inputOptions: IFindInputOptions = {
label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search'),
validation: (value: string) => this.validateSearchInput(value),
Expand All @@ -310,6 +312,7 @@ export class SearchWidget extends Widget {
appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService),
appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService),
history: options.searchHistory,
showHistoryHint,
flexibleHeight: true,
flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT
};
Expand Down Expand Up @@ -392,12 +395,14 @@ export class SearchWidget extends Widget {
private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
const replaceBox = dom.append(this.replaceContainer, dom.$('.replace-input'));
const showHistoryHint = () => { return this.keyBindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && this.keyBindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; };

this.replaceInput = this._register(new ContextScopedReplaceInput(replaceBox, this.contextViewService, {
label: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview'),
placeholder: nls.localize('search.replace.placeHolder', "Replace"),
appendPreserveCaseLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.TogglePreserveCaseId), this.keyBindingService),
history: options.replaceHistory,
showHistoryHint,
flexibleHeight: true,
flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT
}, this.contextKeyService, true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class TestingExplorerFilter extends BaseActionViewItem {
value: this.state.text.value,
placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"),
},
history: this.history.get([]),
history: this.history.get([])
}));
this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService));

Expand Down

0 comments on commit bff1cf8

Please sign in to comment.