0001: /*
0002: * SearchAndReplace.java - Search and replace
0003: * :tabSize=8:indentSize=8:noTabs=false:
0004: * :folding=explicit:collapseFolds=1:
0005: *
0006: * Copyright (C) 1999, 2004 Slava Pestov
0007: * Portions copyright (C) 2001 Tom Locke
0008: *
0009: * This program is free software; you can redistribute it and/or
0010: * modify it under the terms of the GNU General Public License
0011: * as published by the Free Software Foundation; either version 2
0012: * of the License, or any later version.
0013: *
0014: * This program is distributed in the hope that it will be useful,
0015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0017: * GNU General Public License for more details.
0018: *
0019: * You should have received a copy of the GNU General Public License
0020: * along with this program; if not, write to the Free Software
0021: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
0022: */
0023:
0024: package org.gjt.sp.jedit.search;
0025:
0026: //{{{ Imports
0027: import org.gjt.sp.jedit.bsh.*;
0028: import java.awt.*;
0029: import javax.swing.JOptionPane;
0030: import javax.swing.text.Segment;
0031: import org.gjt.sp.jedit.*;
0032: import org.gjt.sp.jedit.gui.TextAreaDialog;
0033: import org.gjt.sp.jedit.io.VFSManager;
0034: import org.gjt.sp.jedit.msg.SearchSettingsChanged;
0035: import org.gjt.sp.jedit.textarea.*;
0036: import org.gjt.sp.jedit.TextUtilities;
0037: import org.gjt.sp.util.SegmentCharSequence;
0038: import org.gjt.sp.util.Log;
0039:
0040: //}}}
0041:
0042: /**
0043: * Class that implements regular expression and literal search within
0044: * jEdit buffers.<p>
0045: *
0046: * There are two main groups of methods in this class:
0047: * <ul>
0048: * <li>Property accessors - for changing search and replace settings.</li>
0049: * <li>Actions - for performing search and replace.</li>
0050: * </ul>
0051: *
0052: * The "HyperSearch" and "Keep dialog" features, as reflected in
0053: * checkbox options in the search dialog, are not handled from within
0054: * this class. If you wish to have these options set before the search dialog
0055: * appears, make a prior call to either or both of the following:
0056: *
0057: * <pre> jEdit.setBooleanProperty("search.hypersearch.toggle",true);
0058: * jEdit.setBooleanProperty("search.keepDialog.toggle",true);</pre>
0059: *
0060: * If you are not using the dialog to undertake a search or replace, you may
0061: * call any of the search and replace methods (including
0062: * {@link #hyperSearch(View)}) without concern for the value of these
0063: * properties.
0064: *
0065: * @author Slava Pestov
0066: * @author John Gellene (API documentation)
0067: * @version $Id: SearchAndReplace.java 10894 2007-10-15 16:11:39Z mediumnet $
0068: */
0069: public class SearchAndReplace {
0070: //{{{ Getters and setters
0071:
0072: //{{{ setSearchString() method
0073: /**
0074: * Sets the current search string.
0075: * @param search The new search string
0076: */
0077: public static void setSearchString(String search) {
0078: if (search.equals(SearchAndReplace.search))
0079: return;
0080:
0081: SearchAndReplace.search = search;
0082: matcher = null;
0083:
0084: EditBus.send(new SearchSettingsChanged(null));
0085: } //}}}
0086:
0087: //{{{ getSearchString() method
0088: /**
0089: * Returns the current search string.
0090: */
0091: public static String getSearchString() {
0092: return search;
0093: } //}}}
0094:
0095: //{{{ setReplaceString() method
0096: /**
0097: * Sets the current replacement string.
0098: * @param replace The new replacement string
0099: */
0100: public static void setReplaceString(String replace) {
0101: if (replace.equals(SearchAndReplace.replace))
0102: return;
0103:
0104: SearchAndReplace.replace = replace;
0105:
0106: EditBus.send(new SearchSettingsChanged(null));
0107: } //}}}
0108:
0109: //{{{ getReplaceString() method
0110: /**
0111: * Returns the current replacement string.
0112: */
0113: public static String getReplaceString() {
0114: return replace;
0115: } //}}}
0116:
0117: //{{{ setIgnoreCase() method
0118: /**
0119: * Sets the ignore case flag.
0120: * @param ignoreCase True if searches should be case insensitive,
0121: * false otherwise
0122: */
0123: public static void setIgnoreCase(boolean ignoreCase) {
0124: if (ignoreCase == SearchAndReplace.ignoreCase)
0125: return;
0126:
0127: SearchAndReplace.ignoreCase = ignoreCase;
0128: matcher = null;
0129:
0130: EditBus.send(new SearchSettingsChanged(null));
0131: } //}}}
0132:
0133: //{{{ getIgnoreCase() method
0134: /**
0135: * Returns the state of the ignore case flag.
0136: * @return True if searches should be case insensitive,
0137: * false otherwise
0138: */
0139: public static boolean getIgnoreCase() {
0140: return ignoreCase;
0141: } //}}}
0142:
0143: //{{{ setRegexp() method
0144: /**
0145: * Sets the state of the regular expression flag.
0146: * @param regexp True if regular expression searches should be
0147: * performed
0148: */
0149: public static void setRegexp(boolean regexp) {
0150: if (regexp == SearchAndReplace.regexp)
0151: return;
0152:
0153: SearchAndReplace.regexp = regexp;
0154: if (regexp && reverse)
0155: reverse = false;
0156:
0157: matcher = null;
0158:
0159: EditBus.send(new SearchSettingsChanged(null));
0160: } //}}}
0161:
0162: //{{{ getRegexp() method
0163: /**
0164: * Returns the state of the regular expression flag.
0165: * @return True if regular expression searches should be performed
0166: */
0167: public static boolean getRegexp() {
0168: return regexp;
0169: } //}}}
0170:
0171: //{{{ setReverseSearch() method
0172: /**
0173: * Determines whether a reverse search will conducted from the current
0174: * position to the beginning of a buffer. Note that reverse search and
0175: * regular expression search is mutually exclusive; enabling one will
0176: * disable the other.
0177: * @param reverse True if searches should go backwards,
0178: * false otherwise
0179: */
0180: public static void setReverseSearch(boolean reverse) {
0181: if (reverse == SearchAndReplace.reverse)
0182: return;
0183:
0184: SearchAndReplace.reverse = reverse;
0185:
0186: EditBus.send(new SearchSettingsChanged(null));
0187: } //}}}
0188:
0189: //{{{ getReverseSearch() method
0190: /**
0191: * Returns the state of the reverse search flag.
0192: * @return True if searches should go backwards,
0193: * false otherwise
0194: */
0195: public static boolean getReverseSearch() {
0196: return reverse;
0197: } //}}}
0198:
0199: //{{{ setBeanShellReplace() method
0200: /**
0201: * Sets the state of the BeanShell replace flag.
0202: * @param beanshell True if the replace string is a BeanShell expression
0203: * @since jEdit 3.2pre2
0204: */
0205: public static void setBeanShellReplace(boolean beanshell) {
0206: if (beanshell == SearchAndReplace.beanshell)
0207: return;
0208:
0209: SearchAndReplace.beanshell = beanshell;
0210:
0211: EditBus.send(new SearchSettingsChanged(null));
0212: } //}}}
0213:
0214: //{{{ getBeanShellReplace() method
0215: /**
0216: * Returns the state of the BeanShell replace flag.
0217: * @return True if the replace string is a BeanShell expression
0218: * @since jEdit 3.2pre2
0219: */
0220: public static boolean getBeanShellReplace() {
0221: return beanshell;
0222: } //}}}
0223:
0224: //{{{ setAutoWrap() method
0225: /**
0226: * Sets the state of the auto wrap around flag.
0227: * @param wrap If true, the 'continue search from start' dialog
0228: * will not be displayed
0229: * @since jEdit 3.2pre2
0230: */
0231: public static void setAutoWrapAround(boolean wrap) {
0232: if (wrap == SearchAndReplace.wrap)
0233: return;
0234:
0235: SearchAndReplace.wrap = wrap;
0236:
0237: EditBus.send(new SearchSettingsChanged(null));
0238: } //}}}
0239:
0240: //{{{ getAutoWrap() method
0241: /**
0242: * Returns the state of the auto wrap around flag.
0243: * @since jEdit 3.2pre2
0244: */
0245: public static boolean getAutoWrapAround() {
0246: return wrap;
0247: } //}}}
0248:
0249: //{{{ setSearchMatcher() method
0250: /**
0251: * Sets a custom search string matcher. Note that calling
0252: * {@link #setSearchString(String)},
0253: * {@link #setIgnoreCase(boolean)}, or {@link #setRegexp(boolean)}
0254: * will reset the matcher to the default.
0255: */
0256: public static void setSearchMatcher(SearchMatcher matcher) {
0257: SearchAndReplace.matcher = matcher;
0258:
0259: EditBus.send(new SearchSettingsChanged(null));
0260: } //}}}
0261:
0262: //{{{ getSearchMatcher() method
0263: /**
0264: * Returns the current search string matcher.
0265: * @return a SearchMatcher or null if there is no search or if the matcher can match empty String
0266: *
0267: * @exception IllegalArgumentException if regular expression search
0268: * is enabled, the search string or replacement string is invalid
0269: * @since jEdit 4.1pre7
0270: */
0271: public static SearchMatcher getSearchMatcher() throws Exception {
0272: if (matcher != null)
0273: return matcher;
0274:
0275: if (search == null || "".equals(search))
0276: return null;
0277:
0278: if (regexp)
0279: matcher = new PatternSearchMatcher(search, ignoreCase);
0280: else
0281: matcher = new BoyerMooreSearchMatcher(search, ignoreCase);
0282:
0283: if (matcher.nextMatch("", true, true, true, false) != null) {
0284: Log.log(Log.WARNING, SearchAndReplace.class, "The matcher "
0285: + matcher + " can match empty string !");
0286: matcher = null;
0287: }
0288:
0289: return matcher;
0290: } //}}}
0291:
0292: //{{{ setSearchFileSet() method
0293: /**
0294: * Sets the current search file set.
0295: * @param fileset The file set to perform searches in
0296: * @see AllBufferSet
0297: * @see CurrentBufferSet
0298: * @see DirectoryListSet
0299: */
0300: public static void setSearchFileSet(SearchFileSet fileset) {
0301: SearchAndReplace.fileset = fileset;
0302:
0303: EditBus.send(new SearchSettingsChanged(null));
0304: } //}}}
0305:
0306: //{{{ getSearchFileSet() method
0307: /**
0308: * Returns the current search file set.
0309: */
0310: public static SearchFileSet getSearchFileSet() {
0311: return fileset;
0312: } //}}}
0313:
0314: //{{{ getSmartCaseReplace() method
0315: /**
0316: * Returns if the replacement string will assume the same case as
0317: * each specific occurrence of the search string.
0318: * @since jEdit 4.2pre10
0319: */
0320: public static boolean getSmartCaseReplace() {
0321: return (replace != null && TextUtilities.getStringCase(replace) == TextUtilities.LOWER_CASE);
0322: } //}}}
0323:
0324: //}}}
0325:
0326: //{{{ Actions
0327:
0328: //{{{ hyperSearch() method
0329: /**
0330: * Performs a HyperSearch.
0331: * @param view The view
0332: * @since jEdit 2.7pre3
0333: */
0334: public static boolean hyperSearch(View view) {
0335: return hyperSearch(view, false);
0336: } //}}}
0337:
0338: //{{{ hyperSearch() method
0339: /**
0340: * Performs a HyperSearch.
0341: * @param view The view
0342: * @param selection If true, will only search in the current selection.
0343: * Note that the file set must be the current buffer file set for this
0344: * to work.
0345: * @since jEdit 4.0pre1
0346: */
0347: public static boolean hyperSearch(View view, boolean selection) {
0348: // component that will parent any dialog boxes
0349: Component comp = SearchDialog.getSearchDialog(view);
0350: if (comp == null)
0351: comp = view;
0352:
0353: record(view, "hyperSearch(view," + selection + ')', false,
0354: !selection);
0355:
0356: view.getDockableWindowManager().addDockableWindow(
0357: HyperSearchResults.NAME);
0358: final HyperSearchResults results = (HyperSearchResults) view
0359: .getDockableWindowManager().getDockable(
0360: HyperSearchResults.NAME);
0361: results.searchStarted();
0362:
0363: try {
0364: SearchMatcher matcher = getSearchMatcher();
0365: if (matcher == null) {
0366: view.getToolkit().beep();
0367: results.searchFailed();
0368: return false;
0369: }
0370:
0371: Selection[] s;
0372: if (selection) {
0373: s = view.getTextArea().getSelection();
0374: if (s == null) {
0375: results.searchFailed();
0376: return false;
0377: }
0378: } else
0379: s = null;
0380: VFSManager.runInWorkThread(new HyperSearchRequest(view,
0381: matcher, results, s));
0382: return true;
0383: } catch (Exception e) {
0384: results.searchFailed();
0385: handleError(comp, e);
0386: return false;
0387: }
0388: } //}}}
0389:
0390: //{{{ find() method
0391: /**
0392: * Finds the next occurrence of the search string.
0393: * @param view The view
0394: * @return True if the operation was successful, false otherwise
0395: */
0396: public static boolean find(View view) {
0397: // component that will parent any dialog boxes
0398: Component comp = SearchDialog.getSearchDialog(view);
0399: if (comp == null || !comp.isShowing())
0400: comp = view;
0401:
0402: String path = fileset.getNextFile(view, null);
0403: if (path == null) {
0404: GUIUtilities.error(comp, "empty-fileset", null);
0405: return false;
0406: }
0407:
0408: boolean _reverse = reverse
0409: && fileset instanceof CurrentBufferSet;
0410: if (_reverse && regexp) {
0411: GUIUtilities.error(comp, "regexp-reverse", null);
0412: return false;
0413: }
0414:
0415: try {
0416: view.showWaitCursor();
0417:
0418: SearchMatcher matcher = getSearchMatcher();
0419: if (matcher == null) {
0420: view.getToolkit().beep();
0421: return false;
0422: }
0423:
0424: record(view, "find(view)", false, true);
0425:
0426: boolean repeat = false;
0427: loop: for (;;) {
0428: while (path != null) {
0429: Buffer buffer = jEdit.openTemporary(view, null,
0430: path, false);
0431:
0432: /* this is stupid and misleading.
0433: * but 'path' is not used anywhere except
0434: * the above line, and if this is done
0435: * after the 'continue', then we will
0436: * either hang, or be forced to duplicate
0437: * it inside the buffer == null, or add
0438: * a 'finally' clause. you decide which one's
0439: * worse. */
0440: path = fileset.getNextFile(view, path);
0441:
0442: if (buffer == null)
0443: continue loop;
0444:
0445: // Wait for the buffer to load
0446: if (!buffer.isLoaded())
0447: VFSManager.waitForRequests();
0448:
0449: int start;
0450:
0451: if (view.getBuffer() == buffer && !repeat) {
0452: JEditTextArea textArea = view.getTextArea();
0453: Selection s = textArea
0454: .getSelectionAtOffset(textArea
0455: .getCaretPosition());
0456: if (s == null)
0457: start = textArea.getCaretPosition();
0458: else if (_reverse)
0459: start = s.getStart();
0460: else
0461: start = s.getEnd();
0462: } else if (_reverse)
0463: start = buffer.getLength();
0464: else
0465: start = 0;
0466:
0467: boolean _search = true;
0468: if (!_reverse && matcher.isMatchingEOL()) {
0469: if (start < buffer.getLength())
0470: start += 1;
0471: else
0472: _search = false;
0473: }
0474:
0475: if (_search
0476: && find(view, buffer, start, repeat,
0477: _reverse))
0478: return true;
0479: }
0480:
0481: if (repeat) {
0482: if (!BeanShell.isScriptRunning()) {
0483: view
0484: .getStatus()
0485: .setMessageAndClear(
0486: jEdit
0487: .getProperty("view.status.search-not-found"));
0488:
0489: view.getToolkit().beep();
0490: }
0491: return false;
0492: }
0493:
0494: boolean restart;
0495:
0496: // if auto wrap is on, always restart search.
0497: // if auto wrap is off, and we're called from
0498: // a macro, stop search. If we're called
0499: // interactively, ask the user what to do.
0500: if (wrap) {
0501: if (!BeanShell.isScriptRunning()) {
0502: view
0503: .getStatus()
0504: .setMessageAndClear(
0505: jEdit
0506: .getProperty("view.status.auto-wrap"));
0507: // beep if beep property set
0508: if (jEdit
0509: .getBooleanProperty("search.beepOnSearchAutoWrap")) {
0510: view.getToolkit().beep();
0511: }
0512: }
0513: restart = true;
0514: } else if (BeanShell.isScriptRunning()) {
0515: restart = false;
0516: } else {
0517: Integer[] args = { Integer
0518: .valueOf(_reverse ? 1 : 0) };
0519: int result = GUIUtilities.confirm(comp,
0520: "keepsearching", args,
0521: JOptionPane.YES_NO_OPTION,
0522: JOptionPane.QUESTION_MESSAGE);
0523: restart = (result == JOptionPane.YES_OPTION);
0524: }
0525:
0526: if (restart) {
0527: // start search from beginning
0528: path = fileset.getFirstFile(view);
0529: repeat = true;
0530: } else
0531: break loop;
0532: }
0533: } catch (Exception e) {
0534: handleError(comp, e);
0535: } finally {
0536: view.hideWaitCursor();
0537: }
0538:
0539: return false;
0540: } //}}}
0541:
0542: //{{{ find() method
0543: /**
0544: * Finds the next instance of the search string in the specified
0545: * buffer.
0546: * @param view The view
0547: * @param buffer The buffer
0548: * @param start Location where to start the search
0549: */
0550: public static boolean find(View view, Buffer buffer, int start)
0551: throws Exception {
0552: return find(view, buffer, start, false, false);
0553: } //}}}
0554:
0555: //{{{ find() method
0556: /**
0557: * Finds the next instance of the search string in the specified
0558: * buffer.
0559: * @param view The view
0560: * @param buffer The buffer
0561: * @param start Location where to start the search
0562: * @param firstTime See {@link SearchMatcher#nextMatch(CharSequence,boolean,boolean,boolean,boolean)}.
0563: * @since jEdit 4.1pre7
0564: */
0565: public static boolean find(View view, Buffer buffer, int start,
0566: boolean firstTime, boolean reverse) throws Exception {
0567: SearchMatcher matcher = getSearchMatcher();
0568: if (matcher == null) {
0569: view.getToolkit().beep();
0570: return false;
0571: }
0572:
0573: Segment text = new Segment();
0574: if (reverse)
0575: buffer.getText(0, start, text);
0576: else
0577: buffer.getText(start, buffer.getLength() - start, text);
0578:
0579: // the start and end flags will be wrong with reverse search enabled,
0580: // but they are only used by the regexp matcher, which doesn't
0581: // support reverse search yet.
0582: //
0583: // REMIND: fix flags when adding reverse regexp search.
0584: SearchMatcher.Match match = matcher.nextMatch(
0585: new SegmentCharSequence(text, reverse), start == 0,
0586: true, firstTime, reverse);
0587:
0588: if (match != null) {
0589: jEdit.commitTemporary(buffer);
0590: view.setBuffer(buffer, true);
0591: JEditTextArea textArea = view.getTextArea();
0592:
0593: if (reverse) {
0594: textArea.setSelection(new Selection.Range(start
0595: - match.end, start - match.start));
0596: // make sure end of match is visible
0597: textArea.scrollTo(start - match.start, false);
0598: textArea.moveCaretPosition(start - match.end);
0599: } else {
0600: textArea.setSelection(new Selection.Range(start
0601: + match.start, start + match.end));
0602: textArea.moveCaretPosition(start + match.end);
0603: // make sure start of match is visible
0604: textArea.scrollTo(start + match.start, false);
0605: }
0606:
0607: return true;
0608: } else
0609: return false;
0610: } //}}}
0611:
0612: //{{{ replace() method
0613: /**
0614: * Replaces the current selection with the replacement string.
0615: * @param view The view
0616: * @return True if the operation was successful, false otherwise
0617: */
0618: public static boolean replace(View view) {
0619: // component that will parent any dialog boxes
0620: Component comp = SearchDialog.getSearchDialog(view);
0621: if (comp == null)
0622: comp = view;
0623:
0624: JEditTextArea textArea = view.getTextArea();
0625:
0626: Buffer buffer = view.getBuffer();
0627: if (!buffer.isEditable())
0628: return false;
0629:
0630: boolean smartCaseReplace = getSmartCaseReplace();
0631:
0632: Selection[] selection = textArea.getSelection();
0633: if (selection.length == 0) {
0634: try {
0635: SearchMatcher matcher = getSearchMatcher();
0636: if ((matcher != null) && (matcher.isMatchingEOL())) {
0637: int caretPosition = textArea.getCaretPosition();
0638: selection = new Selection[] { new Selection.Range(
0639: caretPosition, caretPosition) };
0640: } else {
0641: view.getToolkit().beep();
0642: return false;
0643: }
0644: } catch (Exception e) {
0645: handleError(comp, e);
0646: return false;
0647: }
0648: }
0649:
0650: record(view, "replace(view)", true, false);
0651:
0652: // a little hack for reverse replace and find
0653: int caret = textArea.getCaretPosition();
0654: Selection s = textArea.getSelectionAtOffset(caret);
0655: if (s != null)
0656: caret = s.getStart();
0657:
0658: try {
0659: buffer.beginCompoundEdit();
0660:
0661: SearchMatcher matcher = getSearchMatcher();
0662: if (matcher == null)
0663: return false;
0664:
0665: initReplace();
0666:
0667: int retVal = 0;
0668:
0669: for (int i = 0; i < selection.length; i++) {
0670: s = selection[i];
0671:
0672: retVal += replaceInSelection(view, textArea, buffer,
0673: matcher, smartCaseReplace, s);
0674: }
0675:
0676: boolean _reverse = !regexp && reverse
0677: && fileset instanceof CurrentBufferSet;
0678: if (_reverse) {
0679: // so that Replace and Find continues from
0680: // the right location
0681: textArea.moveCaretPosition(caret);
0682: } else {
0683: s = textArea.getSelectionAtOffset(textArea
0684: .getCaretPosition());
0685: if (s != null)
0686: textArea.moveCaretPosition(s.getEnd());
0687: }
0688:
0689: if (!BeanShell.isScriptRunning()) {
0690: Object[] args = { Integer.valueOf(retVal),
0691: Integer.valueOf(1) };
0692: view.getStatus().setMessageAndClear(
0693: jEdit.getProperty("view.status.replace-all",
0694: args));
0695: }
0696:
0697: if (retVal == 0) {
0698: view.getToolkit().beep();
0699: return false;
0700: }
0701:
0702: return true;
0703: } catch (Exception e) {
0704: handleError(comp, e);
0705: } finally {
0706: buffer.endCompoundEdit();
0707: }
0708:
0709: return false;
0710: } //}}}
0711:
0712: //{{{ replace() method
0713: /**
0714: * Replaces text in the specified range with the replacement string.
0715: * @param view The view
0716: * @param buffer The buffer
0717: * @param start The start offset
0718: * @param end The end offset
0719: * @return True if the operation was successful, false otherwise
0720: */
0721: public static boolean replace(View view, Buffer buffer, int start,
0722: int end) {
0723: if (!buffer.isEditable())
0724: return false;
0725:
0726: // component that will parent any dialog boxes
0727: Component comp = SearchDialog.getSearchDialog(view);
0728: if (comp == null)
0729: comp = view;
0730:
0731: boolean smartCaseReplace = getSmartCaseReplace();
0732:
0733: try {
0734: buffer.beginCompoundEdit();
0735:
0736: SearchMatcher matcher = getSearchMatcher();
0737: if (matcher == null)
0738: return false;
0739:
0740: initReplace();
0741:
0742: int retVal = 0;
0743:
0744: retVal += _replace(view, buffer, matcher, start, end,
0745: smartCaseReplace);
0746:
0747: if (retVal != 0)
0748: return true;
0749: } catch (Exception e) {
0750: handleError(comp, e);
0751: } finally {
0752: buffer.endCompoundEdit();
0753: }
0754:
0755: return false;
0756: } //}}}
0757:
0758: //{{{ replaceAll() method
0759: /**
0760: * Replaces all occurrences of the search string with the replacement
0761: * string.
0762: * @param view The view
0763: */
0764: public static boolean replaceAll(View view) {
0765: return replaceAll(view, false);
0766: } //}}}
0767:
0768: //{{{ replaceAll() method
0769: /**
0770: * Replaces all occurrences of the search string with the replacement
0771: * string.
0772: * @param view The view
0773: * @param dontOpenChangedFiles Whether to open changed files or to autosave them quietly
0774: */
0775: public static boolean replaceAll(View view,
0776: boolean dontOpenChangedFiles) {
0777: // component that will parent any dialog boxes
0778: Component comp = SearchDialog.getSearchDialog(view);
0779: if (comp == null)
0780: comp = view;
0781:
0782: if (fileset.getFileCount(view) == 0) {
0783: GUIUtilities.error(comp, "empty-fileset", null);
0784: return false;
0785: }
0786:
0787: record(view, "replaceAll(view)", true, true);
0788:
0789: view.showWaitCursor();
0790:
0791: boolean smartCaseReplace = (replace != null && TextUtilities
0792: .getStringCase(replace) == TextUtilities.LOWER_CASE);
0793:
0794: int fileCount = 0;
0795: int occurCount = 0;
0796: try {
0797: SearchMatcher matcher = getSearchMatcher();
0798: if (matcher == null)
0799: return false;
0800:
0801: initReplace();
0802:
0803: String path = fileset.getFirstFile(view);
0804: loop: while (path != null) {
0805: Buffer buffer = jEdit.openTemporary(view, null, path,
0806: false);
0807:
0808: /* this is stupid and misleading.
0809: * but 'path' is not used anywhere except
0810: * the above line, and if this is done
0811: * after the 'continue', then we will
0812: * either hang, or be forced to duplicate
0813: * it inside the buffer == null, or add
0814: * a 'finally' clause. you decide which one's
0815: * worse. */
0816: path = fileset.getNextFile(view, path);
0817:
0818: if (buffer == null)
0819: continue loop;
0820:
0821: // Wait for buffer to finish loading
0822: if (buffer.isPerformingIO())
0823: VFSManager.waitForRequests();
0824:
0825: if (!buffer.isEditable())
0826: continue loop;
0827:
0828: // Leave buffer in a consistent state if
0829: // an error occurs
0830: int retVal = 0;
0831:
0832: try {
0833: buffer.beginCompoundEdit();
0834: retVal = _replace(view, buffer, matcher, 0, buffer
0835: .getLength(), smartCaseReplace);
0836: } finally {
0837: buffer.endCompoundEdit();
0838: }
0839:
0840: if (retVal != 0) {
0841: fileCount++;
0842: occurCount += retVal;
0843: if (dontOpenChangedFiles) {
0844: buffer.save(null, null);
0845: } else {
0846: jEdit.commitTemporary(buffer);
0847: }
0848: }
0849: }
0850: } catch (Exception e) {
0851: handleError(comp, e);
0852: } finally {
0853: view.hideWaitCursor();
0854: }
0855:
0856: /* Don't do this when playing a macro, cos it's annoying */
0857: if (!BeanShell.isScriptRunning()) {
0858: Object[] args = { Integer.valueOf(occurCount),
0859: Integer.valueOf(fileCount) };
0860: view.getStatus().setMessageAndClear(
0861: jEdit.getProperty("view.status.replace-all", args));
0862: if (occurCount == 0)
0863: view.getToolkit().beep();
0864: }
0865:
0866: return (fileCount != 0);
0867: } //}}}
0868:
0869: //}}}
0870:
0871: //{{{ escapeRegexp() method
0872: /**
0873: * Escapes characters with special meaning in a regexp.
0874: * @param multiline Should \n be escaped?
0875: * @since jEdit 4.3pre1
0876: */
0877: public static String escapeRegexp(String str, boolean multiline) {
0878: return MiscUtilities.charsToEscapes(str, "\r\t\\()[]{}$^*+?|."
0879: + (multiline ? "" : "\n"));
0880: } //}}}
0881:
0882: //{{{ load() method
0883: /**
0884: * Loads search and replace state from the properties.
0885: */
0886: public static void load() {
0887: search = jEdit.getProperty("search.find.value");
0888: replace = jEdit.getProperty("search.replace.value");
0889: ignoreCase = jEdit
0890: .getBooleanProperty("search.ignoreCase.toggle");
0891: regexp = jEdit.getBooleanProperty("search.regexp.toggle");
0892: beanshell = jEdit.getBooleanProperty("search.beanshell.toggle");
0893: wrap = jEdit.getBooleanProperty("search.wrap.toggle");
0894:
0895: fileset = new CurrentBufferSet();
0896:
0897: // Tags plugin likes to call this method at times other than
0898: // startup; so we need to fire a SearchSettingsChanged to
0899: // notify the search bar and so on.
0900: matcher = null;
0901: EditBus.send(new SearchSettingsChanged(null));
0902: } //}}}
0903:
0904: //{{{ save() method
0905: /**
0906: * Saves search and replace state to the properties.
0907: */
0908: public static void save() {
0909: jEdit.setProperty("search.find.value", search);
0910: jEdit.setProperty("search.replace.value", replace);
0911: jEdit
0912: .setBooleanProperty("search.ignoreCase.toggle",
0913: ignoreCase);
0914: jEdit.setBooleanProperty("search.regexp.toggle", regexp);
0915: jEdit.setBooleanProperty("search.beanshell.toggle", beanshell);
0916: jEdit.setBooleanProperty("search.wrap.toggle", wrap);
0917: } //}}}
0918:
0919: //{{{ handleError() method
0920: static void handleError(Component comp, Exception e) {
0921: Log.log(Log.ERROR, SearchAndReplace.class, e);
0922: if (comp instanceof Dialog) {
0923: new TextAreaDialog((Dialog) comp,
0924: beanshell ? "searcherror-bsh" : "searcherror", e);
0925: } else {
0926: new TextAreaDialog((Frame) comp,
0927: beanshell ? "searcherror-bsh" : "searcherror", e);
0928: }
0929: } //}}}
0930:
0931: //{{{ Private members
0932:
0933: //{{{ Instance variables
0934: private static String search;
0935: private static String replace;
0936: private static BshMethod replaceMethod;
0937: private static NameSpace replaceNS = new NameSpace(BeanShell
0938: .getNameSpace(),
0939: BeanShell.getNameSpace().getClassManager(),
0940: "search and replace");
0941: private static boolean regexp;
0942: private static boolean ignoreCase;
0943: private static boolean reverse;
0944: private static boolean beanshell;
0945: private static boolean wrap;
0946: private static SearchMatcher matcher;
0947: private static SearchFileSet fileset;
0948:
0949: //}}}
0950:
0951: //{{{ initReplace() method
0952: /**
0953: * Set up BeanShell replace if necessary.
0954: */
0955: private static void initReplace() throws Exception {
0956: if (beanshell && replace.length() != 0) {
0957: replaceMethod = BeanShell.cacheBlock("replace", "return ("
0958: + replace + ");", true);
0959: } else
0960: replaceMethod = null;
0961: } //}}}
0962:
0963: //{{{ record() method
0964: private static void record(View view, String action,
0965: boolean replaceAction, boolean recordFileSet) {
0966: Macros.Recorder recorder = view.getMacroRecorder();
0967:
0968: if (recorder != null) {
0969: recorder.record("SearchAndReplace.setSearchString(\""
0970: + MiscUtilities.charsToEscapes(search) + "\");");
0971:
0972: if (replaceAction) {
0973: recorder.record("SearchAndReplace.setReplaceString(\""
0974: + MiscUtilities.charsToEscapes(replace)
0975: + "\");");
0976: recorder.record("SearchAndReplace.setBeanShellReplace("
0977: + beanshell + ");");
0978: } else {
0979: // only record this if doing a find next
0980: recorder.record("SearchAndReplace.setAutoWrapAround("
0981: + wrap + ");");
0982: recorder.record("SearchAndReplace.setReverseSearch("
0983: + reverse + ");");
0984: }
0985:
0986: recorder.record("SearchAndReplace.setIgnoreCase("
0987: + ignoreCase + ");");
0988: recorder.record("SearchAndReplace.setRegexp(" + regexp
0989: + ");");
0990:
0991: if (recordFileSet) {
0992: recorder.record("SearchAndReplace.setSearchFileSet("
0993: + fileset.getCode() + ");");
0994: }
0995:
0996: recorder.record("SearchAndReplace." + action + ';');
0997: }
0998: } //}}}
0999:
1000: //{{{ replaceInSelection() method
1001: private static int replaceInSelection(View view,
1002: JEditTextArea textArea, Buffer buffer,
1003: SearchMatcher matcher, boolean smartCaseReplace, Selection s)
1004: throws Exception {
1005: /* if an occurence occurs at the
1006: beginning of the selection, the
1007: selection start will get moved.
1008: this sucks, so we hack to avoid it. */
1009: int start = s.getStart();
1010:
1011: int returnValue;
1012:
1013: if (s instanceof Selection.Range) {
1014: returnValue = _replace(view, buffer, matcher, s.getStart(),
1015: s.getEnd(), smartCaseReplace);
1016:
1017: textArea.removeFromSelection(s);
1018: textArea.addToSelection(new Selection.Range(start, s
1019: .getEnd()));
1020: } else if (s instanceof Selection.Rect) {
1021: Selection.Rect rect = (Selection.Rect) s;
1022: int startCol = rect.getStartColumn(buffer);
1023: int endCol = rect.getEndColumn(buffer);
1024:
1025: returnValue = 0;
1026: for (int j = s.getStartLine(); j <= s.getEndLine(); j++) {
1027: returnValue += _replace(view, buffer, matcher,
1028: getColumnOnOtherLine(buffer, j, startCol),
1029: getColumnOnOtherLine(buffer, j, endCol),
1030: smartCaseReplace);
1031: }
1032: textArea.addToSelection(new Selection.Rect(start, s
1033: .getEnd()));
1034: } else
1035: throw new RuntimeException("Unsupported: " + s);
1036:
1037: return returnValue;
1038: } //}}}
1039:
1040: //{{{ _replace() method
1041: /**
1042: * Replaces all occurrences of the search string with the replacement
1043: * string.
1044: * @param view The view
1045: * @param buffer The buffer
1046: * @param start The start offset
1047: * @param end The end offset
1048: * @param matcher The search matcher to use
1049: * @param smartCaseReplace See user's guide
1050: * @return The number of occurrences replaced
1051: */
1052: private static int _replace(View view, Buffer buffer,
1053: SearchMatcher matcher, int start, int end,
1054: boolean smartCaseReplace) throws Exception {
1055: int occurCount = 0;
1056:
1057: boolean endOfLine = (buffer.getLineEndOffset(buffer
1058: .getLineOfOffset(end)) - 1 == end);
1059:
1060: Segment text = new Segment();
1061: int offset = start;
1062: loop: for (int counter = 0;; counter++) {
1063: buffer.getText(offset, end - offset, text);
1064:
1065: boolean startOfLine = (buffer.getLineStartOffset(buffer
1066: .getLineOfOffset(offset)) == offset);
1067:
1068: SearchMatcher.Match occur = matcher.nextMatch(
1069: new SegmentCharSequence(text, false), startOfLine,
1070: endOfLine, counter == 0, false);
1071: if (occur == null)
1072: break loop;
1073:
1074: String found = new String(text.array, text.offset
1075: + occur.start, occur.end - occur.start);
1076:
1077: int length = replaceOne(view, buffer, occur, offset, found,
1078: smartCaseReplace);
1079: if (length == -1)
1080: offset += occur.end;
1081: else {
1082: offset += occur.start + length;
1083: end += (length - found.length());
1084: occurCount++;
1085: }
1086:
1087: if (matcher.isMatchingEOL()) {
1088: if (offset < buffer.getLength())
1089: offset += 1;
1090: else
1091: break loop;
1092:
1093: if (offset >= end)
1094: break loop;
1095: }
1096: }
1097:
1098: return occurCount;
1099: } //}}}
1100:
1101: //{{{ replaceOne() method
1102: /**
1103: * Replace one occurrence of the search string with the
1104: * replacement string.
1105: */
1106: private static int replaceOne(View view, Buffer buffer,
1107: SearchMatcher.Match occur, int offset, String found,
1108: boolean smartCaseReplace) throws Exception {
1109: String subst = replaceOne(view, occur, found);
1110: if (smartCaseReplace && ignoreCase) {
1111: int strCase = TextUtilities.getStringCase(found);
1112: if (strCase == TextUtilities.LOWER_CASE)
1113: subst = subst.toLowerCase();
1114: else if (strCase == TextUtilities.UPPER_CASE)
1115: subst = subst.toUpperCase();
1116: else if (strCase == TextUtilities.TITLE_CASE)
1117: subst = TextUtilities.toTitleCase(subst);
1118: }
1119:
1120: if (subst != null) {
1121: int start = offset + occur.start;
1122: int end = offset + occur.end;
1123:
1124: if (end - start > 0)
1125: buffer.remove(start, end - start);
1126: buffer.insert(start, subst);
1127: return subst.length();
1128: } else
1129: return -1;
1130: } //}}}
1131:
1132: //{{{ replaceOne() method
1133: private static String replaceOne(View view,
1134: SearchMatcher.Match occur, String found) throws Exception {
1135: if (regexp) {
1136: if (replaceMethod != null)
1137: return regexpBeanShellReplace(view, occur);
1138: else
1139: return regexpReplace(occur, found);
1140: } else {
1141: if (replaceMethod != null)
1142: return literalBeanShellReplace(view, found);
1143: else
1144: return replace;
1145: }
1146: } //}}}
1147:
1148: //{{{ regexpBeanShellReplace() method
1149: private static String regexpBeanShellReplace(View view,
1150: SearchMatcher.Match occur) throws Exception {
1151: for (int i = 0; i < occur.substitutions.length; i++) {
1152: replaceNS.setVariable("_" + i, occur.substitutions[i]);
1153: }
1154:
1155: Object obj = BeanShell.runCachedBlock(replaceMethod, view,
1156: replaceNS);
1157: if (obj == null)
1158: return "";
1159: else
1160: return obj.toString();
1161: } //}}}
1162:
1163: //{{{ regexpReplace() method
1164: private static String regexpReplace(SearchMatcher.Match occur,
1165: String found) throws Exception {
1166: StringBuilder buf = new StringBuilder();
1167:
1168: for (int i = 0; i < replace.length(); i++) {
1169: char ch = replace.charAt(i);
1170: switch (ch) {
1171: case '$':
1172: if (i == replace.length() - 1) {
1173: buf.append(ch);
1174: break;
1175: }
1176:
1177: ch = replace.charAt(++i);
1178: if (ch == '$')
1179: buf.append('$');
1180: else if (ch == '0')
1181: buf.append(found);
1182: else if (Character.isDigit(ch)) {
1183: int n = ch - '0';
1184: if (n < occur.substitutions.length) {
1185: buf.append(occur.substitutions[n]);
1186: }
1187: }
1188: break;
1189: case '\\':
1190: if (i == replace.length() - 1) {
1191: buf.append('\\');
1192: break;
1193: }
1194: ch = replace.charAt(++i);
1195: switch (ch) {
1196: case 'n':
1197: buf.append('\n');
1198: break;
1199: case 't':
1200: buf.append('\t');
1201: break;
1202: default:
1203: buf.append(ch);
1204: break;
1205: }
1206: break;
1207: default:
1208: buf.append(ch);
1209: break;
1210: }
1211: }
1212:
1213: return buf.toString();
1214: } //}}}
1215:
1216: //{{{ literalBeanShellReplace() method
1217: private static String literalBeanShellReplace(View view,
1218: String found) throws Exception {
1219: replaceNS.setVariable("_0", found);
1220: Object obj = BeanShell.runCachedBlock(replaceMethod, view,
1221: replaceNS);
1222: if (obj == null)
1223: return "";
1224: else
1225: return obj.toString();
1226: } //}}}
1227:
1228: //{{{ getColumnOnOtherLine() method
1229: /**
1230: * Should be somewhere else...
1231: */
1232: private static int getColumnOnOtherLine(Buffer buffer, int line,
1233: int col) {
1234: int returnValue = buffer.getOffsetOfVirtualColumn(line, col,
1235: null);
1236: if (returnValue == -1)
1237: return buffer.getLineEndOffset(line) - 1;
1238: else
1239: return buffer.getLineStartOffset(line) + returnValue;
1240: } //}}}
1241:
1242: //}}}
1243: }
|