PageRenderTime 105ms CodeModel.GetById 61ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/jEdit/tags/jedit-4-3-pre5/org/gjt/sp/jedit/search/SearchAndReplace.java

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