PageRenderTime 65ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

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

#
Java | 1324 lines | 832 code | 164 blank | 328 comment | 172 complexity | 6438106ee537a600fea7d97624071d4d MD5 | raw file
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.SegmentCharSequence;
  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 5443 2006-06-18 18:51:40Z vanza $
  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 PatternSearchMatcher(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 SegmentCharSequence(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. //{{{ escapeRegexp() method
  783. /**
  784. * Escapes characters with special meaning in a regexp.
  785. * @param multiline Should \n be escaped?
  786. * @since jEdit 4.3pre1
  787. */
  788. public static String escapeRegexp(String str, boolean multiline)
  789. {
  790. return MiscUtilities.charsToEscapes(str,
  791. "\r\t\\()[]{}$^*+?|."
  792. + (multiline ? "" : "\n"));
  793. } //}}}
  794. //{{{ load() method
  795. /**
  796. * Loads search and replace state from the properties.
  797. */
  798. public static void load()
  799. {
  800. search = jEdit.getProperty("search.find.value");
  801. replace = jEdit.getProperty("search.replace.value");
  802. ignoreCase = jEdit.getBooleanProperty("search.ignoreCase.toggle");
  803. regexp = jEdit.getBooleanProperty("search.regexp.toggle");
  804. beanshell = jEdit.getBooleanProperty("search.beanshell.toggle");
  805. wrap = jEdit.getBooleanProperty("search.wrap.toggle");
  806. fileset = new CurrentBufferSet();
  807. // Tags plugin likes to call this method at times other than
  808. // startup; so we need to fire a SearchSettingsChanged to
  809. // notify the search bar and so on.
  810. matcher = null;
  811. EditBus.send(new SearchSettingsChanged(null));
  812. } //}}}
  813. //{{{ save() method
  814. /**
  815. * Saves search and replace state to the properties.
  816. */
  817. public static void save()
  818. {
  819. jEdit.setProperty("search.find.value",search);
  820. jEdit.setProperty("search.replace.value",replace);
  821. jEdit.setBooleanProperty("search.ignoreCase.toggle",ignoreCase);
  822. jEdit.setBooleanProperty("search.regexp.toggle",regexp);
  823. jEdit.setBooleanProperty("search.beanshell.toggle",beanshell);
  824. jEdit.setBooleanProperty("search.wrap.toggle",wrap);
  825. } //}}}
  826. //{{{ handleError() method
  827. static void handleError(Component comp, Exception e)
  828. {
  829. Log.log(Log.ERROR,SearchAndReplace.class,e);
  830. if(comp instanceof Dialog)
  831. {
  832. new TextAreaDialog((Dialog)comp,
  833. beanshell ? "searcherror-bsh"
  834. : "searcherror",e);
  835. }
  836. else
  837. {
  838. new TextAreaDialog((Frame)comp,
  839. beanshell ? "searcherror-bsh"
  840. : "searcherror",e);
  841. }
  842. } //}}}
  843. //{{{ Private members
  844. //{{{ Instance variables
  845. private static String search;
  846. private static String replace;
  847. private static BshMethod replaceMethod;
  848. private static NameSpace replaceNS = new NameSpace(
  849. BeanShell.getNameSpace(),
  850. BeanShell.getNameSpace().getClassManager(),
  851. "search and replace");
  852. private static boolean regexp;
  853. private static boolean ignoreCase;
  854. private static boolean reverse;
  855. private static boolean beanshell;
  856. private static boolean wrap;
  857. private static SearchMatcher matcher;
  858. private static SearchFileSet fileset;
  859. //}}}
  860. //{{{ initReplace() method
  861. /**
  862. * Set up BeanShell replace if necessary.
  863. */
  864. private static void initReplace() throws Exception
  865. {
  866. if(beanshell && replace.length() != 0)
  867. {
  868. replaceMethod = BeanShell.cacheBlock("replace",
  869. "return (" + replace + ");",true);
  870. }
  871. else
  872. replaceMethod = null;
  873. } //}}}
  874. //{{{ record() method
  875. private static void record(View view, String action,
  876. boolean replaceAction, boolean recordFileSet)
  877. {
  878. Macros.Recorder recorder = view.getMacroRecorder();
  879. if(recorder != null)
  880. {
  881. recorder.record("SearchAndReplace.setSearchString(\""
  882. + MiscUtilities.charsToEscapes(search) + "\");");
  883. if(replaceAction)
  884. {
  885. recorder.record("SearchAndReplace.setReplaceString(\""
  886. + MiscUtilities.charsToEscapes(replace) + "\");");
  887. recorder.record("SearchAndReplace.setBeanShellReplace("
  888. + beanshell + ");");
  889. }
  890. else
  891. {
  892. // only record this if doing a find next
  893. recorder.record("SearchAndReplace.setAutoWrapAround("
  894. + wrap + ");");
  895. recorder.record("SearchAndReplace.setReverseSearch("
  896. + reverse + ");");
  897. }
  898. recorder.record("SearchAndReplace.setIgnoreCase("
  899. + ignoreCase + ");");
  900. recorder.record("SearchAndReplace.setRegexp("
  901. + regexp + ");");
  902. if(recordFileSet)
  903. {
  904. recorder.record("SearchAndReplace.setSearchFileSet("
  905. + fileset.getCode() + ");");
  906. }
  907. recorder.record("SearchAndReplace." + action + ";");
  908. }
  909. } //}}}
  910. //{{{ replaceInSelection() method
  911. private static int replaceInSelection(View view, JEditTextArea textArea,
  912. Buffer buffer, SearchMatcher matcher, boolean smartCaseReplace,
  913. Selection s) throws Exception
  914. {
  915. /* if an occurence occurs at the
  916. beginning of the selection, the
  917. selection start will get moved.
  918. this sucks, so we hack to avoid it. */
  919. int start = s.getStart();
  920. int returnValue;
  921. if(s instanceof Selection.Range)
  922. {
  923. returnValue = _replace(view,buffer,matcher,
  924. s.getStart(),s.getEnd(),
  925. smartCaseReplace);
  926. textArea.removeFromSelection(s);
  927. textArea.addToSelection(new Selection.Range(
  928. start,s.getEnd()));
  929. }
  930. else if(s instanceof Selection.Rect)
  931. {
  932. Selection.Rect rect = (Selection.Rect)s;
  933. int startCol = rect.getStartColumn(
  934. buffer);
  935. int endCol = rect.getEndColumn(
  936. buffer);
  937. returnValue = 0;
  938. for(int j = s.getStartLine(); j <= s.getEndLine(); j++)
  939. {
  940. returnValue += _replace(view,buffer,matcher,
  941. getColumnOnOtherLine(buffer,j,startCol),
  942. getColumnOnOtherLine(buffer,j,endCol),
  943. smartCaseReplace);
  944. }
  945. textArea.addToSelection(new Selection.Rect(
  946. start,s.getEnd()));
  947. }
  948. else
  949. throw new RuntimeException("Unsupported: " + s);
  950. return returnValue;
  951. } //}}}
  952. //{{{ _replace() method
  953. /**
  954. * Replaces all occurances of the search string with the replacement
  955. * string.
  956. * @param view The view
  957. * @param buffer The buffer
  958. * @param start The start offset
  959. * @param end The end offset
  960. * @param matcher The search matcher to use
  961. * @param smartCaseReplace See user's guide
  962. * @return The number of occurrences replaced
  963. */
  964. private static int _replace(View view, Buffer buffer,
  965. SearchMatcher matcher, int start, int end,
  966. boolean smartCaseReplace)
  967. throws Exception
  968. {
  969. int occurCount = 0;
  970. boolean endOfLine = (buffer.getLineEndOffset(
  971. buffer.getLineOfOffset(end)) - 1 == end);
  972. Segment text = new Segment();
  973. int offset = start;
  974. loop: for(int counter = 0; ; counter++)
  975. {
  976. buffer.getText(offset,end - offset,text);
  977. boolean startOfLine = (buffer.getLineStartOffset(
  978. buffer.getLineOfOffset(offset)) == offset);
  979. SearchMatcher.Match occur = matcher.nextMatch(
  980. new SegmentCharSequence(text,false),
  981. startOfLine,endOfLine,counter == 0,
  982. false);
  983. if(occur == null)
  984. break loop;
  985. String found = new String(text.array,
  986. text.offset + occur.start,
  987. occur.end - occur.start);
  988. int length = replaceOne(view,buffer,occur,offset,
  989. found,smartCaseReplace);
  990. if(length == -1)
  991. offset += occur.end;
  992. else
  993. {
  994. offset += occur.start + length;
  995. end += (length - found.length());
  996. occurCount++;
  997. }
  998. }
  999. return occurCount;
  1000. } //}}}
  1001. //{{{ replaceOne() method
  1002. /**
  1003. * Replace one occurrence of the search string with the
  1004. * replacement string.
  1005. */
  1006. private static int replaceOne(View view, Buffer buffer,
  1007. SearchMatcher.Match occur, int offset, String found,
  1008. boolean smartCaseReplace)
  1009. throws Exception
  1010. {
  1011. String subst = replaceOne(view,occur,found);
  1012. if(smartCaseReplace && ignoreCase)
  1013. {
  1014. int strCase = TextUtilities.getStringCase(found);
  1015. if(strCase == TextUtilities.LOWER_CASE)
  1016. subst = subst.toLowerCase();
  1017. else if(strCase == TextUtilities.UPPER_CASE)
  1018. subst = subst.toUpperCase();
  1019. else if(strCase == TextUtilities.TITLE_CASE)
  1020. subst = TextUtilities.toTitleCase(subst);
  1021. }
  1022. if(subst != null)
  1023. {
  1024. int start = offset + occur.start;
  1025. int end = offset + occur.end;
  1026. buffer.remove(start,end - start);
  1027. buffer.insert(start,subst);
  1028. return subst.length();
  1029. }
  1030. else
  1031. return -1;
  1032. } //}}}
  1033. //{{{ replaceOne() method
  1034. private static String replaceOne(View view,
  1035. SearchMatcher.Match occur, String found)
  1036. throws Exception
  1037. {
  1038. if(regexp)
  1039. {
  1040. if(replaceMethod != null)
  1041. return regexpBeanShellReplace(view,occur);
  1042. else
  1043. return regexpReplace(occur,found);
  1044. }
  1045. else
  1046. {
  1047. if(replaceMethod != null)
  1048. return literalBeanShellReplace(view,found);
  1049. else
  1050. return replace;
  1051. }
  1052. } //}}}
  1053. //{{{ regexpBeanShellReplace() method
  1054. private static String regexpBeanShellReplace(View view,
  1055. SearchMatcher.Match occur) throws Exception
  1056. {
  1057. for(int i = 0; i < occur.substitutions.length; i++)
  1058. {
  1059. replaceNS.setVariable("_" + i,
  1060. occur.substitutions[i]);
  1061. }
  1062. Object obj = BeanShell.runCachedBlock(
  1063. replaceMethod,view,replaceNS);
  1064. if(obj == null)
  1065. return "";
  1066. else
  1067. return obj.toString();
  1068. } //}}}
  1069. //{{{ regexpReplace() method
  1070. private static String regexpReplace(SearchMatcher.Match occur,
  1071. String found) throws Exception
  1072. {
  1073. StringBuffer buf = new StringBuffer();
  1074. for(int i = 0; i < replace.length(); i++)
  1075. {
  1076. char ch = replace.charAt(i);
  1077. switch(ch)
  1078. {
  1079. case '$':
  1080. if(i == replace.length() - 1)
  1081. {
  1082. buf.append(ch);
  1083. break;
  1084. }
  1085. ch = replace.charAt(++i);
  1086. if(ch == '$')
  1087. buf.append('$');
  1088. else if(ch == '0')
  1089. buf.append(found);
  1090. else if(Character.isDigit(ch))
  1091. {
  1092. int n = ch - '0';
  1093. if(n < occur
  1094. .substitutions
  1095. .length)
  1096. {
  1097. buf.append(
  1098. occur
  1099. .substitutions
  1100. [n]
  1101. );
  1102. }
  1103. }
  1104. break;
  1105. case '\\':
  1106. if(i == replace.length() - 1)
  1107. {
  1108. buf.append('\\');
  1109. break;
  1110. }
  1111. ch = replace.charAt(++i);
  1112. switch(ch)
  1113. {
  1114. case 'n':
  1115. buf.append('\n');
  1116. break;
  1117. case 't':
  1118. buf.append('\t');
  1119. break;
  1120. default:
  1121. buf.append(ch);
  1122. break;
  1123. }
  1124. break;
  1125. default:
  1126. buf.append(ch);
  1127. break;
  1128. }
  1129. }
  1130. return buf.toString();
  1131. } //}}}
  1132. //{{{ literalBeanShellReplace() method
  1133. private static String literalBeanShellReplace(View view, String found)
  1134. throws Exception
  1135. {
  1136. replaceNS.setVariable("_0",found);
  1137. Object obj = BeanShell.runCachedBlock(
  1138. replaceMethod,
  1139. view,replaceNS);
  1140. if(obj == null)
  1141. return "";
  1142. else
  1143. return obj.toString();
  1144. } //}}}
  1145. //{{{ getColumnOnOtherLine() method
  1146. /**
  1147. * Should be somewhere else...
  1148. */
  1149. private static int getColumnOnOtherLine(Buffer buffer, int line,
  1150. int col)
  1151. {
  1152. int returnValue = buffer.getOffsetOfVirtualColumn(
  1153. line,col,null);
  1154. if(returnValue == -1)
  1155. return buffer.getLineEndOffset(line) - 1;
  1156. else
  1157. return buffer.getLineStartOffset(line) + returnValue;
  1158. } //}}}
  1159. //}}}
  1160. }