PageRenderTime 130ms CodeModel.GetById 90ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/jEdit/tags/jedit-4-2-pre14/org/gjt/sp/jedit/search/SearchAndReplace.java

#
Java | 1310 lines | 825 code | 163 blank | 322 comment | 172 complexity | 643942061f59b418874eccb18fa1f573 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.CharIndexedSegment;
  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 5008 2004-04-06 18:13:12Z spestov $
  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 RESearchMatcher(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 CharIndexedSegment(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	//{{{ load() method
 906	/**
 907	 * Loads search and replace state from the properties.
 908	 */
 909	public static void load()
 910	{
 911		search = jEdit.getProperty("search.find.value");
 912		replace = jEdit.getProperty("search.replace.value");
 913		ignoreCase = jEdit.getBooleanProperty("search.ignoreCase.toggle");
 914		regexp = jEdit.getBooleanProperty("search.regexp.toggle");
 915		beanshell = jEdit.getBooleanProperty("search.beanshell.toggle");
 916		wrap = jEdit.getBooleanProperty("search.wrap.toggle");
 917
 918		fileset = new CurrentBufferSet();
 919
 920		// Tags plugin likes to call this method at times other than
 921		// startup; so we need to fire a SearchSettingsChanged to
 922		// notify the search bar and so on.
 923		matcher = null;
 924		EditBus.send(new SearchSettingsChanged(null));
 925	} //}}}
 926
 927	//{{{ save() method
 928	/**
 929	 * Saves search and replace state to the properties.
 930	 */
 931	public static void save()
 932	{
 933		jEdit.setProperty("search.find.value",search);
 934		jEdit.setProperty("search.replace.value",replace);
 935		jEdit.setBooleanProperty("search.ignoreCase.toggle",ignoreCase);
 936		jEdit.setBooleanProperty("search.regexp.toggle",regexp);
 937		jEdit.setBooleanProperty("search.beanshell.toggle",beanshell);
 938		jEdit.setBooleanProperty("search.wrap.toggle",wrap);
 939	} //}}}
 940
 941	//{{{ handleError() method
 942	static void handleError(Component comp, Exception e)
 943	{
 944		Log.log(Log.ERROR,SearchAndReplace.class,e);
 945		if(comp instanceof Dialog)
 946		{
 947			new TextAreaDialog((Dialog)comp,
 948				beanshell ? "searcherror-bsh"
 949				: "searcherror",e);
 950		}
 951		else
 952		{
 953			new TextAreaDialog((Frame)comp,
 954				beanshell ? "searcherror-bsh"
 955				: "searcherror",e);
 956		}
 957	} //}}}
 958
 959	//{{{ Private members
 960
 961	//{{{ Instance variables
 962	private static String search;
 963	private static String replace;
 964	private static BshMethod replaceMethod;
 965	private static NameSpace replaceNS = new NameSpace(
 966		BeanShell.getNameSpace(),
 967		BeanShell.getNameSpace().getClassManager(),
 968		"search and replace");
 969	private static boolean regexp;
 970	private static boolean ignoreCase;
 971	private static boolean reverse;
 972	private static boolean beanshell;
 973	private static boolean wrap;
 974	private static SearchMatcher matcher;
 975	private static SearchFileSet fileset;
 976	//}}}
 977
 978	//{{{ initReplace() method
 979	/**
 980	 * Set up BeanShell replace if necessary.
 981	 */
 982	private static void initReplace() throws Exception
 983	{
 984		if(beanshell && replace.length() != 0)
 985		{
 986			replaceMethod = BeanShell.cacheBlock("replace",
 987				"return (" + replace + ");",true);
 988		}
 989		else
 990			replaceMethod = null;
 991	} //}}}
 992
 993	//{{{ record() method
 994	private static void record(View view, String action,
 995		boolean replaceAction, boolean recordFileSet)
 996	{
 997		Macros.Recorder recorder = view.getMacroRecorder();
 998
 999		if(recorder != null)
1000		{
1001			recorder.record("SearchAndReplace.setSearchString(\""
1002				+ MiscUtilities.charsToEscapes(search) + "\");");
1003
1004			if(replaceAction)
1005			{
1006				recorder.record("SearchAndReplace.setReplaceString(\""
1007					+ MiscUtilities.charsToEscapes(replace) + "\");");
1008				recorder.record("SearchAndReplace.setBeanShellReplace("
1009					+ beanshell + ");");
1010			}
1011			else
1012			{
1013				// only record this if doing a find next
1014				recorder.record("SearchAndReplace.setAutoWrapAround("
1015					+ wrap + ");");
1016				recorder.record("SearchAndReplace.setReverseSearch("
1017					+ reverse + ");");
1018			}
1019
1020			recorder.record("SearchAndReplace.setIgnoreCase("
1021				+ ignoreCase + ");");
1022			recorder.record("SearchAndReplace.setRegexp("
1023				+ regexp + ");");
1024
1025			if(recordFileSet)
1026			{
1027				recorder.record("SearchAndReplace.setSearchFileSet("
1028					+ fileset.getCode() + ");");
1029			}
1030
1031			recorder.record("SearchAndReplace." + action + ";");
1032		}
1033	} //}}}
1034
1035	//{{{ replaceInSelection() method
1036	private static int replaceInSelection(View view, JEditTextArea textArea,
1037		Buffer buffer, SearchMatcher matcher, boolean smartCaseReplace,
1038		Selection s) throws Exception
1039	{
1040		/* if an occurence occurs at the
1041		beginning of the selection, the
1042		selection start will get moved.
1043		this sucks, so we hack to avoid it. */
1044		int start = s.getStart();
1045
1046		int returnValue;
1047
1048		if(s instanceof Selection.Range)
1049		{
1050			returnValue = _replace(view,buffer,matcher,
1051				s.getStart(),s.getEnd(),
1052				smartCaseReplace);
1053
1054			textArea.removeFromSelection(s);
1055			textArea.addToSelection(new Selection.Range(
1056				start,s.getEnd()));
1057		}
1058		else if(s instanceof Selection.Rect)
1059		{
1060			Selection.Rect rect = (Selection.Rect)s;
1061			int startCol = rect.getStartColumn(
1062				buffer);
1063			int endCol = rect.getEndColumn(
1064				buffer);
1065
1066			returnValue = 0;
1067			for(int j = s.getStartLine(); j <= s.getEndLine(); j++)
1068			{
1069				returnValue += _replace(view,buffer,
1070					matcher,
1071					getColumnOnOtherLine(buffer,j,startCol),
1072					getColumnOnOtherLine(buffer,j,endCol),
1073					smartCaseReplace);
1074			}
1075			textArea.addToSelection(new Selection.Rect(
1076				start,s.getEnd()));
1077		}
1078		else
1079			throw new RuntimeException("Unsupported: " + s);
1080
1081		return returnValue;
1082	} //}}}
1083
1084	//{{{ _replace() method
1085	/**
1086	 * Replaces all occurances of the search string with the replacement
1087	 * string.
1088	 * @param view The view
1089	 * @param buffer The buffer
1090	 * @param start The start offset
1091	 * @param end The end offset
1092	 * @param matcher The search matcher to use
1093	 * @param smartCaseReplace See user's guide
1094	 * @return The number of occurrences replaced
1095	 */
1096	private static int _replace(View view, Buffer buffer,
1097		SearchMatcher matcher, int start, int end,
1098		boolean smartCaseReplace)
1099		throws Exception
1100	{
1101		int occurCount = 0;
1102
1103		boolean endOfLine = (buffer.getLineEndOffset(
1104			buffer.getLineOfOffset(end)) - 1 == end);
1105
1106		Segment text = new Segment();
1107		int offset = start;
1108loop:		for(int counter = 0; ; counter++)
1109		{
1110			buffer.getText(offset,end - offset,text);
1111
1112			boolean startOfLine = (buffer.getLineStartOffset(
1113				buffer.getLineOfOffset(offset)) == offset);
1114
1115			SearchMatcher.Match occur = matcher.nextMatch(
1116				new CharIndexedSegment(text,false),
1117				startOfLine,endOfLine,counter == 0,
1118				false);
1119			if(occur == null)
1120				break loop;
1121
1122			String found = new String(text.array,
1123				text.offset + occur.start,
1124				occur.end - occur.start);
1125
1126			int length = replaceOne(buffer,occur,offset,found,
1127				smartCaseReplace);
1128			if(length == -1)
1129				offset += occur.end;
1130			else
1131			{
1132				offset += occur.start + length;
1133				end += (length - found.length());
1134				occurCount++;
1135			}
1136		}
1137
1138		return occurCount;
1139	} //}}}
1140
1141	//{{{ replaceOne() method
1142	/**
1143	 * Replace one occurrence of the search string with the
1144	 * replacement string.
1145	 */
1146	private static int replaceOne(Buffer buffer, SearchMatcher.Match occur,
1147		int offset, String found, boolean smartCaseReplace)
1148		throws Exception
1149	{
1150		String subst = replaceOne(occur,found);
1151		if(smartCaseReplace && ignoreCase)
1152		{
1153			int strCase = TextUtilities.getStringCase(found);
1154			if(strCase == TextUtilities.LOWER_CASE)
1155				subst = subst.toLowerCase();
1156			else if(strCase == TextUtilities.UPPER_CASE)
1157				subst = subst.toUpperCase();
1158			else if(strCase == TextUtilities.TITLE_CASE)
1159				subst = TextUtilities.toTitleCase(subst);
1160		}
1161
1162		if(subst != null)
1163		{
1164			int start = offset + occur.start;
1165			int end = offset + occur.end;
1166
1167			buffer.remove(start,end - start);
1168			buffer.insert(start,subst);
1169			return subst.length();
1170		}
1171		else
1172			return -1;
1173	} //}}}
1174
1175	//{{{ replaceOne() method
1176	private static String replaceOne(SearchMatcher.Match occur,
1177		String found) throws Exception
1178	{
1179		if(regexp)
1180		{
1181			if(replaceMethod != null)
1182				return regexpBeanShellReplace(occur,found);
1183			else
1184				return regexpReplace(occur,found);
1185		}
1186		else
1187		{
1188			if(replaceMethod != null)
1189				return literalBeanShellReplace(occur,found);
1190			else
1191				return replace;
1192		}
1193	} //}}}
1194
1195	//{{{ regexpBeanShellReplace() method
1196	private static String regexpBeanShellReplace(SearchMatcher.Match occur,
1197		String found) throws Exception
1198	{
1199		for(int i = 0; i < occur.substitutions.length; i++)
1200		{
1201			replaceNS.setVariable("_" + i,
1202				occur.substitutions[i]);
1203		}
1204
1205		Object obj = BeanShell.runCachedBlock(
1206			replaceMethod,null,replaceNS);
1207		if(obj == null)
1208			return "";
1209		else
1210			return obj.toString();
1211	} //}}}
1212
1213	//{{{ regexpReplace() method
1214	private static String regexpReplace(SearchMatcher.Match occur,
1215		String found) throws Exception
1216	{
1217		StringBuffer buf = new StringBuffer();
1218
1219		for(int i = 0; i < replace.length(); i++)
1220		{
1221			char ch = replace.charAt(i);
1222			switch(ch)
1223			{
1224			case '$':
1225				if(i == replace.length() - 1)
1226				{
1227					buf.append(ch);
1228					break;
1229				}
1230
1231				ch = replace.charAt(++i);
1232				if(ch == '$')
1233					buf.append('$');
1234				else if(ch == '0')
1235					buf.append(found);
1236				else if(Character.isDigit(ch))
1237				{
1238					int n = ch - '0';
1239					if(n < occur
1240						.substitutions
1241						.length)
1242					{
1243						buf.append(
1244							occur
1245							.substitutions
1246							[n]
1247						);
1248					}
1249				}
1250				break;
1251			case '\\':
1252				if(i == replace.length() - 1)
1253				{
1254					buf.append('\\');
1255					break;
1256				}
1257				ch = replace.charAt(++i);
1258				switch(ch)
1259				{
1260				case 'n':
1261					buf.append('\n');
1262					break;
1263				case 't':
1264					buf.append('\t');
1265					break;
1266				default:
1267					buf.append(ch);
1268					break;
1269				}
1270				break;
1271			default:
1272				buf.append(ch);
1273				break;
1274			}
1275		}
1276
1277		return buf.toString();
1278	} //}}}
1279
1280	//{{{ literalBeanShellReplace() method
1281	private static String literalBeanShellReplace(SearchMatcher.Match occur,
1282		String found) throws Exception
1283	{
1284		replaceNS.setVariable("_0",found);
1285		Object obj = BeanShell.runCachedBlock(
1286			replaceMethod,
1287			null,replaceNS);
1288		if(obj == null)
1289			return "";
1290		else
1291			return obj.toString();
1292	} //}}}
1293
1294	//{{{ getColumnOnOtherLine() method
1295	/**
1296	 * Should be somewhere else...
1297	 */
1298	private static int getColumnOnOtherLine(Buffer buffer, int line,
1299		int col)
1300	{
1301		int returnValue = buffer.getOffsetOfVirtualColumn(
1302			line,col,null);
1303		if(returnValue == -1)
1304			return buffer.getLineEndOffset(line) - 1;
1305		else
1306			return buffer.getLineStartOffset(line) + returnValue;
1307	} //}}}
1308
1309	//}}}
1310}