Source code: org/gjt/sp/jedit/search/SearchAndReplace.java
1 /*
2 * SearchAndReplace.java - Search and replace
3 * :tabSize=8:indentSize=8:noTabs=false:
4 * :folding=explicit:collapseFolds=1:
5 *
6 * Copyright (C) 1999, 2000, 2001, 2002 Slava Pestov
7 * Portions copyright (C) 2001 Tom Locke
8 *
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 2
12 * of the License, or any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22 */
23
24 package org.gjt.sp.jedit.search;
25
26 //{{{ Imports
27 import bsh.*;
28 import java.awt.Component;
29 import javax.swing.JOptionPane;
30 import javax.swing.text.Segment;
31 import org.gjt.sp.jedit.*;
32 import org.gjt.sp.jedit.io.VFSManager;
33 import org.gjt.sp.jedit.msg.SearchSettingsChanged;
34 import org.gjt.sp.jedit.textarea.*;
35 import org.gjt.sp.util.CharIndexedSegment;
36 import org.gjt.sp.util.Log;
37 //}}}
38
39 /**
40 * Class that implements regular expression and literal search within
41 * jEdit buffers.<p>
42 *
43 * There are two main groups of methods in this class:
44 * <ul>
45 * <li>Property accessors - for changing search and replace settings.</li>
46 * <li>Actions - for performing search and replace.</li>
47 * </ul>
48 *
49 * The "HyperSearch" and "Keep dialog" features, as reflected in
50 * checkbox options in the search dialog, are not handled from within
51 * this class. If you wish to have these options set before the search dialog
52 * appears, make a prior call to either or both of the following:
53 *
54 * <pre> jEdit.setBooleanProperty("search.hypersearch.toggle",true);
55 * jEdit.setBooleanProperty("search.keepDialog.toggle",true);</pre>
56 *
57 * If you are not using the dialog to undertake a search or replace, you may
58 * call any of the search and replace methods (including
59 * {@link #hyperSearch(View)}) without concern for the value of these properties.
60 *
61 * @author Slava Pestov
62 * @author John Gellene (API documentation)
63 * @version $Id: SearchAndReplace.java,v 1.57 2003/10/10 23:46:24 spestov Exp $
64 */
65 public class SearchAndReplace
66 {
67 //{{{ Getters and setters
68
69 //{{{ setSearchString() method
70 /**
71 * Sets the current search string.
72 * @param search The new search string
73 */
74 public static void setSearchString(String search)
75 {
76 if(search.equals(SearchAndReplace.search))
77 return;
78
79 SearchAndReplace.search = search;
80 matcher = null;
81
82 EditBus.send(new SearchSettingsChanged(null));
83 } //}}}
84
85 //{{{ getSearchString() method
86 /**
87 * Returns the current search string.
88 */
89 public static String getSearchString()
90 {
91 return search;
92 } //}}}
93
94 //{{{ setReplaceString() method
95 /**
96 * Sets the current replacement string.
97 * @param search The new replacement string
98 */
99 public static void setReplaceString(String replace)
100 {
101 if(replace.equals(SearchAndReplace.replace))
102 return;
103
104 SearchAndReplace.replace = replace;
105
106 EditBus.send(new SearchSettingsChanged(null));
107 } //}}}
108
109 //{{{ getReplaceString() method
110 /**
111 * Returns the current replacement string.
112 */
113 public static String getReplaceString()
114 {
115 return replace;
116 } //}}}
117
118 //{{{ setIgnoreCase() method
119 /**
120 * Sets the ignore case flag.
121 * @param ignoreCase True if searches should be case insensitive,
122 * false otherwise
123 */
124 public static void setIgnoreCase(boolean ignoreCase)
125 {
126 if(ignoreCase == SearchAndReplace.ignoreCase)
127 return;
128
129 SearchAndReplace.ignoreCase = ignoreCase;
130 matcher = null;
131
132 EditBus.send(new SearchSettingsChanged(null));
133 } //}}}
134
135 //{{{ getIgnoreCase() method
136 /**
137 * Returns the state of the ignore case flag.
138 * @return True if searches should be case insensitive,
139 * false otherwise
140 */
141 public static boolean getIgnoreCase()
142 {
143 return ignoreCase;
144 } //}}}
145
146 //{{{ setRegexp() method
147 /**
148 * Sets the state of the regular expression flag.
149 * @param regexp True if regular expression searches should be
150 * performed
151 */
152 public static void setRegexp(boolean regexp)
153 {
154 if(regexp == SearchAndReplace.regexp)
155 return;
156
157 SearchAndReplace.regexp = regexp;
158 if(regexp && reverse)
159 reverse = false;
160
161 matcher = null;
162
163 EditBus.send(new SearchSettingsChanged(null));
164 } //}}}
165
166 //{{{ getRegexp() method
167 /**
168 * Returns the state of the regular expression flag.
169 * @return True if regular expression searches should be performed
170 */
171 public static boolean getRegexp()
172 {
173 return regexp;
174 } //}}}
175
176 //{{{ setReverseSearch() method
177 /**
178 * Determines whether a reverse search will conducted from the current
179 * position to the beginning of a buffer. Note that reverse search and
180 * regular expression search is mutually exclusive; enabling one will
181 * disable the other.
182 * @param reverse True if searches should go backwards,
183 * false otherwise
184 */
185 public static void setReverseSearch(boolean reverse)
186 {
187 if(reverse == SearchAndReplace.reverse)
188 return;
189
190 SearchAndReplace.reverse = reverse;
191
192 EditBus.send(new SearchSettingsChanged(null));
193 } //}}}
194
195 //{{{ getReverseSearch() method
196 /**
197 * Returns the state of the reverse search flag.
198 * @return True if searches should go backwards,
199 * false otherwise
200 */
201 public static boolean getReverseSearch()
202 {
203 return reverse;
204 } //}}}
205
206 //{{{ setBeanShellReplace() method
207 /**
208 * Sets the state of the BeanShell replace flag.
209 * @param regexp True if the replace string is a BeanShell expression
210 * @since jEdit 3.2pre2
211 */
212 public static void setBeanShellReplace(boolean beanshell)
213 {
214 if(beanshell == SearchAndReplace.beanshell)
215 return;
216
217 SearchAndReplace.beanshell = beanshell;
218
219 EditBus.send(new SearchSettingsChanged(null));
220 } //}}}
221
222 //{{{ getBeanShellReplace() method
223 /**
224 * Returns the state of the BeanShell replace flag.
225 * @return True if the replace string is a BeanShell expression
226 * @since jEdit 3.2pre2
227 */
228 public static boolean getBeanShellReplace()
229 {
230 return beanshell;
231 } //}}}
232
233 //{{{ setAutoWrap() method
234 /**
235 * Sets the state of the auto wrap around flag.
236 * @param wrap If true, the 'continue search from start' dialog
237 * will not be displayed
238 * @since jEdit 3.2pre2
239 */
240 public static void setAutoWrapAround(boolean wrap)
241 {
242 if(wrap == SearchAndReplace.wrap)
243 return;
244
245 SearchAndReplace.wrap = wrap;
246
247 EditBus.send(new SearchSettingsChanged(null));
248 } //}}}
249
250 //{{{ getAutoWrap() method
251 /**
252 * Returns the state of the auto wrap around flag.
253 * @param wrap If true, the 'continue search from start' dialog
254 * will not be displayed
255 * @since jEdit 3.2pre2
256 */
257 public static boolean getAutoWrapAround()
258 {
259 return wrap;
260 } //}}}
261
262 //{{{ setSearchMatcher() method
263 /**
264 * Sets a custom search string matcher. Note that calling
265 * {@link #setSearchString(String)},
266 * {@link #setIgnoreCase(boolean)}, or {@link #setRegexp(boolean)}
267 * will reset the matcher to the default.
268 */
269 public static void setSearchMatcher(SearchMatcher matcher)
270 {
271 SearchAndReplace.matcher = matcher;
272
273 EditBus.send(new SearchSettingsChanged(null));
274 } //}}}
275
276 //{{{ getSearchMatcher() method
277 /**
278 * Returns the current search string matcher.
279 * @param reverseOK Replacement commands need a non-reversed matcher,
280 * so they set this to false
281 * @exception IllegalArgumentException if regular expression search
282 * is enabled, the search string or replacement string is invalid
283 * @since jEdit 4.1pre7
284 */
285 public static SearchMatcher getSearchMatcher()
286 throws Exception
287 {
288 if(matcher != null)
289 return matcher;
290
291 if(search == null || "".equals(search))
292 return null;
293
294 if(regexp)
295 matcher = new RESearchMatcher(search,ignoreCase);
296 else
297 {
298 matcher = new BoyerMooreSearchMatcher(search,ignoreCase);
299 }
300
301 return matcher;
302 } //}}}
303
304 //{{{ setSearchFileSet() method
305 /**
306 * Sets the current search file set.
307 * @param fileset The file set to perform searches in
308 * @see AllBufferSet
309 * @see CurrentBufferSet
310 * @see DirectoryListSet
311 */
312 public static void setSearchFileSet(SearchFileSet fileset)
313 {
314 SearchAndReplace.fileset = fileset;
315
316 EditBus.send(new SearchSettingsChanged(null));
317 } //}}}
318
319 //{{{ getSearchFileSet() method
320 /**
321 * Returns the current search file set.
322 */
323 public static SearchFileSet getSearchFileSet()
324 {
325 return fileset;
326 } //}}}
327
328 //}}}
329
330 //{{{ Actions
331
332 //{{{ hyperSearch() method
333 /**
334 * Performs a HyperSearch.
335 * @param view The view
336 * @since jEdit 2.7pre3
337 */
338 public static boolean hyperSearch(View view)
339 {
340 return hyperSearch(view,false);
341 } //}}}
342
343 //{{{ hyperSearch() method
344 /**
345 * Performs a HyperSearch.
346 * @param view The view
347 * @param selection If true, will only search in the current selection.
348 * Note that the file set must be the current buffer file set for this
349 * to work.
350 * @since jEdit 4.0pre1
351 */
352 public static boolean hyperSearch(View view, boolean selection)
353 {
354 // component that will parent any dialog boxes
355 Component comp = SearchDialog.getSearchDialog(view);
356 if(comp == null)
357 comp = view;
358
359 record(view,"hyperSearch(view," + selection + ")",false,
360 !selection);
361
362 view.getDockableWindowManager().addDockableWindow(
363 HyperSearchResults.NAME);
364 final HyperSearchResults results = (HyperSearchResults)
365 view.getDockableWindowManager()
366 .getDockable(HyperSearchResults.NAME);
367 results.searchStarted();
368
369 try
370 {
371 SearchMatcher matcher = getSearchMatcher();
372 if(matcher == null)
373 {
374 view.getToolkit().beep();
375 results.searchFailed();
376 return false;
377 }
378
379 Selection[] s;
380 if(selection)
381 {
382 s = view.getTextArea().getSelection();
383 if(s == null)
384 {
385 results.searchFailed();
386 return false;
387 }
388 }
389 else
390 s = null;
391 VFSManager.runInWorkThread(new HyperSearchRequest(view,
392 matcher,results,s));
393 return true;
394 }
395 catch(Exception e)
396 {
397 results.searchFailed();
398 Log.log(Log.ERROR,SearchAndReplace.class,e);
399 Object[] args = { e.toString() };
400 GUIUtilities.error(comp,
401 beanshell ? "searcherror-bsh"
402 : "searcherror",args);
403 return false;
404 }
405 } //}}}
406
407 //{{{ find() method
408 /**
409 * Finds the next occurance of the search string.
410 * @param view The view
411 * @return True if the operation was successful, false otherwise
412 */
413 public static boolean find(View view)
414 {
415 // component that will parent any dialog boxes
416 Component comp = SearchDialog.getSearchDialog(view);
417 if(comp == null)
418 comp = view;
419
420 boolean repeat = false;
421 String path = fileset.getNextFile(view,null);
422 if(path == null)
423 {
424 GUIUtilities.error(comp,"empty-fileset",null);
425 return false;
426 }
427
428 boolean _reverse = reverse && fileset instanceof CurrentBufferSet;
429 if(_reverse && regexp)
430 {
431 GUIUtilities.error(comp,"regexp-reverse",null);
432 return false;
433 }
434
435 try
436 {
437 view.showWaitCursor();
438
439 SearchMatcher matcher = getSearchMatcher();
440 if(matcher == null)
441 {
442 view.getToolkit().beep();
443 return false;
444 }
445
446 record(view,"find(view)",false,true);
447
448 loop: for(;;)
449 {
450 while(path != null)
451 {
452 Buffer buffer = jEdit.openTemporary(
453 view,null,path,false);
454
455 /* this is stupid and misleading.
456 * but 'path' is not used anywhere except
457 * the above line, and if this is done
458 * after the 'continue', then we will
459 * either hang, or be forced to duplicate
460 * it inside the buffer == null, or add
461 * a 'finally' clause. you decide which one's
462 * worse. */
463 path = fileset.getNextFile(view,path);
464
465 if(buffer == null)
466 continue loop;
467
468 // Wait for the buffer to load
469 if(!buffer.isLoaded())
470 VFSManager.waitForRequests();
471
472 int start;
473
474 if(view.getBuffer() == buffer && !repeat)
475 {
476 JEditTextArea textArea = view.getTextArea();
477 Selection s = textArea.getSelectionAtOffset(
478 textArea.getCaretPosition());
479 if(s == null)
480 start = textArea.getCaretPosition();
481 else if(_reverse)
482 start = s.getStart();
483 else
484 start = s.getEnd();
485 }
486 else if(_reverse)
487 start = buffer.getLength();
488 else
489 start = 0;
490
491 if(find(view,buffer,start,repeat,_reverse))
492 return true;
493 }
494
495 if(repeat)
496 {
497 if(!BeanShell.isScriptRunning())
498 {
499 view.getStatus().setMessageAndClear(
500 jEdit.getProperty("view.status.search-not-found"));
501
502 view.getToolkit().beep();
503 }
504 return false;
505 }
506
507 boolean restart;
508
509 // if auto wrap is on, always restart search.
510 // if auto wrap is off, and we're called from
511 // a macro, stop search. If we're called
512 // interactively, ask the user what to do.
513 if(wrap)
514 {
515 if(!BeanShell.isScriptRunning())
516 {
517 view.getStatus().setMessageAndClear(
518 jEdit.getProperty("view.status.auto-wrap"));
519 // beep if beep property set
520 if(jEdit.getBooleanProperty("search.beepOnSearchAutoWrap"))
521 {
522 view.getToolkit().beep();
523 }
524 }
525 restart = true;
526 }
527 else if(BeanShell.isScriptRunning())
528 {
529 restart = false;
530 }
531 else
532 {
533 Integer[] args = { new Integer(_reverse ? 1 : 0) };
534 int result = GUIUtilities.confirm(comp,
535 "keepsearching",args,
536 JOptionPane.YES_NO_OPTION,
537 JOptionPane.QUESTION_MESSAGE);
538 restart = (result == JOptionPane.YES_OPTION);
539 }
540
541 if(restart)
542 {
543 // start search from beginning
544 path = fileset.getFirstFile(view);
545 repeat = true;
546 }
547 else
548 break loop;
549 }
550 }
551 catch(Exception e)
552 {
553 Log.log(Log.ERROR,SearchAndReplace.class,e);
554 Object[] args = { e.toString() };
555 GUIUtilities.error(comp,"searcherror",args);
556 }
557 finally
558 {
559 view.hideWaitCursor();
560 }
561
562 return false;
563 } //}}}
564
565 //{{{ find() method
566 /**
567 * Finds the next instance of the search string in the specified
568 * buffer.
569 * @param view The view
570 * @param buffer The buffer
571 * @param start Location where to start the search
572 */
573 public static boolean find(View view, Buffer buffer, int start)
574 throws Exception
575 {
576 return find(view,buffer,start,false,false);
577 } //}}}
578
579 //{{{ find() method
580 /**
581 * Finds the next instance of the search string in the specified
582 * buffer.
583 * @param view The view
584 * @param buffer The buffer
585 * @param start Location where to start the search
586 * @param firstTime See {@link SearchMatcher#nextMatch(CharIndexed,
587 * boolean,boolean,boolean,boolean)}.
588 * @since jEdit 4.1pre7
589 */
590 public static boolean find(View view, Buffer buffer, int start,
591 boolean firstTime, boolean reverse) throws Exception
592 {
593 SearchMatcher matcher = getSearchMatcher();
594 if(matcher == null)
595 {
596 view.getToolkit().beep();
597 return false;
598 }
599
600 Segment text = new Segment();
601 if(reverse)
602 buffer.getText(0,start,text);
603 else
604 buffer.getText(start,buffer.getLength() - start,text);
605
606 // the start and end flags will be wrong with reverse search enabled,
607 // but they are only used by the regexp matcher, which doesn't
608 // support reverse search yet.
609 //
610 // REMIND: fix flags when adding reverse regexp search.
611 SearchMatcher.Match match = matcher.nextMatch(new CharIndexedSegment(text,reverse),
612 start == 0,true,firstTime,reverse);
613
614 if(match != null)
615 {
616 jEdit.commitTemporary(buffer);
617 view.setBuffer(buffer);
618 JEditTextArea textArea = view.getTextArea();
619
620 if(reverse)
621 {
622 textArea.setSelection(new Selection.Range(
623 start - match.end,
624 start - match.start));
625 // make sure end of match is visible
626 textArea.scrollTo(start - match.start,false);
627 textArea.moveCaretPosition(start - match.end);
628 }
629 else
630 {
631 textArea.setSelection(new Selection.Range(
632 start + match.start,
633 start + match.end));
634 textArea.moveCaretPosition(start + match.end);
635 // make sure start of match is visible
636 textArea.scrollTo(start + match.start,false);
637 }
638
639 return true;
640 }
641 else
642 return false;
643 } //}}}
644
645 //{{{ replace() method
646 /**
647 * Replaces the current selection with the replacement string.
648 * @param view The view
649 * @return True if the operation was successful, false otherwise
650 */
651 public static boolean replace(View view)
652 {
653 // component that will parent any dialog boxes
654 Component comp = SearchDialog.getSearchDialog(view);
655 if(comp == null)
656 comp = view;
657
658 JEditTextArea textArea = view.getTextArea();
659
660 Buffer buffer = view.getBuffer();
661 if(!buffer.isEditable())
662 return false;
663
664 boolean smartCaseReplace = (replace != null
665 && TextUtilities.getStringCase(replace)
666 == TextUtilities.LOWER_CASE);
667
668 Selection[] selection = textArea.getSelection();
669 if(selection.length == 0)
670 {
671 view.getToolkit().beep();
672 return false;
673 }
674
675 record(view,"replace(view)",true,false);
676
677 // a little hack for reverse replace and find
678 int caret = textArea.getCaretPosition();
679 Selection s = textArea.getSelectionAtOffset(caret);
680 if(s != null)
681 caret = s.getStart();
682
683 try
684 {
685 buffer.beginCompoundEdit();
686
687 SearchMatcher matcher = getSearchMatcher();
688 if(matcher == null)
689 return false;
690
691 initReplace();
692
693 int retVal = 0;
694
695 for(int i = 0; i < selection.length; i++)
696 {
697 s = selection[i];
698
699 /* if an occurence occurs at the
700 beginning of the selection, the
701 selection start will get moved.
702 this sucks, so we hack to avoid it. */
703 int start = s.getStart();
704
705 if(s instanceof Selection.Range)
706 {
707 retVal += _replace(view,buffer,matcher,
708 s.getStart(),s.getEnd(),
709 smartCaseReplace);
710
711 textArea.removeFromSelection(s);
712 textArea.addToSelection(new Selection.Range(
713 start,s.getEnd()));
714 }
715 else if(s instanceof Selection.Rect)
716 {
717 Selection.Rect rect = (Selection.Rect)s;
718 int startCol = rect.getStartColumn(
719 buffer);
720 int endCol = rect.getEndColumn(
721 buffer);
722
723 for(int j = s.getStartLine(); j <= s.getEndLine(); j++)
724 {
725 retVal += _replace(view,buffer,
726 matcher,
727 getColumnOnOtherLine(buffer,j,startCol),
728 getColumnOnOtherLine(buffer,j,endCol),
729 smartCaseReplace);
730 }
731 textArea.addToSelection(new Selection.Rect(
732 start,s.getEnd()));
733 }
734 }
735
736 boolean _reverse = !regexp && reverse && fileset instanceof CurrentBufferSet;
737 if(_reverse)
738 {
739 // so that Replace and Find continues from
740 // the right location
741 textArea.moveCaretPosition(caret);
742 }
743 else
744 {
745 s = textArea.getSelectionAtOffset(
746 textArea.getCaretPosition());
747 if(s != null)
748 textArea.moveCaretPosition(s.getEnd());
749 }
750
751 if(retVal == 0)
752 {
753 view.getToolkit().beep();
754 return false;
755 }
756
757 return true;
758 }
759 catch(Exception e)
760 {
761 Log.log(Log.ERROR,SearchAndReplace.class,e);
762 Object[] args = { e.toString() };
763 GUIUtilities.error(comp,
764 beanshell ? "searcherror-bsh"
765 : "searcherror",args);
766 }
767 finally
768 {
769 buffer.endCompoundEdit();
770 }
771
772 return false;
773 } //}}}
774
775 //{{{ replace() method
776 /**
777 * Replaces text in the specified range with the replacement string.
778 * @param view The view
779 * @param buffer The buffer
780 * @param start The start offset
781 * @param end The end offset
782 * @return True if the operation was successful, false otherwise
783 */
784 public static boolean replace(View view, Buffer buffer, int start, int end)
785 {
786 if(!buffer.isEditable())
787 return false;
788
789 // component that will parent any dialog boxes
790 Component comp = SearchDialog.getSearchDialog(view);
791 if(comp == null)
792 comp = view;
793
794 boolean smartCaseReplace = (replace != null
795 && TextUtilities.getStringCase(replace)
796 == TextUtilities.LOWER_CASE);
797
798 try
799 {
800 buffer.beginCompoundEdit();
801
802 SearchMatcher matcher = getSearchMatcher();
803 if(matcher == null)
804 return false;
805
806 initReplace();
807
808 int retVal = 0;
809
810 retVal += _replace(view,buffer,matcher,start,end,
811 smartCaseReplace);
812
813 if(retVal != 0)
814 return true;
815 }
816 catch(Exception e)
817 {
818 Log.log(Log.ERROR,SearchAndReplace.class,e);
819 Object[] args = { e.toString() };
820 GUIUtilities.error(comp,
821 beanshell ? "searcherror-bsh"
822 : "searcherror",args);
823 }
824 finally
825 {
826 buffer.endCompoundEdit();
827 }
828
829 return false;
830 } //}}}
831
832 //{{{ replaceAll() method
833 /**
834 * Replaces all occurances of the search string with the replacement
835 * string.
836 * @param view The view
837 */
838 public static boolean replaceAll(View view)
839 {
840 // component that will parent any dialog boxes
841 Component comp = SearchDialog.getSearchDialog(view);
842 if(comp == null)
843 comp = view;
844
845 int fileCount = 0;
846 int occurCount = 0;
847
848 if(fileset.getFileCount(view) == 0)
849 {
850 GUIUtilities.error(comp,"empty-fileset",null);
851 return false;
852 }
853
854 record(view,"replaceAll(view)",true,true);
855
856 view.showWaitCursor();
857
858 boolean smartCaseReplace = (replace != null
859 && TextUtilities.getStringCase(replace)
860 == TextUtilities.LOWER_CASE);
861
862 try
863 {
864 SearchMatcher matcher = getSearchMatcher();
865 if(matcher == null)
866 return false;
867
868 initReplace();
869
870 String path = fileset.getFirstFile(view);
871 loop: while(path != null)
872 {
873 Buffer buffer = jEdit.openTemporary(
874 view,null,path,false);
875
876 /* this is stupid and misleading.
877 * but 'path' is not used anywhere except
878 * the above line, and if this is done
879 * after the 'continue', then we will
880 * either hang, or be forced to duplicate
881 * it inside the buffer == null, or add
882 * a 'finally' clause. you decide which one's
883 * worse. */
884 path = fileset.getNextFile(view,path);
885
886 if(buffer == null)
887 continue loop;
888
889 // Wait for buffer to finish loading
890 if(buffer.isPerformingIO())
891 VFSManager.waitForRequests();
892
893 if(!buffer.isEditable())
894 continue loop;
895
896 // Leave buffer in a consistent state if
897 // an error occurs
898 int retVal = 0;
899
900 try
901 {
902 buffer.beginCompoundEdit();
903 retVal = _replace(view,buffer,matcher,
904 0,buffer.getLength(),
905 smartCaseReplace);
906 }
907 finally
908 {
909 buffer.endCompoundEdit();
910 }
911
912 if(retVal != 0)
913 {
914 fileCount++;
915 occurCount += retVal;
916 jEdit.commitTemporary(buffer);
917 }
918 }
919 }
920 catch(Exception e)
921 {
922 Log.log(Log.ERROR,SearchAndReplace.class,e);
923 Object[] args = { e.toString() };
924 GUIUtilities.error(comp,
925 beanshell ? "searcherror-bsh"
926 : "searcherror",args);
927 }
928 finally
929 {
930 view.hideWaitCursor();
931 }
932
933 /* Don't do this when playing a macro, cos it's annoying */
934 if(!BeanShell.isScriptRunning())
935 {
936 Object[] args = { new Integer(occurCount),
937 new Integer(fileCount) };
938 view.getStatus().setMessageAndClear(jEdit.getProperty(
939 "view.status.replace-all",args));
940 if(occurCount == 0)
941 view.getToolkit().beep();
942 }
943
944 return (fileCount != 0);
945 } //}}}
946
947 //}}}
948
949 //{{{ load() method
950 /**
951 * Loads search and replace state from the properties.
952 */
953 public static void load()
954 {
955 search = jEdit.getProperty("search.find.value");
956 replace = jEdit.getProperty("search.replace.value");
957 ignoreCase = jEdit.getBooleanProperty("search.ignoreCase.toggle");
958 regexp = jEdit.getBooleanProperty("search.regexp.toggle");
959 beanshell = jEdit.getBooleanProperty("search.beanshell.toggle");
960 wrap = jEdit.getBooleanProperty("search.wrap.toggle");
961
962 fileset = new CurrentBufferSet();
963
964 // Tags plugin likes to call this method at times other than
965 // startup; so we need to fire a SearchSettingsChanged to
966 // notify the search bar and so on.
967 matcher = null;
968 EditBus.send(new SearchSettingsChanged(null));
969 } //}}}
970
971 //{{{ save() method
972 /**
973 * Saves search and replace state to the properties.
974 */
975 public static void save()
976 {
977 jEdit.setProperty("search.find.value",search);
978 jEdit.setProperty("search.replace.value",replace);
979 jEdit.setBooleanProperty("search.ignoreCase.toggle",ignoreCase);
980 jEdit.setBooleanProperty("search.regexp.toggle",regexp);
981 jEdit.setBooleanProperty("search.beanshell.toggle",beanshell);
982 jEdit.setBooleanProperty("search.wrap.toggle",wrap);
983 } //}}}
984
985 //{{{ Private members
986
987 //{{{ Instance variables
988 private static String search;
989 private static String replace;
990 private static BshMethod replaceMethod;
991 private static NameSpace replaceNS = new NameSpace(
992 BeanShell.getNameSpace(),
993 BeanShell.getNameSpace().getClassManager(),
994 "search and replace");
995 private static boolean regexp;
996 private static boolean ignoreCase;
997 private static boolean reverse;
998 private static boolean beanshell;
999 private static boolean wrap;
1000 private static SearchMatcher matcher;
1001 private static SearchFileSet fileset;
1002 //}}}
1003
1004 //{{{ initReplace() method
1005 /**
1006 * Set up BeanShell replace if necessary.
1007 */
1008 private static void initReplace() throws Exception
1009 {
1010 if(beanshell && replace.length() != 0)
1011 {
1012 replaceMethod = BeanShell.cacheBlock("replace",
1013 "return (" + replace + ");",true);
1014 }
1015 else
1016 replaceMethod = null;
1017 } //}}}
1018
1019 //{{{ record() method
1020 private static void record(View view, String action,
1021 boolean replaceAction, boolean recordFileSet)
1022 {
1023 Macros.Recorder recorder = view.getMacroRecorder();
1024
1025 if(recorder != null)
1026 {
1027 recorder.record("SearchAndReplace.setSearchString(\""
1028 + MiscUtilities.charsToEscapes(search) + "\");");
1029
1030 if(replaceAction)
1031 {
1032 recorder.record("SearchAndReplace.setReplaceString(\""
1033 + MiscUtilities.charsToEscapes(replace) + "\");");
1034 recorder.record("SearchAndReplace.setBeanShellReplace("
1035 + beanshell + ");");
1036 }
1037 else
1038 {
1039 // only record this if doing a find next
1040 recorder.record("SearchAndReplace.setAutoWrapAround("
1041 + wrap + ");");
1042 recorder.record("SearchAndReplace.setReverseSearch("
1043 + reverse + ");");
1044 }
1045
1046 recorder.record("SearchAndReplace.setIgnoreCase("
1047 + ignoreCase + ");");
1048 recorder.record("SearchAndReplace.setRegexp("
1049 + regexp + ");");
1050
1051 if(recordFileSet)
1052 {
1053 recorder.record("SearchAndReplace.setSearchFileSet("
1054 + fileset.getCode() + ");");
1055 }
1056
1057 recorder.record("SearchAndReplace." + action + ";");
1058 }
1059 } //}}}
1060
1061 //{{{ _replace() method
1062 /**
1063 * Replaces all occurances of the search string with the replacement
1064 * string.
1065 * @param view The view
1066 * @param buffer The buffer
1067 * @param start The start offset
1068 * @param end The end offset
1069 * @param matcher The search matcher to use
1070 * @param smartCaseReplace See user's guide
1071 * @return The number of occurrences replaced
1072 */
1073 private static int _replace(View view, Buffer buffer,
1074 SearchMatcher matcher, int start, int end,
1075 boolean smartCaseReplace)
1076 throws Exception
1077 {
1078 int occurCount = 0;
1079
1080 boolean endOfLine = (buffer.getLineEndOffset(
1081 buffer.getLineOfOffset(end)) - 1 == end);
1082
1083 Segment text = new Segment();
1084 int offset = start;
1085loop: for(int counter = 0; ; counter++)
1086 {
1087 buffer.getText(offset,end - offset,text);
1088
1089 boolean startOfLine = (buffer.getLineStartOffset(
1090 buffer.getLineOfOffset(offset)) == offset);
1091
1092 SearchMatcher.Match occur = matcher.nextMatch(
1093 new CharIndexedSegment(text,false),
1094 startOfLine,endOfLine,counter == 0,
1095 false);
1096 if(occur == null)
1097 break loop;
1098 int _start = occur.start;
1099 int _length = occur.end - occur.start;
1100
1101 String found = new String(text.array,text.offset + _start,_length);
1102 String subst = _replace(occur,found);
1103 if(smartCaseReplace && ignoreCase)
1104 {
1105 int strCase = TextUtilities.getStringCase(found);
1106 if(strCase == TextUtilities.LOWER_CASE)
1107 subst = subst.toLowerCase();
1108 else if(strCase == TextUtilities.UPPER_CASE)
1109 subst = subst.toUpperCase();
1110 else if(strCase == TextUtilities.TITLE_CASE)
1111 subst = TextUtilities.toTitleCase(subst);
1112 }
1113
1114 if(subst != null)
1115 {
1116 buffer.remove(offset + _start,_length);
1117 buffer.insert(offset + _start,subst);
1118 occurCount++;
1119 offset += _start + subst.length();
1120
1121 end += (subst.length() - found.length());
1122 }
1123 else
1124 offset += _start + _length;
1125 }
1126
1127 return occurCount;
1128 } //}}}
1129
1130 //{{{ _replace() method
1131 private static String _replace(SearchMatcher.Match occur, String found)
1132 throws Exception
1133 {
1134 if(regexp)
1135 {
1136 if(replaceMethod != null)
1137 {
1138 for(int i = 0; i < occur.substitutions.length; i++)
1139 {
1140 replaceNS.setVariable("_" + i,
1141 occur.substitutions[i]);
1142 }
1143
1144 Object obj = BeanShell.runCachedBlock(
1145 replaceMethod,null,replaceNS);
1146 if(obj == null)
1147 return "";
1148 else
1149 return obj.toString();
1150 }
1151 else
1152 {
1153 StringBuffer buf = new StringBuffer();
1154
1155 for(int i = 0; i < replace.length(); i++)
1156 {
1157 char ch = replace.charAt(i);
1158 switch(ch)
1159 {
1160 case '$':
1161 if(i == replace.length() - 1)
1162 {
1163 buf.append(ch);
1164 break;
1165 }
1166
1167 ch = replace.charAt(++i);
1168 if(ch == '$')
1169 buf.append('$');
1170 else if(ch == '0')
1171 buf.append(found);
1172 else if(Character.isDigit(ch))
1173 {
1174 int n = ch - '0';
1175 if(n < occur
1176 .substitutions
1177 .length)
1178 {
1179 buf.append(
1180 occur
1181 .substitutions
1182 [n]
1183 );
1184 }
1185 }
1186 break;
1187 case '\\':
1188 if(i == replace.length() - 1)
1189 {
1190 buf.append('\\');
1191 break;
1192 }
1193 ch = replace.charAt(++i);
1194 switch(ch)
1195 {
1196 case 'n':
1197 buf.append('\n');
1198 break;
1199 case 't':
1200 buf.append('\t');
1201 break;
1202 default:
1203 buf.append(ch);
1204 break;
1205 }
1206 break;
1207 default:
1208 buf.append(ch);
1209 break;
1210 }
1211 }
1212
1213 return buf.toString();
1214 }
1215 }
1216 else
1217 {
1218 if(replaceMethod != null)
1219 {
1220 replaceNS.setVariable("_0",found);
1221 Object obj = BeanShell.runCachedBlock(
1222 replaceMethod,
1223 null,replaceNS);
1224 if(obj == null)
1225 return "";
1226 else
1227 return obj.toString();
1228 }
1229 else
1230 {
1231 return replace;
1232 }
1233 }
1234 } //}}}
1235
1236 //{{{ getColumnOnOtherLine() method
1237 /**
1238 * Should be somewhere else...
1239 */
1240 private static int getColumnOnOtherLine(Buffer buffer, int line,
1241 int col)
1242 {
1243 int returnValue = buffer.getOffsetOfVirtualColumn(
1244 line,col,null);
1245 if(returnValue == -1)
1246 return buffer.getLineEndOffset(line) - 1;
1247 else
1248 return buffer.getLineStartOffset(line) + returnValue;
1249 } //}}}
1250
1251 //}}}
1252}