Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ public IContentProposal[] computeReplaceProposals() {
addBracketProposal("\\c", 2, RegExMessages.getString("displayString_bs_c"), RegExMessages.getString("additionalInfo_bs_c")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
addBsProposal("\\C", RegExMessages.getString("displayString_replace_bs_C"), RegExMessages.getString("additionalInfo_replace_bs_C")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
addBracketProposal("#{1,1,3}", 8, RegExMessages.getString("displayString_counter"), RegExMessages.getString("additionalInfo_counter")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
fPriorityProposals.addAll(fProposals);
return fPriorityProposals.toArray(new IContentProposal[fPriorityProposals.size()]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,4 +540,16 @@ Find: "foo" Replace: "my\\Cbar\\CFar"\n\
"FOO" will be replaced by "myBARFAR".\n\
"Foo" will be replaced by "myBarFar".\n\n\
Note that the content of a group ($i, \\i) is currently inserted unmodified.

displayString_counter=#{start,step,pad} - Auto-incrementing counter
additionalInfo_counter=#{start,step,pad} - Inserts an auto-incrementing counter.\n\n\
Syntax: #{start,step,pad}\n\
start \u2014 initial value (may be negative)\n\
step \u2014 increment per replacement (negative for descending)\n\
pad \u2014 minimum digit width, zero-padded (0 = no padding)\n\n\
Examples:\n\
#{1,1,3} \u2192 001, 002, 003, ...\n\
#{10,5,0} \u2192 10, 15, 20, ...\n\
#{100,-1,0} \u2192 100, 99, 98, ...\n\n\
Can be combined with text and regex groups:\n\
item_#{1,1,2} \u2192 item_01, item_02, ...\n\
$1_#{1,1,2} \u2192 Match1_01, Match1_02, ...
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class FindReplaceLogic implements IFindReplaceLogic {

private String findString = ""; //$NON-NLS-1$
private String replaceString = ""; //$NON-NLS-1$
private ReplaceCounter replaceCounter;

@Override
public void setFindString(String findString) {
Expand All @@ -69,6 +70,22 @@ public void setFindString(String findString) {
@Override
public void setReplaceString(String replaceString) {
this.replaceString = Objects.requireNonNull(replaceString);
this.replaceCounter = ReplaceCounter.parse(replaceString);
}

/**
* Returns the effective replace string for the current replacement. If the
* replace string contains a counter placeholder ({@code #{start,step,pad}}),
* the placeholder is replaced by the next counter value. Otherwise the raw
* replace string is returned unchanged.
*
* @return the replace string with counter placeholder expanded (if any)
*/
private String buildReplaceString() {
if (replaceCounter != null) {
return replaceCounter.expand(replaceString);
}
return replaceString;
}

@Override
Expand Down Expand Up @@ -367,6 +384,9 @@ private int replaceAll() {
}

List<Point> replacements = new ArrayList<>();
if (replaceCounter != null) {
replaceCounter.reset();
}
executeInForwardMode(() -> {
executeWithReplaceAllEnabled(() -> {
Point currentSelection = new Point(0, 0);
Expand Down Expand Up @@ -481,11 +501,12 @@ public int findAndSelect(int offset) {
* @return the selection after replacing, i.e. the inserted text
*/
private Point replaceSelection() {
String effectiveReplaceString = buildReplaceString();
if (target instanceof IFindReplaceTargetExtension3) {
((IFindReplaceTargetExtension3) target).replaceSelection(replaceString,
((IFindReplaceTargetExtension3) target).replaceSelection(effectiveReplaceString,
isAvailableAndActive(SearchOptions.REGEX));
} else {
target.replaceSelection(replaceString);
target.replaceSelection(effectiveReplaceString);
}

return target.getSelection();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*******************************************************************************
* Copyright (c) 2026 Contributors to Eclipse Foundation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Contributors to Eclipse Foundation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.findandreplace;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Provides an auto-incrementing counter that can be embedded in Find/Replace
* replace strings using the placeholder syntax {@code #{start,step,pad}}.
*
* <p>
* Syntax: {@code #{start,step,pad}} where
* <ul>
* <li>{@code start} &mdash; initial value (integer, may be negative)</li>
* <li>{@code step} &mdash; increment per replacement (integer, may be negative
* for descending sequences)</li>
* <li>{@code pad} &mdash; minimum number of digits, zero-padded (0 = no
* padding)</li>
* </ul>
*
* <p>
* Examples:
* <ul>
* <li>{@code #{1,1,3}} &mdash; produces {@code 001}, {@code 002},
* {@code 003}, &hellip;</li>
* <li>{@code #{10,5,0}} &mdash; produces {@code 10}, {@code 15}, {@code 20},
* &hellip;</li>
* <li>{@code #{100,-1,0}} &mdash; produces {@code 100}, {@code 99},
* {@code 98}, &hellip;</li>
* </ul>
*
* <p>
* The placeholder may be combined with literal text and regex back-references,
* for example {@code $1_#{1,1,2}} produces {@code Match1_01},
* {@code Match1_02}, &hellip;
*
* @since 3.21
*/
public class ReplaceCounter {

/**
* Pattern matching the counter placeholder {@code #{start,step,pad}}.
* Captures: group 1 = start, group 2 = step, group 3 = pad.
*/
static final Pattern COUNTER_PATTERN = Pattern.compile("#\\{(-?\\d+),(-?\\d+),(\\d+)\\}"); //$NON-NLS-1$

private final int start;
private final int step;
private final int pad;
private int current;

/**
* Creates a counter with the given parameters.
*
* @param start initial counter value
* @param step increment applied after each {@link #next()} call
* @param pad minimum digit width (0 = no padding)
*/
public ReplaceCounter(int start, int step, int pad) {
this.start = start;
this.step = step;
this.pad = pad;
this.current = start;
}

/** Resets the counter to the initial {@code start} value. */
public void reset() {
current = start;
}

/**
* Returns the current counter value as a (possibly zero-padded) string, then
* advances the counter by {@code step}.
*
* @return formatted counter value
*/
public String next() {
String value;
if (pad > 0) {
value = String.format("%0" + pad + "d", current); //$NON-NLS-1$ //$NON-NLS-2$
} else {
value = Integer.toString(current);
}
current += step;
return value;
}

/**
* Replaces the first counter placeholder in {@code template} with the
* {@link #next()} value.
*
* @param template the replace string possibly containing {@code #{...}}
* @return the template with the placeholder replaced by the next counter value
*/
public String expand(String template) {
Matcher matcher = COUNTER_PATTERN.matcher(template);
if (matcher.find()) {
return matcher.replaceFirst(Matcher.quoteReplacement(next()));
}
return template;
}

/**
* Returns {@code true} if the given string contains a counter placeholder.
*
* @param replaceString string to test
* @return {@code true} if a placeholder is present
*/
public static boolean containsCounter(String replaceString) {
return COUNTER_PATTERN.matcher(replaceString).find();
}

/**
* Parses the first counter placeholder found in {@code replaceString} and
* returns a new {@link ReplaceCounter} with the corresponding parameters.
* Returns {@code null} if no placeholder is present.
*
* @param replaceString string to parse
* @return a new counter, or {@code null}
*/
public static ReplaceCounter parse(String replaceString) {
Matcher matcher = COUNTER_PATTERN.matcher(replaceString);
if (matcher.find()) {
int start = Integer.parseInt(matcher.group(1));
int step = Integer.parseInt(matcher.group(2));
int pad = Math.min(Integer.parseInt(matcher.group(3)), 20);
return new ReplaceCounter(start, step, pad);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %Plugin.name
Bundle-SymbolicName: org.eclipse.ui.workbench.texteditor.tests
Bundle-Version: 3.15.0.qualifier
Bundle-Version: 3.15.100.qualifier
Bundle-Vendor: %Plugin.providerName
Bundle-Localization: plugin
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,4 +992,138 @@ private void expectStatusIsMessageWithString(IFindReplaceLogic findReplaceLogic,
assertThat(((InvalidRegExStatus) findReplaceLogic.getStatus()).getMessage(), equalTo(message));
}

// -----------------------------------------------------------------------
// Counter placeholder #{start,step,pad} integration tests
// -----------------------------------------------------------------------

@Test
public void testReplaceAll_withCounter_paddedAscending() {
TextViewer textViewer= setupTextViewer("a a a");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,3}");
findReplaceLogic.performReplaceAll();

assertThat(textViewer.getDocument().get(), equalTo("001 002 003"));
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
}

@Test
public void testReplaceAll_withCounter_unpaddedLargeStep() {
TextViewer textViewer= setupTextViewer("x x x");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "x", "#{10,10,0}");
findReplaceLogic.performReplaceAll();

assertThat(textViewer.getDocument().get(), equalTo("10 20 30"));
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
}

@Test
public void testReplaceAll_withCounter_resetsOnNewReplaceString() {
TextViewer textViewer= setupTextViewer("a a");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
findReplaceLogic.performReplaceAll();
assertThat(textViewer.getDocument().get(), equalTo("1 2"));

// Reset document and perform again with the same replace string set anew
textViewer.setDocument(new Document("a a"));
setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
findReplaceLogic.performReplaceAll();
assertThat(textViewer.getDocument().get(), equalTo("1 2"));
}

@Test
public void testReplaceAll_withCounter_counterResetsBetweenReplaceAllCalls() {
TextViewer textViewer= setupTextViewer("a a");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
findReplaceLogic.performReplaceAll();
assertThat(textViewer.getDocument().get(), equalTo("1 2"));

// Second Replace All on new content must restart from 1, not continue from 3
textViewer.setDocument(new Document("a a"));
findReplaceLogic.performReplaceAll();
assertThat(textViewer.getDocument().get(), equalTo("1 2"));
}

@Test
public void testReplaceAll_withCounter_descendingSequence() {
TextViewer textViewer= setupTextViewer("z z z z");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "z", "#{10,-2,0}");
findReplaceLogic.performReplaceAll();

assertThat(textViewer.getDocument().get(), equalTo("10 8 6 4"));
expectStatusIsReplaceAllWithCount(findReplaceLogic, 4);
}

@Test
public void testReplaceAll_withCounter_combinedWithLiteralText() {
TextViewer textViewer= setupTextViewer("x x");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "x", "item_#{1,1,2}");
findReplaceLogic.performReplaceAll();

assertThat(textViewer.getDocument().get(), equalTo("item_01 item_02"));
}

@Test
public void testReplaceAll_withoutCounter_behavesAsPlainReplace() {
TextViewer textViewer= setupTextViewer("a a a");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "a", "b");
findReplaceLogic.performReplaceAll();

assertThat(textViewer.getDocument().get(), equalTo("b b b"));
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
}

@Test
public void testReplaceAll_withCounter_andRegex() {
TextViewer textViewer= setupTextViewer("aa bb cc");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);
findReplaceLogic.activate(SearchOptions.REGEX);

setFindAndReplaceString(findReplaceLogic, "(\\w+)", "$1_#{1,1,2}");
findReplaceLogic.performReplaceAll();

assertThat(textViewer.getDocument().get(), equalTo("aa_01 bb_02 cc_03"));
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
}

@Test
public void testSelectAndReplace_withCounter_incrementsAcrossCalls() {
TextViewer textViewer= setupTextViewer("a a a");
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
findReplaceLogic.activate(SearchOptions.FORWARD);

setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");

findReplaceLogic.performSearch();
findReplaceLogic.performSelectAndReplace();
assertThat(textViewer.getDocument().get(), equalTo("1 a a"));

findReplaceLogic.performSelectAndReplace();
assertThat(textViewer.getDocument().get(), equalTo("1 2 a"));

findReplaceLogic.performSelectAndReplace();
assertThat(textViewer.getDocument().get(), equalTo("1 2 3"));
}

}
Loading
Loading