PageRenderTime 49ms CodeModel.GetById 16ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/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}