1111import org .basex .gui .*;
1212import org .basex .gui .layout .*;
1313import 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