Skip to content

Commit 40f7493

Browse files
committed
Add auto-incrementing counter placeholder to Find/Replace #3842
Adds a #{start,step,pad} counter placeholder to Find/Replace replace strings. During Replace All or Replace/Find, the placeholder expands to an auto-incrementing number with optional zero-padding. #3842
1 parent deaeb50 commit 40f7493

File tree

6 files changed

+473
-3
lines changed

6 files changed

+473
-3
lines changed

bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/FindReplaceDocumentAdapterContentProposalProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ public IContentProposal[] computeReplaceProposals() {
230230
addBracketProposal("\\c", 2, RegExMessages.getString("displayString_bs_c"), RegExMessages.getString("additionalInfo_bs_c")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
231231
addBsProposal("\\C", RegExMessages.getString("displayString_replace_bs_C"), RegExMessages.getString("additionalInfo_replace_bs_C")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
232232
}
233+
addBracketProposal("#{1,1,3}", 8, RegExMessages.getString("displayString_counter"), RegExMessages.getString("additionalInfo_counter")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
233234
fPriorityProposals.addAll(fProposals);
234235
return fPriorityProposals.toArray(new IContentProposal[fPriorityProposals.size()]);
235236
}

bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/RegExMessages.properties

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,4 +540,16 @@ Find: "foo" Replace: "my\\Cbar\\CFar"\n\
540540
"FOO" will be replaced by "myBARFAR".\n\
541541
"Foo" will be replaced by "myBarFar".\n\n\
542542
Note that the content of a group ($i, \\i) is currently inserted unmodified.
543-
543+
displayString_counter=#{start,step,pad} - Auto-incrementing counter
544+
additionalInfo_counter=#{start,step,pad} - Inserts an auto-incrementing counter.\n\n\
545+
Syntax: #{start,step,pad}\n\
546+
start \u2014 initial value (may be negative)\n\
547+
step \u2014 increment per replacement (negative for descending)\n\
548+
pad \u2014 minimum digit width, zero-padded (0 = no padding)\n\n\
549+
Examples:\n\
550+
#{1,1,3} \u2192 001, 002, 003, ...\n\
551+
#{10,5,0} \u2192 10, 15, 20, ...\n\
552+
#{100,-1,0} \u2192 100, 99, 98, ...\n\n\
553+
Can be combined with text and regex groups:\n\
554+
item_#{1,1,2} \u2192 item_01, item_02, ...\n\
555+
$1_#{1,1,2} \u2192 Match1_01, Match1_02, ...

bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceLogic.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public class FindReplaceLogic implements IFindReplaceLogic {
5757

5858
private String findString = ""; //$NON-NLS-1$
5959
private String replaceString = ""; //$NON-NLS-1$
60+
private ReplaceCounter replaceCounter;
6061

6162
@Override
6263
public void setFindString(String findString) {
@@ -69,6 +70,22 @@ public void setFindString(String findString) {
6970
@Override
7071
public void setReplaceString(String replaceString) {
7172
this.replaceString = Objects.requireNonNull(replaceString);
73+
this.replaceCounter = ReplaceCounter.parse(replaceString);
74+
}
75+
76+
/**
77+
* Returns the effective replace string for the current replacement. If the
78+
* replace string contains a counter placeholder ({@code #{start,step,pad}}),
79+
* the placeholder is replaced by the next counter value. Otherwise the raw
80+
* replace string is returned unchanged.
81+
*
82+
* @return the replace string with counter placeholder expanded (if any)
83+
*/
84+
private String buildReplaceString() {
85+
if (replaceCounter != null) {
86+
return replaceCounter.expand(replaceString);
87+
}
88+
return replaceString;
7289
}
7390

7491
@Override
@@ -367,6 +384,9 @@ private int replaceAll() {
367384
}
368385

369386
List<Point> replacements = new ArrayList<>();
387+
if (replaceCounter != null) {
388+
replaceCounter.reset();
389+
}
370390
executeInForwardMode(() -> {
371391
executeWithReplaceAllEnabled(() -> {
372392
Point currentSelection = new Point(0, 0);
@@ -481,11 +501,12 @@ public int findAndSelect(int offset) {
481501
* @return the selection after replacing, i.e. the inserted text
482502
*/
483503
private Point replaceSelection() {
504+
String effectiveReplaceString = buildReplaceString();
484505
if (target instanceof IFindReplaceTargetExtension3) {
485-
((IFindReplaceTargetExtension3) target).replaceSelection(replaceString,
506+
((IFindReplaceTargetExtension3) target).replaceSelection(effectiveReplaceString,
486507
isAvailableAndActive(SearchOptions.REGEX));
487508
} else {
488-
target.replaceSelection(replaceString);
509+
target.replaceSelection(effectiveReplaceString);
489510
}
490511

491512
return target.getSelection();
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Contributors to Eclipse Foundation and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Contributors to Eclipse Foundation - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.ui.internal.findandreplace;
15+
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
18+
19+
/**
20+
* Provides an auto-incrementing counter that can be embedded in Find/Replace
21+
* replace strings using the placeholder syntax {@code #{start,step,pad}}.
22+
*
23+
* <p>
24+
* Syntax: {@code #{start,step,pad}} where
25+
* <ul>
26+
* <li>{@code start} &mdash; initial value (integer, may be negative)</li>
27+
* <li>{@code step} &mdash; increment per replacement (integer, may be negative
28+
* for descending sequences)</li>
29+
* <li>{@code pad} &mdash; minimum number of digits, zero-padded (0 = no
30+
* padding)</li>
31+
* </ul>
32+
*
33+
* <p>
34+
* Examples:
35+
* <ul>
36+
* <li>{@code #{1,1,3}} &mdash; produces {@code 001}, {@code 002},
37+
* {@code 003}, &hellip;</li>
38+
* <li>{@code #{10,5,0}} &mdash; produces {@code 10}, {@code 15}, {@code 20},
39+
* &hellip;</li>
40+
* <li>{@code #{100,-1,0}} &mdash; produces {@code 100}, {@code 99},
41+
* {@code 98}, &hellip;</li>
42+
* </ul>
43+
*
44+
* <p>
45+
* The placeholder may be combined with literal text and regex back-references,
46+
* for example {@code $1_#{1,1,2}} produces {@code Match1_01},
47+
* {@code Match1_02}, &hellip;
48+
*
49+
* @since 3.21
50+
*/
51+
public class ReplaceCounter {
52+
53+
/**
54+
* Pattern matching the counter placeholder {@code #{start,step,pad}}.
55+
* Captures: group 1 = start, group 2 = step, group 3 = pad.
56+
*/
57+
static final Pattern COUNTER_PATTERN = Pattern.compile("#\\{(-?\\d+),(-?\\d+),(\\d+)\\}"); //$NON-NLS-1$
58+
59+
private final int start;
60+
private final int step;
61+
private final int pad;
62+
private int current;
63+
64+
/**
65+
* Creates a counter with the given parameters.
66+
*
67+
* @param start initial counter value
68+
* @param step increment applied after each {@link #next()} call
69+
* @param pad minimum digit width (0 = no padding)
70+
*/
71+
public ReplaceCounter(int start, int step, int pad) {
72+
this.start = start;
73+
this.step = step;
74+
this.pad = pad;
75+
this.current = start;
76+
}
77+
78+
/** Resets the counter to the initial {@code start} value. */
79+
public void reset() {
80+
current = start;
81+
}
82+
83+
/**
84+
* Returns the current counter value as a (possibly zero-padded) string, then
85+
* advances the counter by {@code step}.
86+
*
87+
* @return formatted counter value
88+
*/
89+
public String next() {
90+
String value;
91+
if (pad > 0) {
92+
value = String.format("%0" + pad + "d", current); //$NON-NLS-1$ //$NON-NLS-2$
93+
} else {
94+
value = Integer.toString(current);
95+
}
96+
current += step;
97+
return value;
98+
}
99+
100+
/**
101+
* Replaces the first counter placeholder in {@code template} with the
102+
* {@link #next()} value.
103+
*
104+
* @param template the replace string possibly containing {@code #{...}}
105+
* @return the template with the placeholder replaced by the next counter value
106+
*/
107+
public String expand(String template) {
108+
Matcher matcher = COUNTER_PATTERN.matcher(template);
109+
if (matcher.find()) {
110+
return matcher.replaceFirst(Matcher.quoteReplacement(next()));
111+
}
112+
return template;
113+
}
114+
115+
/**
116+
* Returns {@code true} if the given string contains a counter placeholder.
117+
*
118+
* @param replaceString string to test
119+
* @return {@code true} if a placeholder is present
120+
*/
121+
public static boolean containsCounter(String replaceString) {
122+
return COUNTER_PATTERN.matcher(replaceString).find();
123+
}
124+
125+
/**
126+
* Parses the first counter placeholder found in {@code replaceString} and
127+
* returns a new {@link ReplaceCounter} with the corresponding parameters.
128+
* Returns {@code null} if no placeholder is present.
129+
*
130+
* @param replaceString string to parse
131+
* @return a new counter, or {@code null}
132+
*/
133+
public static ReplaceCounter parse(String replaceString) {
134+
Matcher matcher = COUNTER_PATTERN.matcher(replaceString);
135+
if (matcher.find()) {
136+
int start = Integer.parseInt(matcher.group(1));
137+
int step = Integer.parseInt(matcher.group(2));
138+
int pad = Math.min(Integer.parseInt(matcher.group(3)), 20);
139+
return new ReplaceCounter(start, step, pad);
140+
}
141+
return null;
142+
}
143+
}

tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceLogicTest.java

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,4 +992,138 @@ private void expectStatusIsMessageWithString(IFindReplaceLogic findReplaceLogic,
992992
assertThat(((InvalidRegExStatus) findReplaceLogic.getStatus()).getMessage(), equalTo(message));
993993
}
994994

995+
// -----------------------------------------------------------------------
996+
// Counter placeholder #{start,step,pad} integration tests
997+
// -----------------------------------------------------------------------
998+
999+
@Test
1000+
public void testReplaceAll_withCounter_paddedAscending() {
1001+
TextViewer textViewer= setupTextViewer("a a a");
1002+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1003+
findReplaceLogic.activate(SearchOptions.FORWARD);
1004+
1005+
setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,3}");
1006+
findReplaceLogic.performReplaceAll();
1007+
1008+
assertThat(textViewer.getDocument().get(), equalTo("001 002 003"));
1009+
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
1010+
}
1011+
1012+
@Test
1013+
public void testReplaceAll_withCounter_unpaddedLargeStep() {
1014+
TextViewer textViewer= setupTextViewer("x x x");
1015+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1016+
findReplaceLogic.activate(SearchOptions.FORWARD);
1017+
1018+
setFindAndReplaceString(findReplaceLogic, "x", "#{10,10,0}");
1019+
findReplaceLogic.performReplaceAll();
1020+
1021+
assertThat(textViewer.getDocument().get(), equalTo("10 20 30"));
1022+
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
1023+
}
1024+
1025+
@Test
1026+
public void testReplaceAll_withCounter_resetsOnNewReplaceString() {
1027+
TextViewer textViewer= setupTextViewer("a a");
1028+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1029+
findReplaceLogic.activate(SearchOptions.FORWARD);
1030+
1031+
setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
1032+
findReplaceLogic.performReplaceAll();
1033+
assertThat(textViewer.getDocument().get(), equalTo("1 2"));
1034+
1035+
// Reset document and perform again with the same replace string set anew
1036+
textViewer.setDocument(new Document("a a"));
1037+
setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
1038+
findReplaceLogic.performReplaceAll();
1039+
assertThat(textViewer.getDocument().get(), equalTo("1 2"));
1040+
}
1041+
1042+
@Test
1043+
public void testReplaceAll_withCounter_counterResetsBetweenReplaceAllCalls() {
1044+
TextViewer textViewer= setupTextViewer("a a");
1045+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1046+
findReplaceLogic.activate(SearchOptions.FORWARD);
1047+
1048+
setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
1049+
findReplaceLogic.performReplaceAll();
1050+
assertThat(textViewer.getDocument().get(), equalTo("1 2"));
1051+
1052+
// Second Replace All on new content must restart from 1, not continue from 3
1053+
textViewer.setDocument(new Document("a a"));
1054+
findReplaceLogic.performReplaceAll();
1055+
assertThat(textViewer.getDocument().get(), equalTo("1 2"));
1056+
}
1057+
1058+
@Test
1059+
public void testReplaceAll_withCounter_descendingSequence() {
1060+
TextViewer textViewer= setupTextViewer("z z z z");
1061+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1062+
findReplaceLogic.activate(SearchOptions.FORWARD);
1063+
1064+
setFindAndReplaceString(findReplaceLogic, "z", "#{10,-2,0}");
1065+
findReplaceLogic.performReplaceAll();
1066+
1067+
assertThat(textViewer.getDocument().get(), equalTo("10 8 6 4"));
1068+
expectStatusIsReplaceAllWithCount(findReplaceLogic, 4);
1069+
}
1070+
1071+
@Test
1072+
public void testReplaceAll_withCounter_combinedWithLiteralText() {
1073+
TextViewer textViewer= setupTextViewer("x x");
1074+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1075+
findReplaceLogic.activate(SearchOptions.FORWARD);
1076+
1077+
setFindAndReplaceString(findReplaceLogic, "x", "item_#{1,1,2}");
1078+
findReplaceLogic.performReplaceAll();
1079+
1080+
assertThat(textViewer.getDocument().get(), equalTo("item_01 item_02"));
1081+
}
1082+
1083+
@Test
1084+
public void testReplaceAll_withoutCounter_behavesAsPlainReplace() {
1085+
TextViewer textViewer= setupTextViewer("a a a");
1086+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1087+
findReplaceLogic.activate(SearchOptions.FORWARD);
1088+
1089+
setFindAndReplaceString(findReplaceLogic, "a", "b");
1090+
findReplaceLogic.performReplaceAll();
1091+
1092+
assertThat(textViewer.getDocument().get(), equalTo("b b b"));
1093+
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
1094+
}
1095+
1096+
@Test
1097+
public void testReplaceAll_withCounter_andRegex() {
1098+
TextViewer textViewer= setupTextViewer("aa bb cc");
1099+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1100+
findReplaceLogic.activate(SearchOptions.FORWARD);
1101+
findReplaceLogic.activate(SearchOptions.REGEX);
1102+
1103+
setFindAndReplaceString(findReplaceLogic, "(\\w+)", "$1_#{1,1,2}");
1104+
findReplaceLogic.performReplaceAll();
1105+
1106+
assertThat(textViewer.getDocument().get(), equalTo("aa_01 bb_02 cc_03"));
1107+
expectStatusIsReplaceAllWithCount(findReplaceLogic, 3);
1108+
}
1109+
1110+
@Test
1111+
public void testSelectAndReplace_withCounter_incrementsAcrossCalls() {
1112+
TextViewer textViewer= setupTextViewer("a a a");
1113+
IFindReplaceLogic findReplaceLogic= setupFindReplaceLogicObject(textViewer);
1114+
findReplaceLogic.activate(SearchOptions.FORWARD);
1115+
1116+
setFindAndReplaceString(findReplaceLogic, "a", "#{1,1,0}");
1117+
1118+
findReplaceLogic.performSearch();
1119+
findReplaceLogic.performSelectAndReplace();
1120+
assertThat(textViewer.getDocument().get(), equalTo("1 a a"));
1121+
1122+
findReplaceLogic.performSelectAndReplace();
1123+
assertThat(textViewer.getDocument().get(), equalTo("1 2 a"));
1124+
1125+
findReplaceLogic.performSelectAndReplace();
1126+
assertThat(textViewer.getDocument().get(), equalTo("1 2 3"));
1127+
}
1128+
9951129
}

0 commit comments

Comments
 (0)