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