[25584] | 1 | /*
|
---|
| 2 | * 02/19/2006
|
---|
| 3 | *
|
---|
| 4 | * SearchEngine.java - Handles find/replace operations in an RTextArea.
|
---|
| 5 | *
|
---|
| 6 | * This library is distributed under a modified BSD license. See the included
|
---|
| 7 | * RSyntaxTextArea.License.txt file for details.
|
---|
| 8 | */
|
---|
| 9 | package org.fife.ui.rtextarea;
|
---|
| 10 |
|
---|
| 11 | import java.awt.Insets;
|
---|
| 12 | import java.awt.Point;
|
---|
| 13 | import java.awt.Rectangle;
|
---|
| 14 | import java.util.ArrayList;
|
---|
| 15 | import java.util.List;
|
---|
| 16 | import java.util.regex.Matcher;
|
---|
| 17 | import java.util.regex.Pattern;
|
---|
| 18 | import java.util.regex.PatternSyntaxException;
|
---|
| 19 | import javax.swing.JTextArea;
|
---|
| 20 | import javax.swing.text.BadLocationException;
|
---|
| 21 | import javax.swing.text.Caret;
|
---|
| 22 |
|
---|
| 23 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
|
---|
| 24 | import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
|
---|
| 25 |
|
---|
| 26 |
|
---|
| 27 | /**
|
---|
| 28 | * A singleton class that can perform advanced find/replace operations
|
---|
| 29 | * in an {@link RTextArea}. Simply create a {@link SearchContext} and call
|
---|
| 30 | * one of the following methods:
|
---|
| 31 | *
|
---|
| 32 | * <ul>
|
---|
| 33 | * <li>{@link #find(JTextArea, SearchContext)}
|
---|
| 34 | * <li>{@link #replace(RTextArea, SearchContext)}
|
---|
| 35 | * <li>{@link #replaceAll(RTextArea, SearchContext)}
|
---|
| 36 | * </ul>
|
---|
| 37 | *
|
---|
| 38 | * @author Robert Futrell
|
---|
| 39 | * @version 1.0
|
---|
| 40 | * @see SearchContext
|
---|
| 41 | */
|
---|
| 42 | public class SearchEngine {
|
---|
| 43 |
|
---|
| 44 |
|
---|
| 45 | /**
|
---|
| 46 | * Private constructor to prevent instantiation.
|
---|
| 47 | */
|
---|
| 48 | private SearchEngine() {
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 |
|
---|
| 52 | /**
|
---|
| 53 | * Finds the next instance of the string/regular expression specified
|
---|
| 54 | * from the caret position. If a match is found, it is selected in this
|
---|
| 55 | * text area.
|
---|
| 56 | *
|
---|
| 57 | * @param textArea The text area in which to search.
|
---|
| 58 | * @param context What to search for and all search options.
|
---|
| 59 | * @return Whether a match was found (and thus selected).
|
---|
| 60 | * @throws PatternSyntaxException If this is a regular expression search
|
---|
| 61 | * but the search text is an invalid regular expression.
|
---|
| 62 | * @see #replace(RTextArea, SearchContext)
|
---|
| 63 | * @see #replaceAll(RTextArea, SearchContext)
|
---|
| 64 | */
|
---|
| 65 | public static boolean find(JTextArea textArea, SearchContext context) {
|
---|
| 66 |
|
---|
| 67 | String text = context.getSearchFor();
|
---|
| 68 | if (text==null || text.length()==0) {
|
---|
| 69 | return false;
|
---|
| 70 | }
|
---|
| 71 |
|
---|
| 72 | // Be smart about what position we're "starting" at. We don't want
|
---|
| 73 | // to find a match in the currently selected text (if any), so we
|
---|
| 74 | // start searching AFTER the selection if searching forward, and
|
---|
| 75 | // BEFORE the selection if searching backward.
|
---|
| 76 | Caret c = textArea.getCaret();
|
---|
| 77 | boolean forward = context.getSearchForward();
|
---|
| 78 | int start = forward ? Math.max(c.getDot(), c.getMark()) :
|
---|
| 79 | Math.min(c.getDot(), c.getMark());
|
---|
| 80 |
|
---|
| 81 | String findIn = getFindInText(textArea, start, forward);
|
---|
| 82 | if (findIn==null || findIn.length()==0) return false;
|
---|
| 83 |
|
---|
| 84 | // Find the next location of the text we're searching for.
|
---|
| 85 | if (!context.isRegularExpression()) {
|
---|
| 86 | int pos = getNextMatchPos(text, findIn, forward,
|
---|
| 87 | context.getMatchCase(), context.getWholeWord());
|
---|
| 88 | findIn = null; // May help garbage collecting.
|
---|
| 89 | if (pos!=-1) {
|
---|
| 90 | // Without this, if JTextArea isn't in focus, selection
|
---|
| 91 | // won't appear selected.
|
---|
| 92 | c.setSelectionVisible(true);
|
---|
| 93 | pos = forward ? start+pos : pos;
|
---|
| 94 | selectAndPossiblyCenter(textArea, pos, pos+text.length());
|
---|
| 95 | return true;
|
---|
| 96 | }
|
---|
| 97 | }
|
---|
| 98 | else {
|
---|
| 99 | // Regex matches can have varying widths. The returned point's
|
---|
| 100 | // x- and y-values represent the start and end indices of the
|
---|
| 101 | // match in findIn.
|
---|
| 102 | Point regExPos = getNextMatchPosRegEx(text, findIn, forward,
|
---|
| 103 | context.getMatchCase(), context.getWholeWord());
|
---|
| 104 | findIn = null; // May help garbage collecting.
|
---|
| 105 | if (regExPos!=null) {
|
---|
| 106 | // Without this, if JTextArea isn't in focus, selection
|
---|
| 107 | // won't appear selected.
|
---|
| 108 | c.setSelectionVisible(true);
|
---|
| 109 | if (forward) {
|
---|
| 110 | regExPos.translate(start, start);
|
---|
| 111 | }
|
---|
| 112 | selectAndPossiblyCenter(textArea, regExPos.x, regExPos.y);
|
---|
| 113 | return true;
|
---|
| 114 | }
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | // No match.
|
---|
| 118 | return false;
|
---|
| 119 |
|
---|
| 120 | }
|
---|
| 121 |
|
---|
| 122 |
|
---|
| 123 | /**
|
---|
| 124 | * Returns the text in which to search, as a string. This is used
|
---|
| 125 | * internally to grab the smallest buffer possible in which to search.
|
---|
| 126 | */
|
---|
| 127 | private static String getFindInText(JTextArea textArea, int start,
|
---|
| 128 | boolean forward) {
|
---|
| 129 |
|
---|
| 130 | // Be smart about the text we grab to search in. We grab more than
|
---|
| 131 | // a single line because our searches can return multiline results.
|
---|
| 132 | // We copy only the chars that will be searched through.
|
---|
| 133 | String findIn = null;
|
---|
| 134 | if (forward) {
|
---|
| 135 | try {
|
---|
| 136 | findIn = textArea.getText(start,
|
---|
| 137 | textArea.getDocument().getLength()-start);
|
---|
| 138 | } catch (BadLocationException ble) {
|
---|
| 139 | // Never happens; findIn will be null anyway.
|
---|
| 140 | ble.printStackTrace();
|
---|
| 141 | }
|
---|
| 142 | }
|
---|
| 143 | else { // backward
|
---|
| 144 | try {
|
---|
| 145 | findIn = textArea.getText(0, start);
|
---|
| 146 | } catch (BadLocationException ble) {
|
---|
| 147 | // Never happens; findIn will be null anyway.
|
---|
| 148 | ble.printStackTrace();
|
---|
| 149 | }
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | return findIn;
|
---|
| 153 |
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 |
|
---|
| 157 | /**
|
---|
| 158 | * This method is called internally by
|
---|
| 159 | * <code>getNextMatchPosRegExImpl</code> and is used to get the locations
|
---|
| 160 | * of all regular-expression matches, and possibly their replacement
|
---|
| 161 | * strings.<p>
|
---|
| 162 | *
|
---|
| 163 | * Returns either:
|
---|
| 164 | * <ul>
|
---|
| 165 | * <li>A list of points representing the starting and ending positions
|
---|
| 166 | * of all matches returned by the specified matcher, or
|
---|
| 167 | * <li>A list of <code>RegExReplaceInfo</code>s describing the matches
|
---|
| 168 | * found by the matcher and the replacement strings for each.
|
---|
| 169 | * </ul>
|
---|
| 170 | *
|
---|
| 171 | * If <code>replacement</code> is <code>null</code>, this method call is
|
---|
| 172 | * assumed to be part of a "find" operation and points are returned. If
|
---|
| 173 | * if is non-<code>null</code>, it is assumed to be part of a "replace"
|
---|
| 174 | * operation and the <code>RegExReplaceInfo</code>s are returned.<p>
|
---|
| 175 | *
|
---|
| 176 | * @param m The matcher.
|
---|
| 177 | * @param replaceStr The string to replace matches with. This is a
|
---|
| 178 | * "template" string and can contain captured group references in
|
---|
| 179 | * the form "<code>${digit}</code>".
|
---|
| 180 | * @return A list of result objects.
|
---|
| 181 | * @throws IndexOutOfBoundsException If <code>replaceStr</code> references
|
---|
| 182 | * an invalid group (less than zero or greater than the number of
|
---|
| 183 | * groups matched).
|
---|
| 184 | */
|
---|
| 185 | private static List getMatches(Matcher m, String replaceStr) {
|
---|
| 186 | ArrayList matches = new ArrayList();
|
---|
| 187 | while (m.find()) {
|
---|
| 188 | Point loc = new Point(m.start(), m.end());
|
---|
| 189 | if (replaceStr==null) { // Find, not replace.
|
---|
| 190 | matches.add(loc);
|
---|
| 191 | }
|
---|
| 192 | else { // Replace.
|
---|
| 193 | matches.add(new RegExReplaceInfo(m.group(0), loc.x, loc.y,
|
---|
| 194 | getReplacementText(m, replaceStr)));
|
---|
| 195 | }
|
---|
| 196 | }
|
---|
| 197 | return matches;
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 |
|
---|
| 201 | /**
|
---|
| 202 | * Searches <code>searchIn</code> for an occurrence of
|
---|
| 203 | * <code>searchFor</code> either forwards or backwards, matching
|
---|
| 204 | * case or not.<p>
|
---|
| 205 | *
|
---|
| 206 | * Most clients will have no need to call this method directly.
|
---|
| 207 | *
|
---|
| 208 | * @param searchFor The string to look for.
|
---|
| 209 | * @param searchIn The string to search in.
|
---|
| 210 | * @param forward Whether to search forward or backward in
|
---|
| 211 | * <code>searchIn</code>.
|
---|
| 212 | * @param matchCase If <code>true</code>, do a case-sensitive search for
|
---|
| 213 | * <code>searchFor</code>.
|
---|
| 214 | * @param wholeWord If <code>true</code>, <code>searchFor</code>
|
---|
| 215 | * occurrences embedded in longer words in <code>searchIn</code>
|
---|
| 216 | * don't count as matches.
|
---|
| 217 | * @return The starting position of a match, or <code>-1</code> if no
|
---|
| 218 | * match was found.
|
---|
| 219 | */
|
---|
| 220 | public static final int getNextMatchPos(String searchFor, String searchIn,
|
---|
| 221 | boolean forward, boolean matchCase,
|
---|
| 222 | boolean wholeWord) {
|
---|
| 223 |
|
---|
| 224 | // Make our variables lower case if we're ignoring case.
|
---|
| 225 | if (!matchCase) {
|
---|
| 226 | return getNextMatchPosImpl(searchFor.toLowerCase(),
|
---|
| 227 | searchIn.toLowerCase(), forward,
|
---|
| 228 | matchCase, wholeWord);
|
---|
| 229 | }
|
---|
| 230 |
|
---|
| 231 | return getNextMatchPosImpl(searchFor, searchIn, forward,
|
---|
| 232 | matchCase, wholeWord);
|
---|
| 233 |
|
---|
| 234 | }
|
---|
| 235 |
|
---|
| 236 |
|
---|
| 237 | /**
|
---|
| 238 | * Actually does the work of matching; assumes searchFor and searchIn
|
---|
| 239 | * are already upper/lower-cased appropriately.<br>
|
---|
| 240 | * The reason this method is here is to attempt to speed up
|
---|
| 241 | * <code>FindInFilesDialog</code>; since it repeatedly calls
|
---|
| 242 | * this method instead of <code>getNextMatchPos</code>, it gets better
|
---|
| 243 | * performance as it no longer has to allocate a lower-cased string for
|
---|
| 244 | * every call.
|
---|
| 245 | *
|
---|
| 246 | * @param searchFor The string to search for.
|
---|
| 247 | * @param searchIn The string to search in.
|
---|
| 248 | * @param goForward Whether the search is forward or backward.
|
---|
| 249 | * @param matchCase Whether the search is case-sensitive.
|
---|
| 250 | * @param wholeWord Whether only whole words should be matched.
|
---|
| 251 | * @return The location of the next match, or <code>-1</code> if no
|
---|
| 252 | * match was found.
|
---|
| 253 | */
|
---|
| 254 | private static final int getNextMatchPosImpl(String searchFor,
|
---|
| 255 | String searchIn, boolean goForward,
|
---|
| 256 | boolean matchCase, boolean wholeWord) {
|
---|
| 257 |
|
---|
| 258 | if (wholeWord) {
|
---|
| 259 | int len = searchFor.length();
|
---|
| 260 | int temp = goForward ? 0 : searchIn.length();
|
---|
| 261 | int tempChange = goForward ? 1 : -1;
|
---|
| 262 | while (true) {
|
---|
| 263 | if (goForward)
|
---|
| 264 | temp = searchIn.indexOf(searchFor, temp);
|
---|
| 265 | else
|
---|
| 266 | temp = searchIn.lastIndexOf(searchFor, temp);
|
---|
| 267 | if (temp!=-1) {
|
---|
| 268 | if (isWholeWord(searchIn, temp, len)) {
|
---|
| 269 | return temp;
|
---|
| 270 | }
|
---|
| 271 | else {
|
---|
| 272 | temp += tempChange;
|
---|
| 273 | continue;
|
---|
| 274 | }
|
---|
| 275 | }
|
---|
| 276 | return temp; // Always -1.
|
---|
| 277 | }
|
---|
| 278 | }
|
---|
| 279 | else {
|
---|
| 280 | return goForward ? searchIn.indexOf(searchFor) :
|
---|
| 281 | searchIn.lastIndexOf(searchFor);
|
---|
| 282 | }
|
---|
| 283 |
|
---|
| 284 | }
|
---|
| 285 |
|
---|
| 286 |
|
---|
| 287 | /**
|
---|
| 288 | * Searches <code>searchIn</code> for an occurrence of <code>regEx</code>
|
---|
| 289 | * either forwards or backwards, matching case or not.
|
---|
| 290 | *
|
---|
| 291 | * @param regEx The regular expression to look for.
|
---|
| 292 | * @param searchIn The string to search in.
|
---|
| 293 | * @param goForward Whether to search forward. If <code>false</code>,
|
---|
| 294 | * search backward.
|
---|
| 295 | * @param matchCase Whether or not to do a case-sensitive search for
|
---|
| 296 | * <code>regEx</code>.
|
---|
| 297 | * @param wholeWord If <code>true</code>, <code>regEx</code>
|
---|
| 298 | * occurrences embedded in longer words in <code>searchIn</code>
|
---|
| 299 | * don't count as matches.
|
---|
| 300 | * @return A <code>Point</code> representing the starting and ending
|
---|
| 301 | * position of the match, or <code>null</code> if no match was
|
---|
| 302 | * found.
|
---|
| 303 | * @throws PatternSyntaxException If <code>regEx</code> is an invalid
|
---|
| 304 | * regular expression.
|
---|
| 305 | * @see #getNextMatchPos
|
---|
| 306 | */
|
---|
| 307 | private static Point getNextMatchPosRegEx(String regEx,
|
---|
| 308 | CharSequence searchIn, boolean goForward,
|
---|
| 309 | boolean matchCase, boolean wholeWord) {
|
---|
| 310 | return (Point)getNextMatchPosRegExImpl(regEx, searchIn, goForward,
|
---|
| 311 | matchCase, wholeWord, null);
|
---|
| 312 | }
|
---|
| 313 |
|
---|
| 314 |
|
---|
| 315 | /**
|
---|
| 316 | * Searches <code>searchIn</code> for an occurrence of <code>regEx</code>
|
---|
| 317 | * either forwards or backwards, matching case or not.
|
---|
| 318 | *
|
---|
| 319 | * @param regEx The regular expression to look for.
|
---|
| 320 | * @param searchIn The string to search in.
|
---|
| 321 | * @param goForward Whether to search forward. If <code>false</code>,
|
---|
| 322 | * search backward.
|
---|
| 323 | * @param matchCase Whether or not to do a case-sensitive search for
|
---|
| 324 | * <code>regEx</code>.
|
---|
| 325 | * @param wholeWord If <code>true</code>, <code>regEx</code>
|
---|
| 326 | * occurrences embedded in longer words in <code>searchIn</code>
|
---|
| 327 | * don't count as matches.
|
---|
| 328 | * @param replaceStr The string that will replace the match found (if
|
---|
| 329 | * a match is found). The object returned will contain the
|
---|
| 330 | * replacement string with matched groups substituted. If this
|
---|
| 331 | * value is <code>null</code>, it is assumed this call is part of a
|
---|
| 332 | * "find" instead of a "replace" operation.
|
---|
| 333 | * @return If <code>replaceStr</code> is <code>null</code>, a
|
---|
| 334 | * <code>Point</code> representing the starting and ending points
|
---|
| 335 | * of the match. If it is non-<code>null</code>, an object with
|
---|
| 336 | * information about the match and the morphed string to replace
|
---|
| 337 | * it with. If no match is found, <code>null</code> is returned.
|
---|
| 338 | * @throws PatternSyntaxException If <code>regEx</code> is an invalid
|
---|
| 339 | * regular expression.
|
---|
| 340 | * @throws IndexOutOfBoundsException If <code>replaceStr</code> references
|
---|
| 341 | * an invalid group (less than zero or greater than the number of
|
---|
| 342 | * groups matched).
|
---|
| 343 | * @see #getNextMatchPos
|
---|
| 344 | */
|
---|
| 345 | private static Object getNextMatchPosRegExImpl(String regEx,
|
---|
| 346 | CharSequence searchIn, boolean goForward,
|
---|
| 347 | boolean matchCase, boolean wholeWord,
|
---|
| 348 | String replaceStr) {
|
---|
| 349 |
|
---|
| 350 | if (wholeWord) {
|
---|
| 351 | regEx = "\\b" + regEx + "\\b";
|
---|
| 352 | }
|
---|
| 353 |
|
---|
| 354 | // Make a pattern that takes into account whether or not to match case.
|
---|
| 355 | int flags = Pattern.MULTILINE; // '^' and '$' are done per line.
|
---|
| 356 | flags |= matchCase ? 0 : (Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE);
|
---|
| 357 | Pattern pattern = Pattern.compile(regEx, flags);
|
---|
| 358 |
|
---|
| 359 | // Make a Matcher to find the regEx instances.
|
---|
| 360 | Matcher m = pattern.matcher(searchIn);
|
---|
| 361 |
|
---|
| 362 | // Search forwards
|
---|
| 363 | if (goForward) {
|
---|
| 364 | if (m.find()) {
|
---|
| 365 | if (replaceStr==null) { // Find, not replace.
|
---|
| 366 | return new Point(m.start(), m.end());
|
---|
| 367 | }
|
---|
| 368 | // Otherwise, replace
|
---|
| 369 | return new RegExReplaceInfo(m.group(0),
|
---|
| 370 | m.start(), m.end(),
|
---|
| 371 | getReplacementText(m, replaceStr));
|
---|
| 372 | }
|
---|
| 373 | }
|
---|
| 374 |
|
---|
| 375 | // Search backwards
|
---|
| 376 | else {
|
---|
| 377 | List matches = getMatches(m, replaceStr);
|
---|
| 378 | if (!matches.isEmpty()) {
|
---|
| 379 | return matches.get(matches.size()-1);
|
---|
| 380 | }
|
---|
| 381 | }
|
---|
| 382 |
|
---|
| 383 | return null; // No match found
|
---|
| 384 |
|
---|
| 385 | }
|
---|
| 386 |
|
---|
| 387 |
|
---|
| 388 | /**
|
---|
| 389 | * Returns information on how to implement a regular expression "replace"
|
---|
| 390 | * action in the specified text with the specified replacement string.
|
---|
| 391 | *
|
---|
| 392 | * @param searchIn The string to search in.
|
---|
| 393 | * @param context The search options.
|
---|
| 394 | * @return A <code>RegExReplaceInfo</code> object describing how to
|
---|
| 395 | * implement the replace.
|
---|
| 396 | * @throws PatternSyntaxException If the search text is an invalid regular
|
---|
| 397 | * expression.
|
---|
| 398 | * @throws IndexOutOfBoundsException If the replacement text references an
|
---|
| 399 | * invalid group (less than zero or greater than the number of
|
---|
| 400 | * groups matched).
|
---|
| 401 | * @see #getNextMatchPos
|
---|
| 402 | */
|
---|
| 403 | private static RegExReplaceInfo getRegExReplaceInfo(String searchIn,
|
---|
| 404 | SearchContext context) {
|
---|
| 405 | // Can't pass null to getNextMatchPosRegExImpl or it'll think
|
---|
| 406 | // you're doing a "find" operation instead of "replace, and return a
|
---|
| 407 | // Point.
|
---|
| 408 | String replacement = context.getReplaceWith();
|
---|
| 409 | if (replacement==null) {
|
---|
| 410 | replacement = "";
|
---|
| 411 | }
|
---|
| 412 | String regex = context.getSearchFor();
|
---|
| 413 | boolean goForward = context.getSearchForward();
|
---|
| 414 | boolean matchCase = context.getMatchCase();
|
---|
| 415 | boolean wholeWord = context.getWholeWord();
|
---|
| 416 | return (RegExReplaceInfo)getNextMatchPosRegExImpl(regex, searchIn,
|
---|
| 417 | goForward, matchCase, wholeWord, replacement);
|
---|
| 418 | }
|
---|
| 419 |
|
---|
| 420 |
|
---|
| 421 | /**
|
---|
| 422 | * Called internally by <code>getMatches()</code>. This method assumes
|
---|
| 423 | * that the specified matcher has just found a match, and that you want
|
---|
| 424 | * to get the string with which to replace that match.<p>
|
---|
| 425 | *
|
---|
| 426 | * Escapes simply insert the escaped character, except for <code>\n</code>
|
---|
| 427 | * and <code>\t</code>, which insert a newline and tab respectively.
|
---|
| 428 | * Substrings of the form <code>$\d+</code> are considered to be matched
|
---|
| 429 | * groups. To include a literal dollar sign in your template, escape it
|
---|
| 430 | * (i.e. <code>\$</code>).<p>
|
---|
| 431 | *
|
---|
| 432 | * Most clients will have no need to call this method directly.
|
---|
| 433 | *
|
---|
| 434 | * @param m The matcher.
|
---|
| 435 | * @param template The template for the replacement string. For example,
|
---|
| 436 | * "<code>foo</code>" would yield the replacement string
|
---|
| 437 | * "<code>foo</code>", while "<code>$1 is the greatest</code>"
|
---|
| 438 | * would yield different values depending on the value of the first
|
---|
| 439 | * captured group in the match.
|
---|
| 440 | * @return The string to replace the match with.
|
---|
| 441 | * @throws IndexOutOfBoundsException If <code>template</code> references
|
---|
| 442 | * an invalid group (less than zero or greater than the number of
|
---|
| 443 | * groups matched).
|
---|
| 444 | */
|
---|
| 445 | public static String getReplacementText(Matcher m, CharSequence template) {
|
---|
| 446 |
|
---|
| 447 | // NOTE: This code was mostly ripped off from J2SE's Matcher
|
---|
| 448 | // class.
|
---|
| 449 |
|
---|
| 450 | // Process substitution string to replace group references with groups
|
---|
| 451 | int cursor = 0;
|
---|
| 452 | StringBuffer result = new StringBuffer();
|
---|
| 453 |
|
---|
| 454 | while (cursor < template.length()) {
|
---|
| 455 |
|
---|
| 456 | char nextChar = template.charAt(cursor);
|
---|
| 457 |
|
---|
| 458 | if (nextChar == '\\') { // Escape character.
|
---|
| 459 | nextChar = template.charAt(++cursor);
|
---|
| 460 | switch (nextChar) { // Special cases.
|
---|
| 461 | case 'n':
|
---|
| 462 | nextChar = '\n';
|
---|
| 463 | break;
|
---|
| 464 | case 't':
|
---|
| 465 | nextChar = '\t';
|
---|
| 466 | break;
|
---|
| 467 | }
|
---|
| 468 | result.append(nextChar);
|
---|
| 469 | cursor++;
|
---|
| 470 | }
|
---|
| 471 | else if (nextChar == '$') { // Group reference.
|
---|
| 472 |
|
---|
| 473 | cursor++; // Skip the '$'.
|
---|
| 474 |
|
---|
| 475 | // The first number is always a group
|
---|
| 476 | int refNum = template.charAt(cursor) - '0';
|
---|
| 477 | if ((refNum < 0)||(refNum > 9)) {
|
---|
| 478 | // This should really be an IllegalArgumentException,
|
---|
| 479 | // but we cheat to keep all "group" errors throwing
|
---|
| 480 | // the same exception type.
|
---|
| 481 | throw new IndexOutOfBoundsException(
|
---|
| 482 | "No group " + template.charAt(cursor));
|
---|
| 483 | }
|
---|
| 484 | cursor++;
|
---|
| 485 |
|
---|
| 486 | // Capture the largest legal group string
|
---|
| 487 | boolean done = false;
|
---|
| 488 | while (!done) {
|
---|
| 489 | if (cursor >= template.length()) {
|
---|
| 490 | break;
|
---|
| 491 | }
|
---|
| 492 | int nextDigit = template.charAt(cursor) - '0';
|
---|
| 493 | if ((nextDigit < 0)||(nextDigit > 9)) { // not a number
|
---|
| 494 | break;
|
---|
| 495 | }
|
---|
| 496 | int newRefNum = (refNum * 10) + nextDigit;
|
---|
| 497 | if (m.groupCount() < newRefNum) {
|
---|
| 498 | done = true;
|
---|
| 499 | }
|
---|
| 500 | else {
|
---|
| 501 | refNum = newRefNum;
|
---|
| 502 | cursor++;
|
---|
| 503 | }
|
---|
| 504 | }
|
---|
| 505 |
|
---|
| 506 | // Append group
|
---|
| 507 | if (m.group(refNum) != null)
|
---|
| 508 | result.append(m.group(refNum));
|
---|
| 509 |
|
---|
| 510 | }
|
---|
| 511 |
|
---|
| 512 | else {
|
---|
| 513 | result.append(nextChar);
|
---|
| 514 | cursor++;
|
---|
| 515 | }
|
---|
| 516 |
|
---|
| 517 | }
|
---|
| 518 |
|
---|
| 519 | return result.toString();
|
---|
| 520 |
|
---|
| 521 | }
|
---|
| 522 |
|
---|
| 523 |
|
---|
| 524 | /**
|
---|
| 525 | * Returns whether the characters on either side of
|
---|
| 526 | * <code>substr(searchIn, startPos, startPos+searchStringLength)</code>
|
---|
| 527 | * are <em>not</em> letters or digits.
|
---|
| 528 | */
|
---|
| 529 | private static final boolean isWholeWord(CharSequence searchIn,
|
---|
| 530 | int offset, int len) {
|
---|
| 531 |
|
---|
| 532 | boolean wsBefore, wsAfter;
|
---|
| 533 |
|
---|
| 534 | try {
|
---|
| 535 | wsBefore = !Character.isLetterOrDigit(searchIn.charAt(offset - 1));
|
---|
| 536 | } catch (IndexOutOfBoundsException e) { wsBefore = true; }
|
---|
| 537 | try {
|
---|
| 538 | wsAfter = !Character.isLetterOrDigit(searchIn.charAt(offset + len));
|
---|
| 539 | } catch (IndexOutOfBoundsException e) { wsAfter = true; }
|
---|
| 540 |
|
---|
| 541 | return wsBefore && wsAfter;
|
---|
| 542 |
|
---|
| 543 | }
|
---|
| 544 |
|
---|
| 545 |
|
---|
| 546 | /**
|
---|
| 547 | * Makes the caret's dot and mark the same location so that, for the
|
---|
| 548 | * next search in the specified direction, a match will be found even
|
---|
| 549 | * if it was within the original dot and mark's selection.
|
---|
| 550 | *
|
---|
| 551 | * @param textArea The text area.
|
---|
| 552 | * @param forward Whether the search will be forward through the
|
---|
| 553 | * document (<code>false</code> means backward).
|
---|
| 554 | * @return The new dot and mark position.
|
---|
| 555 | */
|
---|
| 556 | private static int makeMarkAndDotEqual(JTextArea textArea,
|
---|
| 557 | boolean forward) {
|
---|
| 558 | Caret c = textArea.getCaret();
|
---|
| 559 | int val = forward ? Math.min(c.getDot(), c.getMark()) :
|
---|
| 560 | Math.max(c.getDot(), c.getMark());
|
---|
| 561 | c.setDot(val);
|
---|
| 562 | return val;
|
---|
| 563 | }
|
---|
| 564 |
|
---|
| 565 |
|
---|
| 566 | /**
|
---|
| 567 | * Finds the next instance of the regular expression specified from
|
---|
| 568 | * the caret position. If a match is found, it is replaced with
|
---|
| 569 | * the specified replacement string.
|
---|
| 570 | *
|
---|
| 571 | * @param textArea The text area in which to search.
|
---|
| 572 | * @param context What to search for and all search options.
|
---|
| 573 | * @return Whether a match was found (and thus replaced).
|
---|
| 574 | * @throws PatternSyntaxException If this is a regular expression search
|
---|
| 575 | * but the search text is an invalid regular expression.
|
---|
| 576 | * @throws IndexOutOfBoundsException If this is a regular expression search
|
---|
| 577 | * but the replacement text references an invalid group (less than
|
---|
| 578 | * zero or greater than the number of groups matched).
|
---|
| 579 | * @see #replace(RTextArea, SearchContext)
|
---|
| 580 | * @see #find(JTextArea, SearchContext)
|
---|
| 581 | */
|
---|
| 582 | private static boolean regexReplace(JTextArea textArea,
|
---|
| 583 | SearchContext context) throws PatternSyntaxException {
|
---|
| 584 |
|
---|
| 585 | // Be smart about what position we're "starting" at. For example,
|
---|
| 586 | // if they are searching backwards and there is a selection such that
|
---|
| 587 | // the dot is past the mark, and the selection is the text for which
|
---|
| 588 | // you're searching, this search will find and return the current
|
---|
| 589 | // selection. So, in that case we start at the beginning of the
|
---|
| 590 | // selection.
|
---|
| 591 | Caret c = textArea.getCaret();
|
---|
| 592 | boolean forward = context.getSearchForward();
|
---|
| 593 | int start = makeMarkAndDotEqual(textArea, forward);
|
---|
| 594 |
|
---|
| 595 | String findIn = getFindInText(textArea, start, forward);
|
---|
| 596 | if (findIn==null) return false;
|
---|
| 597 |
|
---|
| 598 | // Find the next location of the text we're searching for.
|
---|
| 599 | RegExReplaceInfo info = getRegExReplaceInfo(findIn, context);
|
---|
| 600 |
|
---|
| 601 | findIn = null; // May help garbage collecting.
|
---|
| 602 |
|
---|
| 603 | // If a match was found, do the replace and return!
|
---|
| 604 | if (info!=null) {
|
---|
| 605 |
|
---|
| 606 | // Without this, if JTextArea isn't in focus, selection won't
|
---|
| 607 | // appear selected.
|
---|
| 608 | c.setSelectionVisible(true);
|
---|
| 609 |
|
---|
| 610 | int matchStart = info.getStartIndex();
|
---|
| 611 | int matchEnd = info.getEndIndex();
|
---|
| 612 | if (forward) {
|
---|
| 613 | matchStart += start;
|
---|
| 614 | matchEnd += start;
|
---|
| 615 | }
|
---|
| 616 | selectAndPossiblyCenter(textArea, matchStart, matchEnd);
|
---|
| 617 | textArea.replaceSelection(info.getReplacement());
|
---|
| 618 |
|
---|
| 619 | return true;
|
---|
| 620 |
|
---|
| 621 | }
|
---|
| 622 |
|
---|
| 623 | // No match.
|
---|
| 624 | return false;
|
---|
| 625 |
|
---|
| 626 | }
|
---|
| 627 |
|
---|
| 628 |
|
---|
| 629 | /**
|
---|
| 630 | * Finds the next instance of the text/regular expression specified from
|
---|
| 631 | * the caret position. If a match is found, it is replaced with the
|
---|
| 632 | * specified replacement string.
|
---|
| 633 | *
|
---|
| 634 | * @param textArea The text area in which to search.
|
---|
| 635 | * @param context What to search for and all search options.
|
---|
| 636 | * @return Whether a match was found (and thus replaced).
|
---|
| 637 | * @throws PatternSyntaxException If this is a regular expression search
|
---|
| 638 | * but the search text is an invalid regular expression.
|
---|
| 639 | * @throws IndexOutOfBoundsException If this is a regular expression search
|
---|
| 640 | * but the replacement text references an invalid group (less than
|
---|
| 641 | * zero or greater than the number of groups matched).
|
---|
| 642 | * @see #replaceAll(RTextArea, SearchContext)
|
---|
| 643 | * @see #find(JTextArea, SearchContext)
|
---|
| 644 | */
|
---|
| 645 | public static boolean replace(RTextArea textArea, SearchContext context)
|
---|
| 646 | throws PatternSyntaxException {
|
---|
| 647 |
|
---|
| 648 | String toFind = context.getSearchFor();
|
---|
| 649 | if (toFind==null || toFind.length()==0) {
|
---|
| 650 | return false;
|
---|
| 651 | }
|
---|
| 652 |
|
---|
| 653 | textArea.beginAtomicEdit();
|
---|
| 654 | try {
|
---|
| 655 |
|
---|
| 656 | // Regular expression replacements have their own method.
|
---|
| 657 | if (context.isRegularExpression()) {
|
---|
| 658 | return regexReplace(textArea, context);
|
---|
| 659 | }
|
---|
| 660 |
|
---|
| 661 | // Plain text search. If we find it, replace it!
|
---|
| 662 | // First make the dot and mark equal (get rid of any selection), as
|
---|
| 663 | // a common use-case is the user will use "Find" to select the text
|
---|
| 664 | // to replace, then click "Replace" to replace the current
|
---|
| 665 | // selection. Since our find() method searches from an endpoint of
|
---|
| 666 | // the selection, we must remove the selection to work properly.
|
---|
| 667 | makeMarkAndDotEqual(textArea, context.getSearchForward());
|
---|
| 668 | if (find(textArea, context)) {
|
---|
| 669 | textArea.replaceSelection(context.getReplaceWith());
|
---|
| 670 | return true;
|
---|
| 671 | }
|
---|
| 672 |
|
---|
| 673 | } finally {
|
---|
| 674 | textArea.endAtomicEdit();
|
---|
| 675 | }
|
---|
| 676 |
|
---|
| 677 | return false;
|
---|
| 678 |
|
---|
| 679 | }
|
---|
| 680 |
|
---|
| 681 |
|
---|
| 682 | /**
|
---|
| 683 | * Replaces all instances of the text/regular expression specified in
|
---|
| 684 | * the specified document with the specified replacement.
|
---|
| 685 | *
|
---|
| 686 | * @param textArea The text area in which to search.
|
---|
| 687 | * @param context What to search for and all search options.
|
---|
| 688 | * @throws PatternSyntaxException If this is a regular expression search
|
---|
| 689 | * but the replacement text is an invalid regular expression.
|
---|
| 690 | * @throws IndexOutOfBoundsException If this is a regular expression search
|
---|
| 691 | * but the replacement text references an invalid group (less than
|
---|
| 692 | * zero or greater than the number of groups matched).
|
---|
| 693 | * @see #replace(RTextArea, SearchContext)
|
---|
| 694 | * @see #find(JTextArea, SearchContext)
|
---|
| 695 | */
|
---|
| 696 | public static int replaceAll(RTextArea textArea, SearchContext context)
|
---|
| 697 | throws PatternSyntaxException {
|
---|
| 698 |
|
---|
| 699 | String toFind = context.getSearchFor();
|
---|
| 700 | if (toFind==null || toFind.length()==0) {
|
---|
| 701 | return 0;
|
---|
| 702 | }
|
---|
| 703 |
|
---|
| 704 | int count = 0;
|
---|
| 705 |
|
---|
| 706 | textArea.beginAtomicEdit();
|
---|
| 707 | try {
|
---|
| 708 | int oldOffs = textArea.getCaretPosition();
|
---|
| 709 | textArea.setCaretPosition(0);
|
---|
| 710 | while (SearchEngine.replace(textArea, context)) {
|
---|
| 711 | count++;
|
---|
| 712 | }
|
---|
| 713 | if (count==0) { // If nothing was found, don't move the caret.
|
---|
| 714 | textArea.setCaretPosition(oldOffs);
|
---|
| 715 | }
|
---|
| 716 | } finally {
|
---|
| 717 | textArea.endAtomicEdit();
|
---|
| 718 | }
|
---|
| 719 |
|
---|
| 720 | return count;
|
---|
| 721 |
|
---|
| 722 | }
|
---|
| 723 |
|
---|
| 724 |
|
---|
| 725 | /**
|
---|
| 726 | * Selects a range of text in a text component. If the new selection is
|
---|
| 727 | * outside of the previous viewable rectangle, then the view is centered
|
---|
| 728 | * around the new selection.
|
---|
| 729 | *
|
---|
| 730 | * @param textArea The text component whose selection is to be centered.
|
---|
| 731 | * @param start The start of the range to select.
|
---|
| 732 | * @param end The end of the range to select.
|
---|
| 733 | */
|
---|
| 734 | private static void selectAndPossiblyCenter(JTextArea textArea, int start,
|
---|
| 735 | int end) {
|
---|
| 736 |
|
---|
| 737 | boolean foldsExpanded = false;
|
---|
| 738 | if (textArea instanceof RSyntaxTextArea) {
|
---|
| 739 | RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
|
---|
| 740 | FoldManager fm = rsta.getFoldManager();
|
---|
| 741 | if (fm.isCodeFoldingSupportedAndEnabled()) {
|
---|
| 742 | foldsExpanded = fm.ensureOffsetNotInClosedFold(start);
|
---|
| 743 | foldsExpanded |= fm.ensureOffsetNotInClosedFold(end);
|
---|
| 744 | }
|
---|
| 745 | }
|
---|
| 746 |
|
---|
| 747 | textArea.setSelectionStart(start);
|
---|
| 748 | textArea.setSelectionEnd(end);
|
---|
| 749 |
|
---|
| 750 | Rectangle r = null;
|
---|
| 751 | try {
|
---|
| 752 | r = textArea.modelToView(start);
|
---|
| 753 | if (r==null) { // Not yet visible; i.e. JUnit tests
|
---|
| 754 | return;
|
---|
| 755 | }
|
---|
| 756 | if (end!=start) {
|
---|
| 757 | r = r.union(textArea.modelToView(end));
|
---|
| 758 | }
|
---|
| 759 | } catch (BadLocationException ble) { // Never happens
|
---|
| 760 | ble.printStackTrace();
|
---|
| 761 | textArea.setSelectionStart(start);
|
---|
| 762 | textArea.setSelectionEnd(end);
|
---|
| 763 | return;
|
---|
| 764 | }
|
---|
| 765 |
|
---|
| 766 | Rectangle visible = textArea.getVisibleRect();
|
---|
| 767 |
|
---|
| 768 | // If the new selection is already in the view, don't scroll,
|
---|
| 769 | // as that is visually jarring.
|
---|
| 770 | if (!foldsExpanded && visible.contains(r)) {
|
---|
| 771 | textArea.setSelectionStart(start);
|
---|
| 772 | textArea.setSelectionEnd(end);
|
---|
| 773 | return;
|
---|
| 774 | }
|
---|
| 775 |
|
---|
| 776 | visible.x = r.x - (visible.width - r.width) / 2;
|
---|
| 777 | visible.y = r.y - (visible.height - r.height) / 2;
|
---|
| 778 |
|
---|
| 779 | Rectangle bounds = textArea.getBounds();
|
---|
| 780 | Insets i = textArea.getInsets();
|
---|
| 781 | bounds.x = i.left;
|
---|
| 782 | bounds.y = i.top;
|
---|
| 783 | bounds.width -= i.left + i.right;
|
---|
| 784 | bounds.height -= i.top + i.bottom;
|
---|
| 785 |
|
---|
| 786 | if (visible.x < bounds.x) {
|
---|
| 787 | visible.x = bounds.x;
|
---|
| 788 | }
|
---|
| 789 |
|
---|
| 790 | if (visible.x + visible.width > bounds.x + bounds.width) {
|
---|
| 791 | visible.x = bounds.x + bounds.width - visible.width;
|
---|
| 792 | }
|
---|
| 793 |
|
---|
| 794 | if (visible.y < bounds.y) {
|
---|
| 795 | visible.y = bounds.y;
|
---|
| 796 | }
|
---|
| 797 |
|
---|
| 798 | if (visible.y + visible.height > bounds.y + bounds.height) {
|
---|
| 799 | visible.y = bounds.y + bounds.height - visible.height;
|
---|
| 800 | }
|
---|
| 801 |
|
---|
| 802 | textArea.scrollRectToVisible(visible);
|
---|
| 803 |
|
---|
| 804 | }
|
---|
| 805 |
|
---|
| 806 |
|
---|
| 807 | } |
---|