Skip to content

Commit af11dab

Browse files
[MOD] XQuery, text editor: shortcuts, improved search&replace
1 parent 7662015 commit af11dab

29 files changed

Lines changed: 604 additions & 240 deletions

basex-core/src/main/java/org/basex/core/Text.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -843,21 +843,17 @@ public interface Text {
843843
/** Command info. */
844844
String REPLACE_ALL = lang("replace_all");
845845
/** Command info. */
846+
String REPLACE_NEXT = lang("replace_next");
847+
/** Command info. */
846848
String REPLACE_WITH = lang("replace_with");
847849
/** Match case. */
848850
String MATCH_CASE = lang("match_case");
849851
/** Whole word. */
850852
String WHOLE_WORD = lang("whole_word");
851-
/** Multi-line mode. */
852-
String MULTI_LINE = lang("multi_line");
853+
/** Dot matches newline. */
854+
String DOT_ALL = lang("dot_all");
853855
/** Regular expression. */
854856
String REGULAR_EXPR = lang("regular_expr");
855-
/** % string(s) found.. */
856-
String STRINGS_FOUND_X = lang("strings_found_%");
857-
/** Strings were replaced.. */
858-
String STRINGS_REPLACED = lang("strings_replaced");
859-
/** Searching. */
860-
String SEARCHING = lang("searching");
861857

862858
/** Serialization parameters. */
863859
String SERIALIZATION = lang("serialization");

basex-core/src/main/java/org/basex/gui/layout/BaseXKeys.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public enum BaseXKeys {
9292
/** Match case. */ MATCHCASE(SHIFT, VK_F4),
9393
/** Whole word. */ WHOLEWORD(SHIFT, VK_F5),
9494
/** Regular expression. */ REGEX(SHIFT, VK_F6),
95-
/** Multi-line. */ MULTILINE(SHIFT, VK_F7),
95+
/** Dot matches newline. */ DOTALL(SHIFT, VK_F7),
96+
/** Replace all. */ REPLACEALL(META | SHIFT, VK_R),
9697

9798
// Font
9899

basex-core/src/main/java/org/basex/gui/text/ReplaceContext.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
final class ReplaceContext {
1616
/** Replace string. */
1717
private final String replace;
18+
/** Replace only the first hit in the range (regex mode). */
19+
private final boolean single;
1820
/** Text. */
1921
byte[] text;
2022

@@ -23,7 +25,17 @@ final class ReplaceContext {
2325
* @param replace replacement text
2426
*/
2527
ReplaceContext(final String replace) {
28+
this(replace, false);
29+
}
30+
31+
/**
32+
* Constructor.
33+
* @param replace replacement text
34+
* @param single replace only the first hit in the range (regex mode)
35+
*/
36+
ReplaceContext(final String replace, final boolean single) {
2637
this.replace = replace;
38+
this.single = single;
2739
}
2840

2941
/**
@@ -41,21 +53,9 @@ int[] replace(final SearchContext sc, final byte[] txt, final int start, final i
4153
} else {
4254
final TokenBuilder tb = new TokenBuilder(os).add(txt, 0, start);
4355
if(sc.regex) {
44-
// regular expressions, ignoring position arrays
45-
int flags = Pattern.DOTALL;
46-
if(!sc.mcase) flags |= Pattern.CASE_INSENSITIVE;
47-
final Pattern pattern = Pattern.compile(sc.string, flags);
48-
if(sc.multi) {
49-
tb.add(pattern.matcher(string(txt, start, end - start)).replaceAll(replace));
50-
} else {
51-
for(int e = start, s = start; e <= end; e++) {
52-
if(e < end ? txt[e] == '\n' : e != s) {
53-
tb.add(pattern.matcher(string(txt, s, e - s)).replaceAll(replace));
54-
if(e < end) tb.add('\n');
55-
s = e + 1;
56-
}
57-
}
58-
}
56+
// regular expressions, applied to the whole range (ignoring position arrays)
57+
final Matcher matcher = sc.pattern.matcher(string(txt, start, end - start));
58+
tb.add(single ? matcher.replaceFirst(replace) : matcher.replaceAll(replace));
5959
} else {
6060
final byte[] srch = token(sc.string);
6161
final byte[] rplc = token(replace);

basex-core/src/main/java/org/basex/gui/text/SearchBar.java

Lines changed: 133 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import org.basex.gui.*;
1212
import org.basex.gui.layout.*;
1313
import org.basex.gui.listener.*;
14-
import org.basex.util.*;
1514

1615
/**
1716
* This panel provides search and replace facilities.
@@ -36,8 +35,8 @@ public enum SearchDir {
3635
final AbstractButton mcase;
3736
/** Mode: whole word. */
3837
final AbstractButton word;
39-
/** Mode: multi-line. */
40-
final AbstractButton multi;
38+
/** Mode: dot matches newline. */
39+
final AbstractButton dotall;
4140
/** Action: replace. */
4241
private final AbstractButton rplc;
4342
/** Action: close. */
@@ -49,6 +48,12 @@ public enum SearchDir {
4948
private final BaseXCombo find;
5049
/** Replace text. */
5150
private final BaseXCombo replace;
51+
/** Hit count label. */
52+
private final BaseXLabel count;
53+
/** Total number of hits ({@code -1} if no search is active). */
54+
private int total = -1;
55+
/** Whether the next refresh-driven jump should select its hit (set by {@link #replaceNext}). */
56+
private boolean selectNext;
5257

5358
/** Escape key listener. */
5459
private final KeyListener keys;
@@ -75,14 +80,16 @@ public enum SearchDir {
7580
find.hint(Text.FIND + "\u2026");
7681
replace = new BaseXCombo(gui, true).history(GUIOptions.REPLACED, gui.gopts);
7782
replace.hint(Text.REPLACE_WITH + "\u2026");
83+
count = new BaseXLabel(" ");
7884

7985
final ActionListener al = e -> search();
8086
mcase = button("f_case", BaseXLayout.addShortcut(Text.MATCH_CASE, MATCHCASE.toString()), al);
8187
word = button("f_word", BaseXLayout.addShortcut(Text.WHOLE_WORD, WHOLEWORD.toString()), al);
8288
regex = button("f_regex", BaseXLayout.addShortcut(Text.REGULAR_EXPR, REGEX.toString()), al);
83-
multi = button("f_multi", BaseXLayout.addShortcut(Text.MULTI_LINE, MULTILINE.toString()), al);
89+
dotall = button("f_dotall", BaseXLayout.addShortcut(Text.DOT_ALL, DOTALL.toString()), al);
8490

85-
rplc = BaseXButton.get("f_replace", Text.REPLACE_ALL, false, gui);
91+
rplc = BaseXButton.get("f_replace", BaseXLayout.addShortcut(Text.REPLACE_ALL,
92+
REPLACEALL.toString()), false, gui);
8693
rplc.setFocusable(true);
8794
cls = BaseXButton.get("f_close", BaseXLayout.addShortcut(Text.CLOSE, ESCAPE.toString()),
8895
false, gui);
@@ -106,9 +113,6 @@ public void keyPressed(final KeyEvent e) {
106113
public void keyReleased(final KeyEvent e) {
107114
final String srch = find.getText();
108115
if(!oldSearch.equals(srch)) {
109-
if(regex.isEnabled() && find.getText().matches("^.*(?<!\\\\)\\\\n.*")) {
110-
multi.setSelected(true);
111-
}
112116
oldSearch = srch;
113117
search();
114118
}
@@ -121,28 +125,36 @@ public void keyReleased(final KeyEvent e) {
121125
search();
122126
});
123127

128+
// replace the current hit when Enter is pressed in the replace field
129+
replace.addKeyListener(new KeyAdapter() {
130+
@Override
131+
public void keyPressed(final KeyEvent e) {
132+
if(ENTER.is(e)) {
133+
replace.updateHistory();
134+
replaceNext();
135+
}
136+
}
137+
});
138+
124139
// add default shortcuts to all interaction components
125140
keys = (KeyPressedListener) e -> {
126141
if(ESCAPE.is(e)) deactivate(false);
127142
else if(MATCHCASE.is(e)) toggle(mcase);
128143
else if(WHOLEWORD.is(e)) toggle(word);
129144
else if(REGEX.is(e)) toggle(regex);
130-
else if(MULTILINE.is(e)) toggle(multi);
145+
else if(DOTALL.is(e)) toggle(dotall);
146+
else if(REPLACEALL.is(e)) replaceAll();
131147
};
132148
mcase.addKeyListener(keys);
133149
word.addKeyListener(keys);
134150
regex.addKeyListener(keys);
135-
multi.addKeyListener(keys);
151+
dotall.addKeyListener(keys);
136152
find.addKeyListener(keys);
137153
replace.addKeyListener(keys);
138154
rplc.addKeyListener(keys);
139155
cls.addKeyListener(keys);
140156

141-
rplc.addActionListener(e -> {
142-
deactivate(true);
143-
final String in = replace.getText();
144-
editor.replace(new ReplaceContext(regex.isSelected() ? decode(in) : in));
145-
});
157+
rplc.addActionListener(e -> replaceAll());
146158
cls.addActionListener(e -> deactivate(true));
147159

148160
// set initial values
@@ -165,10 +177,14 @@ public void editor(final TextPanel text, final boolean srch) {
165177
west.add(mcase);
166178
west.add(word);
167179
west.add(regex);
168-
west.add(multi);
180+
west.add(dotall);
181+
182+
final BaseXBack found = new BaseXBack(false).layout(new BorderLayout(8, 0));
183+
found.add(find, BorderLayout.CENTER);
184+
found.add(count, BorderLayout.EAST);
169185

170186
final BaseXBack center = new BaseXBack(false).layout(new GridLayout(1, 2, 2, 0));
171-
center.add(find);
187+
center.add(found);
172188
if(editable) center.add(replace);
173189

174190
final BaseXToolBar east = new BaseXToolBar();
@@ -261,12 +277,43 @@ public void toggle(final AbstractButton button) {
261277
activate(find.getText(), false, true);
262278
}
263279

280+
/**
281+
* Indicates whether the current hits can be replaced.
282+
* @return result of check
283+
*/
284+
public boolean replaceEnabled() {
285+
return editor != null && editor.isEditable() && isVisible() && rplc.isEnabled();
286+
}
287+
288+
/**
289+
* Replaces all hits of the current search.
290+
*/
291+
public void replaceAll() {
292+
if(!replaceEnabled()) return;
293+
deactivate(true);
294+
final String in = replace.getText();
295+
editor.replace(new ReplaceContext(regex.isSelected() ? normalize(in) : in));
296+
}
297+
298+
/**
299+
* Replaces the current hit and advances to the next one.
300+
*/
301+
public void replaceNext() {
302+
if(!replaceEnabled() || editor.editor.searchIndex() < 0) return;
303+
final String in = replace.getText();
304+
final boolean rgx = regex.isSelected();
305+
if(editor.replaceNext(new ReplaceContext(rgx ? normalize(in) : in, true))) {
306+
selectNext = true;
307+
search();
308+
}
309+
}
310+
264311
/**
265312
* Searches text in the current editor.
266313
*/
267314
private void search() {
268315
final boolean sel = regex.isSelected();
269-
multi.setEnabled(sel);
316+
dotall.setEnabled(sel);
270317
word.setEnabled(!sel);
271318
search(true);
272319
}
@@ -280,8 +327,31 @@ void refresh(final SearchContext sc, final boolean jump) {
280327
final boolean hits = sc.nr != 0, empty = sc.string.isEmpty();
281328
rplc.setEnabled(hits && !empty);
282329
find.highlight(hits || empty);
283-
if(isVisible()) gui.status.setText(Util.info(Text.STRINGS_FOUND_X, sc.nr()), true);
284-
if(jump) editor.jump(SearchDir.CURRENT, false);
330+
total = empty ? -1 : sc.nr;
331+
count.setText(countText());
332+
// a Replace Next selects the following hit; a plain search only repositions the view
333+
final boolean select = selectNext;
334+
selectNext = false;
335+
if(jump) editor.jump(SearchDir.CURRENT, select);
336+
}
337+
338+
/**
339+
* Updates the hit count to the current navigation position.
340+
*/
341+
void refreshCount() {
342+
count.setText(countText());
343+
}
344+
345+
/**
346+
* Returns the hit count text ("N/M" with a trailing space, empty if no search is active).
347+
* @return count text
348+
*/
349+
private String countText() {
350+
if(total < 0) return "";
351+
int n = editor == null ? -1 : editor.editor.searchIndex();
352+
if(n < 0 || n >= total) n = 0;
353+
// count string: thin spaces (U+2009) around the slash, trailing space for padding
354+
return (total == 0 ? 0 : n + 1) + "\u2009/\u2009" + total + " ";
285355
}
286356

287357
// PRIVATE METHODS ==============================================================================
@@ -301,8 +371,17 @@ private void setSearch(final String text) {
301371
*/
302372
private void search(final boolean jump) {
303373
final String text = isVisible() ? find.getText() : "";
304-
if(!text.isEmpty()) gui.status.setText(Text.SEARCHING + Text.DOTS, true);
305-
editor.search(new SearchContext(this, text), jump);
374+
final SearchContext sc = new SearchContext(this, text);
375+
if(sc.error != null) {
376+
// invalid regular expression: highlight the field red, show the cause as its tooltip
377+
find.highlight(false);
378+
find.setToolTipText(sc.error);
379+
total = -1;
380+
count.setText("");
381+
} else {
382+
find.setToolTipText(Text.FIND + "\u2026");
383+
editor.search(sc, jump);
384+
}
306385
}
307386

308387
/**
@@ -321,34 +400,50 @@ private AbstractButton button(final String icon, final String tooltip,
321400
}
322401

323402
/**
324-
* Decodes the specified string and replaces backslashed n's and t's with
325-
* newlines and tab characters.
403+
* Normalizes a regular-expression replacement string to a valid Java replacement.
404+
* Supported: {@code \n}, {@code \t}, {@code \\}, {@code \$}, group references
405+
* {@code $1}/{@code \1}/{@code ${name}}; a bare {@code $} is treated as literal.
326406
* @param in input
327-
* @return decoded string
407+
* @return normalized string
328408
*/
329-
private static String decode(final String in) {
409+
static String normalize(final String in) {
330410
final StringBuilder sb = new StringBuilder();
331-
boolean bs = false;
332411
final int is = in.length();
333412
for(int i = 0; i < is; i++) {
334413
final char ch = in.charAt(i);
335-
if(bs) {
336-
if(ch == 'n') {
337-
sb.append('\n');
338-
} else if(ch == 't') {
339-
sb.append('\t');
414+
if(ch == '\\') {
415+
final char n = i + 1 < is ? in.charAt(++i) : 0;
416+
if(n == 'n') sb.append('\n');
417+
else if(n == 't') sb.append('\t');
418+
else if(n == '\\') sb.append("\\\\");
419+
else if(n == '$') sb.append("\\$");
420+
else if(n >= '0' && n <= '9') sb.append('$').append(n);
421+
else {
422+
sb.append("\\\\");
423+
if(n != 0) sb.append(n);
424+
}
425+
} else if(ch == '$') {
426+
final char n = i + 1 < is ? in.charAt(i + 1) : 0;
427+
if(n >= '0' && n <= '9') {
428+
sb.append('$');
429+
} else if(n == '{') {
430+
// ${digits} -> $digits (numbered); ${name} passes through as a named reference
431+
int k = i + 2;
432+
while(k < is && in.charAt(k) != '}') k++;
433+
final String name = in.substring(i + 2, Math.min(k, is));
434+
if(k < is && !name.isEmpty() && name.chars().allMatch(Character::isDigit)) {
435+
sb.append('$').append(name);
436+
i = k;
437+
} else {
438+
sb.append('$');
439+
}
340440
} else {
341-
sb.append('\\');
342-
if(ch != '\\') sb.append(ch);
441+
sb.append("\\$");
343442
}
344-
bs = false;
345-
} else if(ch == '\\') {
346-
bs = true;
347443
} else {
348444
sb.append(ch);
349445
}
350446
}
351-
if(bs) sb.append('\\');
352447
return sb.toString();
353448
}
354449
}

0 commit comments

Comments
 (0)