source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rtextarea/SearchEngine.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 26.5 KB
Line 
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 */
9package org.fife.ui.rtextarea;
10
11import java.awt.Insets;
12import java.awt.Point;
13import java.awt.Rectangle;
14import java.util.ArrayList;
15import java.util.List;
16import java.util.regex.Matcher;
17import java.util.regex.Pattern;
18import java.util.regex.PatternSyntaxException;
19import javax.swing.JTextArea;
20import javax.swing.text.BadLocationException;
21import javax.swing.text.Caret;
22
23import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
24import 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 */
42public 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}
Note: See TracBrowser for help on using the repository browser.