/*
* 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.Insets;
import java.awt.Point;
import java.awt.Rectangle;
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 {@link 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 <code>text</code> is a Java regular expression to search for.
* @return Whether a match was found (and thus selected).
* @throws PatternSyntaxException
* If <code>regex</code> is <code>true</code> but <code>text</code> 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;
selectAndPossiblyCenter(textArea, pos, 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);
}
selectAndPossiblyCenter(textArea, regExPos.x, 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 <code>getNextMatchPosRegExImpl</code> and is used to get the locations of all
* regular-expression matches, and possibly their replacement strings.
* <p>
*
* Returns either:
* <ul>
* <li>A list of points representing the starting and ending positions of all matches returned by the specified
* matcher, or
* <li>A list of <code>RegExReplaceInfo</code>s describing the matches found by the matcher and the replacement
* strings for each.
* </ul>
*
* If <code>replacement</code> is <code>null</code>, this method call is assumed to be part of a "find" operation
* and points are returned. If if is non-<code>null</code>, it is assumed to be part of a "replace" operation and
* the <code>RegExReplaceInfo</code>s are returned.
* <p>
*
* @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 "<code>${digit}</code>".
* @return A list of result objects.
* @throws IndexOutOfBoundsException
* If <code>replaceStr</code> 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 <code>searchIn</code> for an occurrence of <code>searchFor</code> 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 <code>searchIn</code>.
* @param matchCase
* If <code>true</code>, do a case-sensitive search for <code>searchFor</code>.
* @param wholeWord
* If <code>true</code>, <code>searchFor</code> occurrences embedded in longer words in
* <code>searchIn</code> don't count as matches.
* @return The starting position of a match, or <code>-1</code> 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.<br>
* The reason this method is here is to attempt to speed up <code>FindInFilesDialog</code>; since it repeatedly
* calls this method instead of <code>getNextMatchPos</code>, 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 <code>-1</code> 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 <code>searchIn</code> for an occurrence of <code>regEx</code> 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 <code>false</code>, search backward.
* @param matchCase
* Whether or not to do a case-sensitive search for <code>regEx</code>.
* @param wholeWord
* If <code>true</code>, <code>regEx</code> occurrences embedded in longer words in <code>searchIn</code>
* don't count as matches.
* @return A <code>Point</code> representing the starting and ending position of the match, or <code>null</code> if
* no match was found.
* @throws PatternSyntaxException
* If <code>regEx</code> 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 <code>searchIn</code> for an occurrence of <code>regEx</code> 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 <code>false</code>, search backward.
* @param matchCase
* Whether or not to do a case-sensitive search for <code>regEx</code>.
* @param wholeWord
* If <code>true</code>, <code>regEx</code> occurrences embedded in longer words in <code>searchIn</code>
* 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 <code>null</code>, it is
* assumed this call is part of a "find" instead of a "replace" operation.
* @return If <code>replaceStr</code> is <code>null</code>, a <code>Point</code> representing the starting and
* ending points of the match. If it is non-<code>null</code>, an object with information about the match
* and the morphed string to replace it with. If no match is found, <code>null</code> is returned.
* @throws PatternSyntaxException
* If <code>regEx</code> is an invalid regular expression.
* @throws IndexOutOfBoundsException
* If <code>replaceStr</code> 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 <code>false</code>, search backward.
* @param matchCase
* Whether or not to do a case-sensitive search for <code>regEx</code>.
* @param wholeWord
* If <code>true</code>, <code>regEx</code> occurrences embedded in longer words in <code>searchIn</code>
* don't count as matches.
* @param replacement
* A template for the replacement string (e.g., this can contain <code>\t</code> and <code>\n</code> to
* mean tabs and newlines, respectively, as well as group references <code>$n</code>).
* @return A <code>RegExReplaceInfo</code> object describing how to implement the replace.
* @throws PatternSyntaxException
* If <code>regEx</code> is an invalid regular expression.
* @throws IndexOutOfBoundsException
* If <code>replacement</code> 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 <code>getMatches()</code>. 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, "<code>foo</code>" would yield the replacement
* string "<code>foo</code>", while "<code>$1 is the greatest</code>" 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 <code>template</code> 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
* <code>substr(searchIn,startPos,startPos+searchStringLength)</code> 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.isLetterOrDigit(searchIn.charAt(offset - 1));
} catch (IndexOutOfBoundsException e) {
wsBefore = true;
}
try {
wsAfter = !Character.isLetterOrDigit(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 (<code>false</code> 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 <code>toFind</code> is not a valid regular expression.
* @throws IndexOutOfBoundsException
* If <code>replaceWith</code> 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;
}
selectAndPossiblyCenter(textArea, matchStart, 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 <code>regex</code> is <code>true</code> but <code>toFind</code> is not a valid regular expression.
* @throws IndexOutOfBoundsException
* If <code>regex</code> is <code>true</code> and <code>replaceWith</code> references an invalid group
* (less than zero or greater than the number of groups matched).
* @see #regexReplace
* @see #find
*/
public static boolean replace(RTextArea textArea, String toFind,
String replaceWith, boolean forward, boolean matchCase,
boolean wholeWord, boolean regex)
throws PatternSyntaxException {
textArea.beginAtomicEdit();
try {
// 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;
}
} finally {
textArea.endAtomicEdit();
}
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 <code>regex</code> is <code>true</code> and <code>toFind</code> is an invalid regular expression.
* @throws IndexOutOfBoundsException
* If <code>replaceWith</code> 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(RTextArea textArea, String toFind,
String replaceWith, boolean matchCase,
boolean wholeWord, boolean regex)
throws PatternSyntaxException {
int count = 0;
textArea.beginAtomicEdit();
try {
if (regex) {
if (replaceWith == null) {
replaceWith = ""; // Needed by getReplacementText() below.
}
int oldOffs = textArea.getCaretPosition();
textArea.setCaretPosition(0);
int flags = Pattern.MULTILINE; // '^' and '$' are done per line.
flags |= matchCase ? 0 :
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
Pattern p = Pattern.compile(toFind, flags);
while (SearchEngine.find(textArea, toFind, true, matchCase,
wholeWord, true)) {
Matcher m = p.matcher(textArea.getSelectedText());
String replacement = getReplacementText(m, replaceWith);
textArea.replaceSelection(replacement);
count++;
}
if (count == 0) { // If nothing was found, don't move the caret.
textArea.setCaretPosition(oldOffs);
}
}
else { // Non-regular expression search.
textArea.setCaretPosition(0);
while (SearchEngine.find(textArea, toFind, true, matchCase,
wholeWord, false)) {
textArea.replaceSelection(replaceWith);
count++;
}
}
} finally {
textArea.endAtomicEdit();
}
return count;
}
/**
* Selects a range of text in a text component. If the new selection is outside of the previous viewable rectangle,
* then the view is centered around the new selection.
*
* @param textArea
* The text component whose selection is to be centered.
* @param start
* The start of the range to select.
* @param end
* The end of the range to select.
*/
private static void selectAndPossiblyCenter(JTextArea textArea, int start,
int end) {
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
Rectangle r = null;
try {
r = textArea.modelToView(start);
if (r == null) { // Not yet visible; i.e. JUnit tests
return;
}
if (end != start) {
r = r.union(textArea.modelToView(end));
}
} catch (BadLocationException ble) { // Never happens
ble.printStackTrace();
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
return;
}
Rectangle visible = textArea.getVisibleRect();
// If the new selection is already in the view, don't scroll,
// as that is visually jarring.
if (visible.contains(r)) {
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
return;
}
visible.x = r.x - (visible.width - r.width) / 2;
visible.y = r.y - (visible.height - r.height) / 2;
Rectangle bounds = textArea.getBounds();
Insets i = textArea.getInsets();
bounds.x = i.left;
bounds.y = i.top;
bounds.width -= i.left + i.right;
bounds.height -= i.top + i.bottom;
if (visible.x < bounds.x) {
visible.x = bounds.x;
}
if (visible.x + visible.width > bounds.x + bounds.width) {
visible.x = bounds.x + bounds.width - visible.width;
}
if (visible.y < bounds.y) {
visible.y = bounds.y;
}
if (visible.y + visible.height > bounds.y + bounds.height) {
visible.y = bounds.y + bounds.height - visible.height;
}
textArea.scrollRectToVisible(visible);
}
}