/jEdit/tags/jedit-4-2-pre4/org/gjt/sp/jedit/search/SearchAndReplace.java
Java | 1218 lines | 769 code | 148 blank | 301 comment | 179 complexity | 1e8f6009647d4d4ffd8c62a2d7bb7bfc MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, Apache-2.0, LGPL-2.0, LGPL-3.0, GPL-2.0, CC-BY-SA-3.0, LGPL-2.1, GPL-3.0, MPL-2.0-no-copyleft-exception, IPL-1.0
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
24package org.gjt.sp.jedit.search;
25
26//{{{ Imports
27import bsh.*;
28import java.awt.Component;
29import javax.swing.JOptionPane;
30import javax.swing.text.Segment;
31import org.gjt.sp.jedit.*;
32import org.gjt.sp.jedit.io.VFSManager;
33import org.gjt.sp.jedit.msg.SearchSettingsChanged;
34import org.gjt.sp.jedit.textarea.*;
35import org.gjt.sp.util.CharIndexedSegment;
36import 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 4840 2003-07-28 18:54:13Z spestov $
64 */
65public 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(beanshell && replace.length() != 0)
295 {
296 replaceMethod = BeanShell.cacheBlock("replace","return ("
297 + replace + ");",true);
298 }
299 else
300 replaceMethod = null;
301
302 if(regexp)
303 matcher = new RESearchMatcher(search,ignoreCase);
304 else
305 {
306 matcher = new BoyerMooreSearchMatcher(search,ignoreCase);
307 }
308
309 return matcher;
310 } //}}}
311
312 //{{{ setSearchFileSet() method
313 /**
314 * Sets the current search file set.
315 * @param fileset The file set to perform searches in
316 * @see AllBufferSet
317 * @see CurrentBufferSet
318 * @see DirectoryListSet
319 */
320 public static void setSearchFileSet(SearchFileSet fileset)
321 {
322 SearchAndReplace.fileset = fileset;
323
324 EditBus.send(new SearchSettingsChanged(null));
325 } //}}}
326
327 //{{{ getSearchFileSet() method
328 /**
329 * Returns the current search file set.
330 */
331 public static SearchFileSet getSearchFileSet()
332 {
333 return fileset;
334 } //}}}
335
336 //}}}
337
338 //{{{ Actions
339
340 //{{{ hyperSearch() method
341 /**
342 * Performs a HyperSearch.
343 * @param view The view
344 * @since jEdit 2.7pre3
345 */
346 public static boolean hyperSearch(View view)
347 {
348 return hyperSearch(view,false);
349 } //}}}
350
351 //{{{ hyperSearch() method
352 /**
353 * Performs a HyperSearch.
354 * @param view The view
355 * @param selection If true, will only search in the current selection.
356 * Note that the file set must be the current buffer file set for this
357 * to work.
358 * @since jEdit 4.0pre1
359 */
360 public static boolean hyperSearch(View view, boolean selection)
361 {
362 // component that will parent any dialog boxes
363 Component comp = SearchDialog.getSearchDialog(view);
364 if(comp == null)
365 comp = view;
366
367 record(view,"hyperSearch(view," + selection + ")",false,
368 !selection);
369
370 view.getDockableWindowManager().addDockableWindow(
371 HyperSearchResults.NAME);
372 final HyperSearchResults results = (HyperSearchResults)
373 view.getDockableWindowManager()
374 .getDockable(HyperSearchResults.NAME);
375 results.searchStarted();
376
377 try
378 {
379 SearchMatcher matcher = getSearchMatcher();
380 if(matcher == null)
381 {
382 view.getToolkit().beep();
383 results.searchFailed();
384 return false;
385 }
386
387 Selection[] s;
388 if(selection)
389 {
390 s = view.getTextArea().getSelection();
391 if(s == null)
392 {
393 results.searchFailed();
394 return false;
395 }
396 }
397 else
398 s = null;
399 VFSManager.runInWorkThread(new HyperSearchRequest(view,
400 matcher,results,s));
401 return true;
402 }
403 catch(Exception e)
404 {
405 results.searchFailed();
406 Log.log(Log.ERROR,SearchAndReplace.class,e);
407 Object[] args = { e.getMessage() };
408 if(args[0] == null)
409 args[0] = e.toString();
410 GUIUtilities.error(comp,"searcherror",args);
411 return false;
412 }
413 } //}}}
414
415 //{{{ find() method
416 /**
417 * Finds the next occurance of the search string.
418 * @param view The view
419 * @return True if the operation was successful, false otherwise
420 */
421 public static boolean find(View view)
422 {
423 // component that will parent any dialog boxes
424 Component comp = SearchDialog.getSearchDialog(view);
425 if(comp == null)
426 comp = view;
427
428 boolean repeat = false;
429 String path = fileset.getNextFile(view,null);
430 if(path == null)
431 {
432 GUIUtilities.error(comp,"empty-fileset",null);
433 return false;
434 }
435
436 boolean _reverse = reverse && fileset instanceof CurrentBufferSet;
437 if(_reverse && regexp)
438 {
439 GUIUtilities.error(comp,"regexp-reverse",null);
440 return false;
441 }
442
443 try
444 {
445 view.showWaitCursor();
446
447 SearchMatcher matcher = getSearchMatcher();
448 if(matcher == null)
449 {
450 view.getToolkit().beep();
451 return false;
452 }
453
454 record(view,"find(view)",false,true);
455
456loop: for(;;)
457 {
458 while(path != null)
459 {
460 Buffer buffer = jEdit.openTemporary(
461 view,null,path,false);
462
463 /* this is stupid and misleading.
464 * but 'path' is not used anywhere except
465 * the above line, and if this is done
466 * after the 'continue', then we will
467 * either hang, or be forced to duplicate
468 * it inside the buffer == null, or add
469 * a 'finally' clause. you decide which one's
470 * worse. */
471 path = fileset.getNextFile(view,path);
472
473 if(buffer == null)
474 continue loop;
475
476 // Wait for the buffer to load
477 if(!buffer.isLoaded())
478 VFSManager.waitForRequests();
479
480 int start;
481
482 if(view.getBuffer() == buffer && !repeat)
483 {
484 JEditTextArea textArea = view.getTextArea();
485 Selection s = textArea.getSelectionAtOffset(
486 textArea.getCaretPosition());
487 if(s == null)
488 start = textArea.getCaretPosition();
489 else if(_reverse)
490 start = s.getStart();
491 else
492 start = s.getEnd();
493 }
494 else if(_reverse)
495 start = buffer.getLength();
496 else
497 start = 0;
498
499 if(find(view,buffer,start,repeat,_reverse))
500 return true;
501 }
502
503 if(repeat)
504 {
505 if(!BeanShell.isScriptRunning())
506 {
507 view.getStatus().setMessageAndClear(
508 jEdit.getProperty("view.status.search-not-found"));
509
510 view.getToolkit().beep();
511 }
512 return false;
513 }
514
515 boolean restart;
516
517 // if auto wrap is on, always restart search.
518 // if auto wrap is off, and we're called from
519 // a macro, stop search. If we're called
520 // interactively, ask the user what to do.
521 if(wrap)
522 {
523 if(!BeanShell.isScriptRunning())
524 {
525 view.getStatus().setMessageAndClear(
526 jEdit.getProperty("view.status.auto-wrap"));
527 // beep if beep property set
528 if(jEdit.getBooleanProperty("search.beepOnSearchAutoWrap"))
529 {
530 view.getToolkit().beep();
531 }
532 }
533 restart = true;
534 }
535 else if(BeanShell.isScriptRunning())
536 {
537 restart = false;
538 }
539 else
540 {
541 Integer[] args = { new Integer(_reverse ? 1 : 0) };
542 int result = GUIUtilities.confirm(comp,
543 "keepsearching",args,
544 JOptionPane.YES_NO_OPTION,
545 JOptionPane.QUESTION_MESSAGE);
546 restart = (result == JOptionPane.YES_OPTION);
547 }
548
549 if(restart)
550 {
551 // start search from beginning
552 path = fileset.getFirstFile(view);
553 repeat = true;
554 }
555 else
556 break loop;
557 }
558 }
559 catch(Exception e)
560 {
561 Log.log(Log.ERROR,SearchAndReplace.class,e);
562 Object[] args = { e.getMessage() };
563 if(args[0] == null)
564 args[0] = e.toString();
565 GUIUtilities.error(comp,"searcherror",args);
566 }
567 finally
568 {
569 view.hideWaitCursor();
570 }
571
572 return false;
573 } //}}}
574
575 //{{{ find() method
576 /**
577 * Finds the next instance of the search string in the specified
578 * buffer.
579 * @param view The view
580 * @param buffer The buffer
581 * @param start Location where to start the search
582 */
583 public static boolean find(View view, Buffer buffer, int start)
584 throws Exception
585 {
586 return find(view,buffer,start,false,false);
587 } //}}}
588
589 //{{{ find() method
590 /**
591 * Finds the next instance of the search string in the specified
592 * buffer.
593 * @param view The view
594 * @param buffer The buffer
595 * @param start Location where to start the search
596 * @param firstTime See {@link SearchMatcher#nextMatch(CharIndexed,
597 * boolean,boolean,boolean,boolean)}.
598 * @since jEdit 4.1pre7
599 */
600 public static boolean find(View view, Buffer buffer, int start,
601 boolean firstTime, boolean reverse) throws Exception
602 {
603 SearchMatcher matcher = getSearchMatcher();
604 if(matcher == null)
605 {
606 view.getToolkit().beep();
607 return false;
608 }
609
610 Segment text = new Segment();
611 if(reverse)
612 buffer.getText(0,start,text);
613 else
614 buffer.getText(start,buffer.getLength() - start,text);
615
616 // the start and end flags will be wrong with reverse search enabled,
617 // but they are only used by the regexp matcher, which doesn't
618 // support reverse search yet.
619 //
620 // REMIND: fix flags when adding reverse regexp search.
621 SearchMatcher.Match match = matcher.nextMatch(new CharIndexedSegment(text,reverse),
622 start == 0,true,firstTime,reverse);
623
624 if(match != null)
625 {
626 jEdit.commitTemporary(buffer);
627 view.setBuffer(buffer);
628 JEditTextArea textArea = view.getTextArea();
629
630 if(reverse)
631 {
632 textArea.setSelection(new Selection.Range(
633 start - match.end,
634 start - match.start));
635 // make sure end of match is visible
636 textArea.scrollTo(start - match.start,false);
637 textArea.moveCaretPosition(start - match.end);
638 }
639 else
640 {
641 textArea.setSelection(new Selection.Range(
642 start + match.start,
643 start + match.end));
644 textArea.moveCaretPosition(start + match.end);
645 // make sure start of match is visible
646 textArea.scrollTo(start + match.start,false);
647 }
648
649 return true;
650 }
651 else
652 return false;
653 } //}}}
654
655 //{{{ replace() method
656 /**
657 * Replaces the current selection with the replacement string.
658 * @param view The view
659 * @return True if the operation was successful, false otherwise
660 */
661 public static boolean replace(View view)
662 {
663 // component that will parent any dialog boxes
664 Component comp = SearchDialog.getSearchDialog(view);
665 if(comp == null)
666 comp = view;
667
668 JEditTextArea textArea = view.getTextArea();
669
670 Buffer buffer = view.getBuffer();
671 if(!buffer.isEditable())
672 return false;
673
674 boolean smartCaseReplace = (replace != null
675 && TextUtilities.getStringCase(replace)
676 == TextUtilities.LOWER_CASE);
677
678 Selection[] selection = textArea.getSelection();
679 if(selection.length == 0)
680 {
681 view.getToolkit().beep();
682 return false;
683 }
684
685 record(view,"replace(view)",true,false);
686
687 // a little hack for reverse replace and find
688 int caret = textArea.getCaretPosition();
689 Selection s = textArea.getSelectionAtOffset(caret);
690 if(s != null)
691 caret = s.getStart();
692
693 try
694 {
695 buffer.beginCompoundEdit();
696
697 SearchMatcher matcher = getSearchMatcher();
698 if(matcher == null)
699 return false;
700
701 int retVal = 0;
702
703 for(int i = 0; i < selection.length; i++)
704 {
705 s = selection[i];
706
707 /* if an occurence occurs at the
708 beginning of the selection, the
709 selection start will get moved.
710 this sucks, so we hack to avoid it. */
711 int start = s.getStart();
712
713 if(s instanceof Selection.Range)
714 {
715 retVal += _replace(view,buffer,matcher,
716 s.getStart(),s.getEnd(),
717 smartCaseReplace);
718
719 textArea.removeFromSelection(s);
720 textArea.addToSelection(new Selection.Range(
721 start,s.getEnd()));
722 }
723 else if(s instanceof Selection.Rect)
724 {
725 for(int j = s.getStartLine(); j <= s.getEndLine(); j++)
726 {
727 retVal += _replace(view,buffer,matcher,
728 s.getStart(buffer,j),s.getEnd(buffer,j),
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.getMessage() };
763 if(args[0] == null)
764 args[0] = e.toString();
765 GUIUtilities.error(comp,"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 int retVal = 0;
807
808 retVal += _replace(view,buffer,matcher,start,end,
809 smartCaseReplace);
810
811 if(retVal != 0)
812 return true;
813 }
814 catch(Exception e)
815 {
816 Log.log(Log.ERROR,SearchAndReplace.class,e);
817 Object[] args = { e.getMessage() };
818 if(args[0] == null)
819 args[0] = e.toString();
820 GUIUtilities.error(comp,"searcherror",args);
821 }
822 finally
823 {
824 buffer.endCompoundEdit();
825 }
826
827 return false;
828 } //}}}
829
830 //{{{ replaceAll() method
831 /**
832 * Replaces all occurances of the search string with the replacement
833 * string.
834 * @param view The view
835 */
836 public static boolean replaceAll(View view)
837 {
838 // component that will parent any dialog boxes
839 Component comp = SearchDialog.getSearchDialog(view);
840 if(comp == null)
841 comp = view;
842
843 int fileCount = 0;
844 int occurCount = 0;
845
846 if(fileset.getFileCount(view) == 0)
847 {
848 GUIUtilities.error(comp,"empty-fileset",null);
849 return false;
850 }
851
852 record(view,"replaceAll(view)",true,true);
853
854 view.showWaitCursor();
855
856 boolean smartCaseReplace = (replace != null
857 && TextUtilities.getStringCase(replace)
858 == TextUtilities.LOWER_CASE);
859
860 try
861 {
862 SearchMatcher matcher = getSearchMatcher();
863 if(matcher == null)
864 return false;
865
866 String path = fileset.getFirstFile(view);
867loop: while(path != null)
868 {
869 Buffer buffer = jEdit.openTemporary(
870 view,null,path,false);
871
872 /* this is stupid and misleading.
873 * but 'path' is not used anywhere except
874 * the above line, and if this is done
875 * after the 'continue', then we will
876 * either hang, or be forced to duplicate
877 * it inside the buffer == null, or add
878 * a 'finally' clause. you decide which one's
879 * worse. */
880 path = fileset.getNextFile(view,path);
881
882 if(buffer == null)
883 continue loop;
884
885 // Wait for buffer to finish loading
886 if(buffer.isPerformingIO())
887 VFSManager.waitForRequests();
888
889 if(!buffer.isEditable())
890 continue loop;
891
892 // Leave buffer in a consistent state if
893 // an error occurs
894 int retVal = 0;
895
896 try
897 {
898 buffer.beginCompoundEdit();
899 retVal = _replace(view,buffer,matcher,
900 0,buffer.getLength(),
901 smartCaseReplace);
902 }
903 finally
904 {
905 buffer.endCompoundEdit();
906 }
907
908 if(retVal != 0)
909 {
910 fileCount++;
911 occurCount += retVal;
912 jEdit.commitTemporary(buffer);
913 }
914 }
915 }
916 catch(Exception e)
917 {
918 Log.log(Log.ERROR,SearchAndReplace.class,e);
919 Object[] args = { e.getMessage() };
920 if(args[0] == null)
921 args[0] = e.toString();
922 GUIUtilities.error(comp,"searcherror",args);
923 }
924 finally
925 {
926 view.hideWaitCursor();
927 }
928
929 /* Don't do this when playing a macro, cos it's annoying */
930 if(!BeanShell.isScriptRunning())
931 {
932 Object[] args = { new Integer(occurCount),
933 new Integer(fileCount) };
934 view.getStatus().setMessageAndClear(jEdit.getProperty(
935 "view.status.replace-all",args));
936 if(occurCount == 0)
937 view.getToolkit().beep();
938 }
939
940 return (fileCount != 0);
941 } //}}}
942
943 //}}}
944
945 //{{{ load() method
946 /**
947 * Loads search and replace state from the properties.
948 */
949 public static void load()
950 {
951 search = jEdit.getProperty("search.find.value");
952 replace = jEdit.getProperty("search.replace.value");
953 ignoreCase = jEdit.getBooleanProperty("search.ignoreCase.toggle");
954 regexp = jEdit.getBooleanProperty("search.regexp.toggle");
955 beanshell = jEdit.getBooleanProperty("search.beanshell.toggle");
956 wrap = jEdit.getBooleanProperty("search.wrap.toggle");
957
958 fileset = new CurrentBufferSet();
959
960 // Tags plugin likes to call this method at times other than
961 // startup; so we need to fire a SearchSettingsChanged to
962 // notify the search bar and so on.
963 matcher = null;
964 EditBus.send(new SearchSettingsChanged(null));
965 } //}}}
966
967 //{{{ save() method
968 /**
969 * Saves search and replace state to the properties.
970 */
971 public static void save()
972 {
973 jEdit.setProperty("search.find.value",search);
974 jEdit.setProperty("search.replace.value",replace);
975 jEdit.setBooleanProperty("search.ignoreCase.toggle",ignoreCase);
976 jEdit.setBooleanProperty("search.regexp.toggle",regexp);
977 jEdit.setBooleanProperty("search.beanshell.toggle",beanshell);
978 jEdit.setBooleanProperty("search.wrap.toggle",wrap);
979 } //}}}
980
981 //{{{ Private members
982
983 //{{{ Instance variables
984 private static String search;
985 private static String replace;
986 private static BshMethod replaceMethod;
987 private static NameSpace replaceNS = new NameSpace(
988 BeanShell.getNameSpace(),
989 BeanShell.getNameSpace().getClassManager(),
990 "search and replace");
991 private static boolean regexp;
992 private static boolean ignoreCase;
993 private static boolean reverse;
994 private static boolean beanshell;
995 private static boolean wrap;
996 private static SearchMatcher matcher;
997 private static SearchFileSet fileset;
998 //}}}
999
1000 //{{{ record() method
1001 private static void record(View view, String action,
1002 boolean replaceAction, boolean recordFileSet)
1003 {
1004 Macros.Recorder recorder = view.getMacroRecorder();
1005
1006 if(recorder != null)
1007 {
1008 recorder.record("SearchAndReplace.setSearchString(\""
1009 + MiscUtilities.charsToEscapes(search) + "\");");
1010
1011 if(replaceAction)
1012 {
1013 recorder.record("SearchAndReplace.setReplaceString(\""
1014 + MiscUtilities.charsToEscapes(replace) + "\");");
1015 recorder.record("SearchAndReplace.setBeanShellReplace("
1016 + beanshell + ");");
1017 }
1018 else
1019 {
1020 // only record this if doing a find next
1021 recorder.record("SearchAndReplace.setAutoWrapAround("
1022 + wrap + ");");
1023 recorder.record("SearchAndReplace.setReverseSearch("
1024 + reverse + ");");
1025 }
1026
1027 recorder.record("SearchAndReplace.setIgnoreCase("
1028 + ignoreCase + ");");
1029 recorder.record("SearchAndReplace.setRegexp("
1030 + regexp + ");");
1031
1032 if(recordFileSet)
1033 {
1034 recorder.record("SearchAndReplace.setSearchFileSet("
1035 + fileset.getCode() + ");");
1036 }
1037
1038 recorder.record("SearchAndReplace." + action + ";");
1039 }
1040 } //}}}
1041
1042 //{{{ _replace() method
1043 /**
1044 * Replaces all occurances of the search string with the replacement
1045 * string.
1046 * @param view The view
1047 * @param buffer The buffer
1048 * @param start The start offset
1049 * @param end The end offset
1050 * @param matcher The search matcher to use
1051 * @param smartCaseReplace See user's guide
1052 * @return The number of occurrences replaced
1053 */
1054 private static int _replace(View view, Buffer buffer,
1055 SearchMatcher matcher, int start, int end,
1056 boolean smartCaseReplace)
1057 throws Exception
1058 {
1059 int occurCount = 0;
1060
1061 boolean endOfLine = (buffer.getLineEndOffset(
1062 buffer.getLineOfOffset(end)) - 1 == end);
1063
1064 Segment text = new Segment();
1065 int offset = start;
1066loop: for(int counter = 0; ; counter++)
1067 {
1068 buffer.getText(offset,end - offset,text);
1069
1070 boolean startOfLine = (buffer.getLineStartOffset(
1071 buffer.getLineOfOffset(offset)) == offset);
1072
1073 SearchMatcher.Match occur = matcher.nextMatch(
1074 new CharIndexedSegment(text,false),
1075 startOfLine,endOfLine,counter == 0,
1076 false);
1077 if(occur == null)
1078 break loop;
1079 int _start = occur.start;
1080 int _length = occur.end - occur.start;
1081
1082 String found = new String(text.array,text.offset + _start,_length);
1083 String subst = _replace(occur,found);
1084 if(smartCaseReplace && ignoreCase)
1085 {
1086 int strCase = TextUtilities.getStringCase(found);
1087 if(strCase == TextUtilities.LOWER_CASE)
1088 subst = subst.toLowerCase();
1089 else if(strCase == TextUtilities.UPPER_CASE)
1090 subst = subst.toUpperCase();
1091 else if(strCase == TextUtilities.TITLE_CASE)
1092 subst = TextUtilities.toTitleCase(subst);
1093 }
1094
1095 if(subst != null)
1096 {
1097 buffer.remove(offset + _start,_length);
1098 buffer.insert(offset + _start,subst);
1099 occurCount++;
1100 offset += _start + subst.length();
1101
1102 end += (subst.length() - found.length());
1103 }
1104 else
1105 offset += _start + _length;
1106 }
1107
1108 return occurCount;
1109 } //}}}
1110
1111 //{{{ _replace() method
1112 private static String _replace(SearchMatcher.Match occur, String found)
1113 throws Exception
1114 {
1115 if(regexp)
1116 {
1117 if(replaceMethod != null)
1118 {
1119 for(int i = 0; i <= occur.substitutions.length; i++)
1120 {
1121 replaceNS.setVariable("_" + i,
1122 occur.substitutions[i]);
1123 }
1124
1125 Object obj = BeanShell.runCachedBlock(
1126 replaceMethod,null,replaceNS);
1127 if(obj == null)
1128 return "";
1129 else
1130 return obj.toString();
1131 }
1132 else
1133 {
1134 StringBuffer buf = new StringBuffer();
1135
1136 for(int i = 0; i < replace.length(); i++)
1137 {
1138 char ch = replace.charAt(i);
1139 switch(ch)
1140 {
1141 case '$':
1142 if(i == replace.length() - 1)
1143 {
1144 buf.append(ch);
1145 break;
1146 }
1147
1148 ch = replace.charAt(++i);
1149 if(ch == '$')
1150 buf.append('$');
1151 else if(ch == '0')
1152 buf.append(found);
1153 else if(Character.isDigit(ch))
1154 {
1155 int n = ch - '0';
1156 if(n < occur
1157 .substitutions
1158 .length)
1159 {
1160 buf.append(
1161 occur
1162 .substitutions
1163 [n]
1164 );
1165 }
1166 }
1167 break;
1168 case '\\':
1169 if(i == replace.length() - 1)
1170 {
1171 buf.append('\\');
1172 break;
1173 }
1174 ch = replace.charAt(++i);
1175 switch(ch)
1176 {
1177 case 'n':
1178 buf.append('\n');
1179 break;
1180 case 't':
1181 buf.append('\t');
1182 break;
1183 default:
1184 buf.append(ch);
1185 break;
1186 }
1187 break;
1188 default:
1189 buf.append(ch);
1190 break;
1191 }
1192 }
1193
1194 return buf.toString();
1195 }
1196 }
1197 else
1198 {
1199 if(replaceMethod != null)
1200 {
1201 replaceNS.setVariable("_0",found);
1202 Object obj = BeanShell.runCachedBlock(
1203 replaceMethod,
1204 null,replaceNS);
1205 if(obj == null)
1206 return "";
1207 else
1208 return obj.toString();
1209 }
1210 else
1211 {
1212 return replace;
1213 }
1214 }
1215 } //}}}
1216
1217 //}}}
1218}