All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fife.ui.rtextarea.SearchEngine Maven / Gradle / Ivy

Go to download

RSyntaxTextArea is the syntax highlighting text editor for Swing applications. Features include syntax highlighting for 40+ languages, code folding, code completion, regex find and replace, macros, code templates, undo/redo, line numbering and bracket matching.

There is a newer version: 3.5.1
Show newest version
/*
 * 02/19/2006
 *
 * SearchEngine.java - Handles find/replace operations in an RTextArea.
 * Copyright (C) 2006 Robert Futrell
 * robert_futrell at users.sourceforge.net
 * http://fifesoft.com/rsyntaxtextarea
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA.
 */
package org.fife.ui.rtextarea;

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.JTextArea;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;


/**
 * A singleton class that can perform advanced find/replace operations
 * in an RTextArea.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class SearchEngine {


	/**
	 * Private constructor to prevent instantiation.
	 */
	private SearchEngine() {
	}


	/**
	 * Finds the next instance of the string/regular expression specified
	 * from the caret position.  If a match is found, it is selected in this
	 * text area.
	 *
	 * @param textArea The text area in which to search.
	 * @param text The string literal or regular expression to search for.
	 * @param forward Whether to search forward from the caret position or
	 *        backward from it.
	 * @param matchCase Whether the search should be case-sensitive.
	 * @param wholeWord Whether there should be spaces or tabs on either side
	 *        of the match.
	 * @param regex Whether text is a Java regular expression to
	 *        search for.
	 * @return Whether a match was found (and thus selected).
	 * @throws PatternSyntaxException If regex is
	 *         true but text is not a valid regular
	 *         expression.
	 * @see #replace
	 * @see #regexReplace
	 */
	public static boolean find(JTextArea textArea, String text,
							boolean forward, boolean matchCase,
							boolean wholeWord, boolean regex)
									throws PatternSyntaxException {

		// Be smart about what position we're "starting" at.  We don't want
		// to find a match in the currently selected text (if any), so we
		// start searching AFTER the selection if searching forward, and
		// BEFORE the selection if searching backward.
		Caret c = textArea.getCaret();
		int start = forward ? Math.max(c.getDot(), c.getMark()) :
						Math.min(c.getDot(), c.getMark());

		String findIn = getFindInText(textArea, start, forward);
		if (findIn==null || findIn.length()==0) return false;

		// Find the next location of the text we're searching for.
		if (regex==false) {
			int pos = getNextMatchPos(text, findIn, forward,
											matchCase, wholeWord);
			findIn = null; // May help garbage collecting.
			if (pos!=-1) {
				// Without this, if JTextArea isn't in focus, selection
				// won't appear selected.
				c.setSelectionVisible(true);
				pos = forward ? start+pos : pos;
				c.setDot(pos);
				c.moveDot(pos + text.length());
				return true;
			}
		}
		else {
			// Regex matches can have varying widths.  The returned point's
			// x- and y-values represent the start and end indices of the
			// match in findIn.
			Point regExPos = getNextMatchPosRegEx(text, findIn,
									forward, matchCase, wholeWord);
			findIn = null; // May help garbage collecting.
			if (regExPos!=null) {
				// Without this, if JTextArea isn't in focus, selection
				// won't appear selected.
				c.setSelectionVisible(true);
				if (forward) {
					regExPos.translate(start, start);
				}
				c.setDot(regExPos.x);
				c.moveDot(regExPos.y);
				return true;
			}
		}

		// No match.
		return false;

	}


	/**
	 * Returns the text in which to search, as a string.  This is used
	 * internally to grab the smallest buffer possible in which to search.
	 */
	protected static String getFindInText(JTextArea textArea, int start,
									boolean forward) {

		// Be smart about the text we grab to search in.  We grab more than
		// a single line because our searches can return multiline results.
		// We copy only the chars that will be searched through.
		String findIn = null;
		if (forward) {
			try {
				findIn = textArea.getText(start,
							textArea.getDocument().getLength()-start);
			} catch (BadLocationException ble) {
				// Never happens; findIn will be null anyway.
				ble.printStackTrace();
			}
		}
		else { // backward
			try {
				findIn = textArea.getText(0, start);
			} catch (BadLocationException ble) {
				// Never happens; findIn will be null anyway.
				ble.printStackTrace();
			}
		}

		return findIn;

	}


	/**
	 * This method is called internally by
	 * getNextMatchPosRegExImpl and is used to get the locations
	 * of all regular-expression matches, and possibly their replacement
	 * strings.

* * Returns either: *

    *
  • A list of points representing the starting and ending positions * of all matches returned by the specified matcher, or *
  • A list of RegExReplaceInfos describing the matches * found by the matcher and the replacement strings for each. *
* * If replacement is null, this method call is * assumed to be part of a "find" operation and points are returned. If * if is non-null, it is assumed to be part of a "replace" * operation and the RegExReplaceInfos are returned.

* * @param m The matcher. * @param replaceStr The string to replace matches with. This is a * "template" string and can contain captured group references in * the form "${digit}". * @return A list of result objects. * @throws IndexOutOfBoundsException If replaceStr references * an invalid group (less than zero or greater than the number of * groups matched). */ protected static List getMatches(Matcher m, String replaceStr) { ArrayList matches = new ArrayList(); while (m.find()) { Point loc = new Point(m.start(), m.end()); if (replaceStr==null) { // Find, not replace. matches.add(loc); } else { // Replace. matches.add(new RegExReplaceInfo(m.group(0), loc.x, loc.y, getReplacementText(m, replaceStr))); } } return matches; } /** * Searches searchIn for an occurrence of * searchFor either forwards or backwards, matching * case or not. * * @param searchFor The string to look for. * @param searchIn The string to search in. * @param forward Whether to search forward or backward in * searchIn. * @param matchCase If true, do a case-sensitive search for * searchFor. * @param wholeWord If true, searchFor * occurrences embedded in longer words in searchIn * don't count as matches. * @return The starting position of a match, or -1 if no * match was found. * @see #getNextMatchPosImpl * @see #getNextMatchPosRegEx */ public static final int getNextMatchPos(String searchFor, String searchIn, boolean forward, boolean matchCase, boolean wholeWord) { // Make our variables lower case if we're ignoring case. if (!matchCase) { return getNextMatchPosImpl(searchFor.toLowerCase(), searchIn.toLowerCase(), forward, matchCase, wholeWord); } return getNextMatchPosImpl(searchFor, searchIn, forward, matchCase, wholeWord); } /** * Actually does the work of matching; assumes searchFor and searchIn * are already upper/lower-cased appropriately.
* The reason this method is here is to attempt to speed up * FindInFilesDialog; since it repeatedly calls * this method instead of getNextMatchPos, it gets better * performance as it no longer has to allocate a lower-cased string for * every call. * * @param searchFor The string to search for. * @param searchIn The string to search in. * @param goForward Whether the search is forward or backward. * @param matchCase Whether the search is case-sensitive. * @param wholeWord Whether only whole words should be matched. * @return The location of the next match, or -1 if no * match was found. */ protected static final int getNextMatchPosImpl(String searchFor, String searchIn, boolean goForward, boolean matchCase, boolean wholeWord) { if (wholeWord) { int len = searchFor.length(); int temp = goForward ? 0 : searchIn.length(); int tempChange = goForward ? 1 : -1; while (true) { if (goForward) temp = searchIn.indexOf(searchFor, temp); else temp = searchIn.lastIndexOf(searchFor, temp); if (temp!=-1) { if (isWholeWord(searchIn, temp, len)) { return temp; } else { temp += tempChange; continue; } } return temp; // Always -1. } } else { return goForward ? searchIn.indexOf(searchFor) : searchIn.lastIndexOf(searchFor); } } /** * Searches searchIn for an occurrence of regEx * either forwards or backwards, matching case or not. * * @param regEx The regular expression to look for. * @param searchIn The string to search in. * @param goForward Whether to search forward. If false, * search backward. * @param matchCase Whether or not to do a case-sensitive search for * regEx. * @param wholeWord If true, regEx * occurrences embedded in longer words in searchIn * don't count as matches. * @return A Point representing the starting and ending * position of the match, or null if no match was * found. * @throws PatternSyntaxException If regEx is an invalid * regular expression. * @see #getNextMatchPos */ public static Point getNextMatchPosRegEx(String regEx, CharSequence searchIn, boolean goForward, boolean matchCase, boolean wholeWord) { return (Point)getNextMatchPosRegExImpl(regEx, searchIn, goForward, matchCase, wholeWord, null); } /** * Searches searchIn for an occurrence of regEx * either forwards or backwards, matching case or not. * * @param regEx The regular expression to look for. * @param searchIn The string to search in. * @param goForward Whether to search forward. If false, * search backward. * @param matchCase Whether or not to do a case-sensitive search for * regEx. * @param wholeWord If true, regEx * occurrences embedded in longer words in searchIn * don't count as matches. * @param replaceStr The string that will replace the match found (if * a match is found). The object returned will contain the * replacement string with matched groups substituted. If this * value is null, it is assumed this call is part of a * "find" instead of a "replace" operation. * @return If replaceStr is null, a * Point representing the starting and ending points * of the match. If it is non-null, an object with * information about the match and the morphed string to replace * it with. If no match is found, null is returned. * @throws PatternSyntaxException If regEx is an invalid * regular expression. * @throws IndexOutOfBoundsException If replaceStr references * an invalid group (less than zero or greater than the number of * groups matched). * @see #getNextMatchPos */ protected static Object getNextMatchPosRegExImpl(String regEx, CharSequence searchIn, boolean goForward, boolean matchCase, boolean wholeWord, String replaceStr) { // Make a pattern that takes into account whether or not to match case. int flags = Pattern.MULTILINE; // '^' and '$' are done per line. flags |= matchCase ? 0 : (Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE); Pattern pattern = Pattern.compile(regEx, flags); // Make a Matcher to find the regEx instances. Matcher m = pattern.matcher(searchIn); /* * Our algorithm is broken into four cases: * 1. Forward search, not whole-word: Just take first match found. * 2. Forward search, whole-word: Loop until the first whole-word * match is found. * 3. Backward search, not whole-word. Find all matches first * (must do this since we can't search for regexes backwards), * and return last match found. * 4. Backward search, whole-word. Find all matches first, then * loop through them backwards until the first (i.e., the last!) * whole-word match is found. */ // If this is a forward-direction search... if (goForward) { // 1. Forward search, not whole word => easy. Just return // the first match found. if (!wholeWord) { if (m.find()) { if (replaceStr==null) { // Find, not replace. return new Point(m.start(), m.end()); } else { // Replace. return new RegExReplaceInfo(m.group(0), m.start(), m.end(), getReplacementText(m, replaceStr)); } } } // 2. Forward search, whole word => just okay. Find and look at // matches one at a time until you find one that's "whole word." else { while (m.find()) { Point loc = new Point(m.start(), m.end()); if (isWholeWord(searchIn, loc.x,loc.y-loc.x)) { if (replaceStr==null) { // Find, not replace. return loc; } else { // Replace. return new RegExReplaceInfo(m.group(0), loc.x, loc.y, getReplacementText(m, replaceStr)); } } } } } // End of if (goForward). // If this is a backward-direction search... else { // Get some variables ready. List matches = getMatches(m, replaceStr); if (matches.isEmpty()) return null; int pos = matches.size() - 1; // 3. If they're not looking for a "whole word" just return // the first (i.e., last) match. if (wholeWord==false) { if (replaceStr==null) { // Find, not replace. return /*(Point)*/matches.get(pos); } else { // Replace. return /*(RegExReplaceInfo)*/matches.get(pos); } } // 4. Otherwise, go through the matches last-to-first. while (pos>=0) { Object matchObj = matches.get(pos); if (replaceStr==null) { // Find, not replace. Point loc = (Point)matchObj; if (isWholeWord(searchIn, loc.x,loc.y-loc.x)) { return matchObj; } } else { // Replace. RegExReplaceInfo info = (RegExReplaceInfo)matchObj; int x = info.getStartIndex(); int y = info.getEndIndex(); if (isWholeWord(searchIn, x,y-x)) { return matchObj; } } pos--; } } // If we didn't find a match after all that, return null. return null; } /** * Returns information on how to implement a regular expression "replace" * action in the specified text with the specified replacement string. * * @param regEx The regular expression to look for. * @param searchIn The string to search in. * @param goForward Whether to search forward. If false, * search backward. * @param matchCase Whether or not to do a case-sensitive search for * regEx. * @param wholeWord If true, regEx occurrences * embedded in longer words in searchIn don't count as * matches. * @param replacement A template for the replacement string (e.g., this * can contain \t and \n to mean tabs * and newlines, respectively, as well as group references * $n). * @return A RegExReplaceInfo object describing how to * implement the replace. * @throws PatternSyntaxException If regEx is an invalid * regular expression. * @throws IndexOutOfBoundsException If replacement references * an invalid group (less than zero or greater than the number of * groups matched). * @see #getNextMatchPos */ protected static RegExReplaceInfo getRegExReplaceInfo(String regEx, String searchIn, boolean goForward, boolean matchCase, boolean wholeWord, String replacement) { // Can't pass null to getNextMatchPosRegExImpl or it'll think // you're doing a "find" operation instead of "replace, and return a // Point. if (replacement==null) { replacement = ""; } return (RegExReplaceInfo)getNextMatchPosRegExImpl(regEx, searchIn, goForward, matchCase, wholeWord, replacement); } /** * Called internally by getMatches(). This method assumes * that the specified matcher has just found a match, and that you want * to get the string with which to replace that match. * * @param m The matcher. * @param template The template for the replacement string. For example, * "foo" would yield the replacement string * "foo", while "$1 is the greatest" * would yield different values depending on the value of the first * captured group in the match. * @return The string to replace the match with. * @throws IndexOutOfBoundsException If template references * an invalid group (less than zero or greater than the number of * groups matched). */ public static String getReplacementText(Matcher m, CharSequence template) { // NOTE: This code was mostly ripped off from J2SE's Matcher // class. // Process substitution string to replace group references with groups int cursor = 0; StringBuffer result = new StringBuffer(); while (cursor < template.length()) { char nextChar = template.charAt(cursor); if (nextChar == '\\') { // Escape character. nextChar = template.charAt(++cursor); switch (nextChar) { // Special cases. case 'n': nextChar = '\n'; break; case 't': nextChar = '\t'; break; } result.append(nextChar); cursor++; } else if (nextChar == '$') { // Group reference. cursor++; // Skip the '$'. // The first number is always a group int refNum = template.charAt(cursor) - '0'; if ((refNum < 0)||(refNum > 9)) { // This should really be an IllegalArgumentException, // but we cheat to keep all "group" errors throwing // the same exception type. throw new IndexOutOfBoundsException( "No group " + template.charAt(cursor)); } cursor++; // Capture the largest legal group string boolean done = false; while (!done) { if (cursor >= template.length()) { break; } int nextDigit = template.charAt(cursor) - '0'; if ((nextDigit < 0)||(nextDigit > 9)) { // not a number break; } int newRefNum = (refNum * 10) + nextDigit; if (m.groupCount() < newRefNum) { done = true; } else { refNum = newRefNum; cursor++; } } // Append group if (m.group(refNum) != null) result.append(m.group(refNum)); } else { result.append(nextChar); cursor++; } } return result.toString(); } /** * Returns whether the characters on either side of * substr(searchIn,startPos,startPos+searchStringLength) * are whitespace. While this isn't the best definition of "whole word", * it's the one we're going to use for now. */ private static final boolean isWholeWord(CharSequence searchIn, int offset, int len) { boolean wsBefore, wsAfter; try { wsBefore = Character.isWhitespace(searchIn.charAt(offset - 1)); } catch (IndexOutOfBoundsException e) { wsBefore = true; } try { wsAfter = Character.isWhitespace(searchIn.charAt(offset + len)); } catch (IndexOutOfBoundsException e) { wsAfter = true; } return wsBefore && wsAfter; } /** * Makes the caret's dot and mark the same location so that, for the * next search in the specified direction, a match will be found even * if it was within the original dot and mark's selection. * * @param textArea The text area. * @param forward Whether the search will be forward through the * document (false means backward). * @return The new dot and mark position. */ protected static int makeMarkAndDotEqual(JTextArea textArea, boolean forward) { Caret c = textArea.getCaret(); int val = forward ? Math.min(c.getDot(), c.getMark()) : Math.max(c.getDot(), c.getMark()); c.setDot(val); return val; } /** * Finds the next instance of the regular expression specified from * the caret position. If a match is found, it is replaced with * the specified replacement string. * * @param textArea The text area in which to search. * @param toFind The regular expression to search for. * @param replaceWith The string to replace the found regex with. * @param forward Whether to search forward from the caret position * or backward from it. * @param matchCase Whether the search should be case-sensitive. * @param wholeWord Whether there should be spaces or tabs on either * side of the match. * @return Whether a match was found (and thus replaced). * @throws PatternSyntaxException If toFind is not a * valid regular expression. * @throws IndexOutOfBoundsException If replaceWith references * an invalid group (less than zero or greater than the number of * groups matched). * @see #replace * @see #find */ protected static boolean regexReplace(JTextArea textArea, String toFind, String replaceWith, boolean forward, boolean matchCase, boolean wholeWord) throws PatternSyntaxException { // Be smart about what position we're "starting" at. For example, // if they are searching backwards and there is a selection such that // the dot is past the mark, and the selection is the text for which // you're searching, this search will find and return the current // selection. So, in that case we start at the beginning of the // selection. Caret c = textArea.getCaret(); int start = makeMarkAndDotEqual(textArea, forward); String findIn = getFindInText(textArea, start, forward); if (findIn==null) return false; // Find the next location of the text we're searching for. RegExReplaceInfo info = getRegExReplaceInfo(toFind, findIn, forward, matchCase, wholeWord, replaceWith); findIn = null; // May help garbage collecting. // If a match was found, do the replace and return! if (info!=null) { // Without this, if JTextArea isn't in focus, selection won't // appear selected. c.setSelectionVisible(true); int matchStart = info.getStartIndex(); int matchEnd = info.getEndIndex(); if (forward) { matchStart += start; matchEnd += start; } c.setDot(matchStart); c.moveDot(matchEnd); textArea.replaceSelection(info.getReplacement()); return true; } // No match. return false; } /** * Finds the next instance of the text/regular expression specified from * the caret position. If a match is found, it is replaced with the * specified replacement string. * * @param textArea The text area in which to search. * @param toFind The text/regular expression to search for. * @param replaceWith The string to replace the found text with. * @param forward Whether to search forward from the caret position or * backward from it. * @param matchCase Whether the search should be case-sensitive. * @param wholeWord Whether there should be spaces or tabs on either * side of the match. * @param regex Whether or not this is a regular expression search. * @return Whether a match was found (and thus replaced). * @throws PatternSyntaxException If regex is * true but toFind is not a valid * regular expression. * @throws IndexOutOfBoundsException If regex is * true and replaceWith references * an invalid group (less than zero or greater than the number * of groups matched). * @see #regexReplace * @see #find */ public static boolean replace(JTextArea textArea, String toFind, String replaceWith, boolean forward, boolean matchCase, boolean wholeWord, boolean regex) throws PatternSyntaxException { // Regular expression replacements have their own method. if (regex) { return regexReplace(textArea, toFind, replaceWith, forward, matchCase, wholeWord); } // Plain text search. If we find it, replace it! // First make the dot and mark equal (get rid of any selection), as // a common use-case is the user will use "Find" to select the text // to replace, then click "Replace" to replace the current selection. // Since our find() method searches from an endpoint of the selection, // we must remove the selection to work properly. makeMarkAndDotEqual(textArea, forward); if (find(textArea, toFind, forward, matchCase, wholeWord, false)) { textArea.replaceSelection(replaceWith); return true; } return false; } /** * Replaces all instances of the text/regular expression specified in * the specified document with the specified replacement. * * @param textArea The text area in which to search. * @param toFind The text/regular expression to search for. * @param replaceWith The string to replace the found text with. * @param matchCase Whether the search should be case-sensitive. * @param wholeWord Whether there should be spaces or tabs on either * side of the match. * @param regex Whether or not this is a regular expression search. * @return The number of replacements done. * @throws PatternSyntaxException If regex is * true and toFind is an invalid * regular expression. * @throws IndexOutOfBoundsException If replaceWith references * an invalid group (less than zero or greater than the number of * groups matched). * @see #replace * @see #regexReplace * @see #find */ public static int replaceAll(JTextArea textArea, String toFind, String replaceWith, boolean matchCase, boolean wholeWord, boolean regex) throws PatternSyntaxException { int count = 0; if (regex) { if (replaceWith==null) { replaceWith = ""; // Needed by getReplacementText() below. } // NOTE: This is a high-memory operation. First me make a copy // of the current document in a string, then we build yet a // third copy in a StringBuffer with replacements substituted. // Memory could be saved by replacing text into the document // directly as opposed to writing a StringBuffer, but this slows // down the operation considerably (after each doc.replace(), // all listeners are notified, etc.). StringBuffer sb = new StringBuffer(); String findIn = textArea.getText(); int lastEnd = 0; Pattern p = Pattern.compile(toFind); Matcher m = p.matcher(findIn); try { // NOTE: Instead of using m.replaceAll() (and thus // m.appendReplacement() and m.appendTail()), we // do this ourselves since we have our own method // of getting the "replacement text" which converts // "\n" to newlines and "\t" to tabs. while (m.find()) { //m.appendReplacement(sb, replaceWith); sb.append(findIn.substring(lastEnd, m.start())); sb.append(getReplacementText(m, replaceWith)); lastEnd = m.end(); count++; } //m.appendTail(sb); sb.append(findIn.substring(lastEnd)); textArea.setText(sb.toString()); } finally { findIn = null; // May help GC. } } else { // Non-regular expression search. textArea.setCaretPosition(0); while (SearchEngine.find(textArea, toFind, true, matchCase, wholeWord, false)) { textArea.replaceSelection(replaceWith); count++; } } return count; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy