Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
90dc328
findWidget - Editable Match Location Field:
Apr 2, 2026
32eeec1
findWidget - Editable Match Location Field:
Apr 2, 2026
622de8f
findWidget - Iterating and Misc. Improvements
Apr 3, 2026
ceda74b
findWidget - More Iteration and Cleanup
Apr 4, 2026
7da8071
simpleFindWidget - Add Nth Match support to the terminal's finder wid…
Apr 6, 2026
f272897
simpleFindWidget - Nth Match Support:
Apr 7, 2026
119f812
findWidget - Nth Match Support:
Apr 7, 2026
d366cfc
findWidget / simpleFindWidget: Merging changes
boborrob Apr 8, 2026
9c2c341
findWidget / terminalFindWidget: Add inline "Find Nth" functionality …
boborrob Apr 8, 2026
11d593c
findWidget / terminalFindWidget: Add inline, "Find Nth" functionality…
boborrob Apr 8, 2026
e607e3e
11.2.0
boborrob Apr 8, 2026
ee61f36
findWidget / terminalFindWidget: Stress Testing
boborrob Apr 9, 2026
fd67d22
Merge branch 'main' into clean_branch-findWidget_terminalFindWidget
boborrob Apr 10, 2026
ee803a0
Merge branch 'main' into findWidget_terminalFindWidget-arbitrary_acce…
boborrob Apr 10, 2026
4449e9a
Merge branch 'main' into findWidget_terminalFindWidget-arbitrary_acce…
boborrob Apr 14, 2026
2ed6713
findWidget / terminalFindWidget: More alignment
boborrob Apr 14, 2026
8899bf0
findWidget / terminalFindWidget: Consolidate nthMatchInput styles and…
boborrob Apr 15, 2026
d5b4bb8
Merge branch 'main' into findWidget_terminalFindWidget-arbitrary_acce…
boborrob Apr 15, 2026
1e582d2
findWidget/terminalFindWidget: More enhancements
boborrob May 11, 2026
173ac8a
findWidget/terminalFindWidget: Last Match Button
boborrob May 13, 2026
44a944a
findWidget/terminalFindWidget: Code Review - 1st Iteration
boborrob May 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/vs/base/browser/ui/findinput/findContants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MATCHES_LIMIT = 19999;
34 changes: 34 additions & 0 deletions src/vs/base/browser/ui/findinput/nthMatchInput.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/


/* ---------- Nth Match Input - FindWidget ---------- */
.nth-match {
width: 45px;
min-width: 45px;
max-width: 60px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
bottom: 1.5px;
margin-right: 10px;
transition: width 300ms ease;
}

.nth-match.elongated {
width: 60px;
}

.nth-match input {
text-align: center;
overflow-x: hidden;
text-overflow: clip;
font-size: 12px;
}


/* ---------- Nth Match Input - SimpleFindWidget ---------- */
.nth-match.simple-nth-match {
bottom: unset;
}
225 changes: 225 additions & 0 deletions src/vs/base/browser/ui/findinput/nthMatchInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as dom from '../../dom.js';
import { IKeyboardEvent } from '../../keyboardEvent.js';
import { IMouseEvent } from '../../mouseEvent.js';
import { IContextViewProvider } from '../contextview/contextview.js';
import { InputBox, IInputBoxStyles, IMessage as InputBoxMessage } from '../inputbox/inputBox.js';
import { Widget } from '../widget.js';
import { Emitter, Event } from '../../../common/event.js';
import { KeyCode } from '../../../common/keyCodes.js';
import './nthMatchInput.css';
import * as nls from '../../../../nls.js';
import { MATCHES_LIMIT } from './findContants.js';

Comment thread
boborrob marked this conversation as resolved.
export interface INthMatchInputOptions {
readonly placeholder?: string;
readonly tooltip?: string;
readonly label: string;
readonly type: 'text';
readonly min?: number;
readonly max?: number;

readonly inputBoxStyles: IInputBoxStyles;
}

export interface IStepEvent {
to: 'previous' | 'next';
}

const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input");

export class NthMatchInput extends Widget {

private placeholder: string;
private tooltip: string;
private label: string;
private type: string;
private imeSessionInProgress = false;

public readonly domNode: HTMLElement;
public readonly inputBox: InputBox;
public min: number;
public max: number;

private readonly _onDidOptionChange = this._register(new Emitter<boolean>());
public readonly onDidOptionChange: Event<boolean /* via keyboard */> = this._onDidOptionChange.event;

private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;

private readonly _onMouseDown = this._register(new Emitter<IMouseEvent>());
public readonly onMouseDown: Event<IMouseEvent> = this._onMouseDown.event;

private readonly _onInput = this._register(new Emitter<void>());
public readonly onInput: Event<void> = this._onInput.event;

private readonly _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;

private readonly _onStep = this._register(new Emitter<IStepEvent>());
public readonly onStep: Event<IStepEvent> = this._onStep.event;


constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, options: INthMatchInputOptions) {
super();
this.placeholder = options.placeholder || '';
this.tooltip = options.tooltip || '';
this.label = options.label || NLS_DEFAULT_LABEL;
this.type = options.type || 'text';
this.min = options.min || 1;
this.max = options.max || MATCHES_LIMIT;

this.domNode = document.createElement('div');
this.domNode.classList.add('monaco-findInput');

this.inputBox = this._register(new InputBox(this.domNode, contextViewProvider, {
placeholder: this.placeholder || '',
tooltip: this.tooltip || '',
ariaLabel: this.label || '',
inputBoxStyles: options.inputBoxStyles,
type: this.type
}));

this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
// Arrow-Key support for stepping to the previous match or to the next one.
if (event.equals(KeyCode.UpArrow)) {
this._onStep.fire({ to: 'previous' });
}
else if (event.equals(KeyCode.DownArrow)) {
this._onStep.fire({ to: 'next' });
}
});

this.onchange(this.domNode, () => {
this.updateInputWrapperWidth();
});

parent?.appendChild(this.domNode);

this._register(dom.addDisposableListener(this.inputBox.inputElement, 'compositionstart', (e: CompositionEvent) => {
this.imeSessionInProgress = true;
}));
this._register(dom.addDisposableListener(this.inputBox.inputElement, 'compositionend', (e: CompositionEvent) => {
this.imeSessionInProgress = false;
this._onInput.fire();
}));

this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}

public get isImeSessionInProgress(): boolean {
return this.imeSessionInProgress;
}

public get onDidChange(): Event<string> {
return this.inputBox.onDidChange;
}

public layout(style: { collapsedFindWidget: boolean; narrowFindWidget: boolean; reducedFindWidget: boolean }) {
this.inputBox.layout();
this.updateInputBoxPadding(style.collapsedFindWidget);
this.updateInputWrapperWidth();
}

public updateInputWrapperWidth() {
const currentInputValue = `${this.getSanitizedCurrentValue()}`;
const containerElem = (this.inputBox.element.parentElement as HTMLElement);
if ((currentInputValue.length >= 5)) {
if (!containerElem.classList.contains('elongated')) {
containerElem.classList.add(...['elongated']);
}
}
else if (currentInputValue.length <= 4) {
if (containerElem.classList.contains('elongated')) {
containerElem.classList.remove(...['elongated']);
}
}
}

public enable(): void {
this.domNode.classList.remove('disabled');
this.inputBox.enable();
}

public disable(): void {
this.domNode.classList.add('disabled');
this.inputBox.disable();
}

public setEnabled(enabled: boolean): void {
if (enabled) {
this.enable();
} else {
this.disable();
}
}

private updateInputBoxPadding(controlsHidden = false) {
if (controlsHidden) {
this.inputBox.paddingRight = 0;
} else {
this.inputBox.paddingRight = 0;
}
}

public clear(): void {
this.clearValidation();
this.setValue('');
this.focus();
}

public getValue(): string {
return this.inputBox.value;
}

public setValue(value: string): void {
if (this.inputBox.value !== value) {
this.inputBox.value = value;
}
this.updateInputWrapperWidth();
}

public select(): void {
this.inputBox.select();
}

public focus(): void {
this.inputBox.focus();
}

public validate(): void {
this.inputBox.validate();
}

public showMessage(message: InputBoxMessage): void {
this.inputBox.showMessage(message);
}

public clearMessage(): void {
this.inputBox.hideMessage();
}

private clearValidation(): void {
this.inputBox.hideMessage();
}

public getSanitizedCurrentValue(): number {
if (!this || !this.getValue()) {
return this.min;
}

// Enforce the numerical input and min/max constraints here.
const currentValueAsInt = parseInt(this.getValue(), 10);
return isNaN(currentValueAsInt) ?
this.min : currentValueAsInt > this.max ?
this.max : currentValueAsInt < this.min ?
this.min : currentValueAsInt;
}
}
116 changes: 116 additions & 0 deletions src/vs/editor/contrib/find/browser/findController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { FindWidgetSearchHistory } from './findWidgetSearchHistory.js';
import { ReplaceWidgetHistory } from './replaceWidgetHistory.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
import { MATCHES_LIMIT } from '../../../../base/browser/ui/findinput/findContants.js';

const SEARCH_STRING_MAX_LENGTH = 524288;

Expand Down Expand Up @@ -932,6 +933,119 @@ export class MoveToMatchFindAction extends EditorAction {
}
}

export class MoveToNthMatchFindAction extends EditorAction {

protected _highlightDecorations: string[] = [];

constructor(id?: string, label?: string, alias?: string) {
super({
id: id || FIND_IDS.NthMatchFindAction,
label: label || nls.localize('findMatchAction.nthMatch', "Go to Nth Match..."),
alias: alias || 'Go to Nth Match...',
precondition: CONTEXT_FIND_WIDGET_VISIBLE
});
}

public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void> {
const controller = CommonFindController.get(editor);
if (!controller || !args?.n) {
return;
Comment thread
boborrob marked this conversation as resolved.
}

const matchesCount = controller.getState().matchesCount;
if (matchesCount < 1) {
const notificationService = accessor.get(INotificationService);
notificationService.notify({
severity: Severity.Warning,
message: nls.localize('findMatchAction.noResults', "No matches. Try searching for something else.")
});
return;
}

const toFindMatchIndex = (value: string): number | undefined => {
const index = parseInt(value);
if (isNaN(index)) {
return undefined;
}

const matchCount = controller.getState().matchesCount;
if (index > 0 && index <= matchCount) {
return index - 1; // zero based
} else if (index < 0 && index >= -matchCount) {
// Always clamp to the start if
// the index is out-of-bounds.
return 0;
}

return undefined;
};

const index = toFindMatchIndex((args?.n || '1'));
if (typeof index === 'number') {
// valid
controller.goToMatch(index);
const currentMatch = controller.getState().currentMatch;
if (currentMatch) {
this.addDecorations(editor, currentMatch);
}
else {
this.clearDecorations(editor);
}
}
else {
this.clearDecorations(editor);
}
}

private clearDecorations(editor: ICodeEditor): void {
editor.changeDecorations(changeAccessor => {
this._highlightDecorations = changeAccessor.deltaDecorations(this._highlightDecorations, []);
});
}

private addDecorations(editor: ICodeEditor, range: IRange): void {
editor.changeDecorations(changeAccessor => {
this._highlightDecorations = changeAccessor.deltaDecorations(this._highlightDecorations, [
{
range,
options: {
description: 'find-match-quick-access-range-highlight',
className: 'rangeHighlight',
isWholeLine: true
}
},
{
range,
options: {
description: 'find-match-quick-access-range-highlight-overview',
overviewRuler: {
color: themeColorFromId(overviewRulerRangeHighlight),
position: OverviewRulerLane.Full
}
}
}
]);
});
}
}

export class MoveToLastMatchFindAction extends MoveToNthMatchFindAction {
protected override _highlightDecorations: string[] = [];

constructor() {
super(
FIND_IDS.LastMatchFindAction,
nls.localize('findMatchAction.goToLastMatchFindAction', "Go to Last Match..."),
'Go to Last Match...'
);
}

public override run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void> {
const lastMatchIndex = CommonFindController.get(editor)?.getState().matchesCount || MATCHES_LIMIT;
super.run(accessor, editor, { ...(args || {}), n: lastMatchIndex });
}
}

export abstract class SelectionMatchFindAction extends EditorAction {
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
const controller = CommonFindController.get(editor);
Expand Down Expand Up @@ -1063,6 +1177,8 @@ registerEditorContribution(CommonFindController.ID, FindController, EditorContri
registerEditorAction(StartFindWithArgsAction);
registerEditorAction(StartFindWithSelectionAction);
registerEditorAction(MoveToMatchFindAction);
registerEditorAction(MoveToNthMatchFindAction);
registerEditorAction(MoveToLastMatchFindAction);
registerEditorAction(NextSelectionMatchFindAction);
registerEditorAction(PreviousSelectionMatchFindAction);

Expand Down
Loading