Search and replace behavior - SearchEngine

General Discussion on RSyntaxTextArea.

Moderator: robert

Search and replace behavior - SearchEngine

Postby lubomir.benes » Mon Nov 11, 2013 12:07 pm

Some remarks and proposals to search and replace functionality in RSyntaxTextArea:

1. The text to be replaced should be marked as selected before the replacement is performed. Currently the text what has been replaced remains selected.
2. Replace all should follow the direction. Now when we replace "Up" direction with "Replace" button and choose "Replace All" it goes backward.
3. There is a bug when "Replace All" is performed and the new string is an extension of string being replaced. It ends in endless loop, I guess.

Example:
I have a script like:
Code: Select all
var sum = 0;
var result = sum;
log.info(sum);


When I try to replace "sum" with eg. "sumx" and choose "Replace All" it crashes the application. It happens only when "Whole Words" is unchecked.

I have already made changes to SearchEngine.java, what fixes bugs and includes proposed features. If you are interested in let me know how to send it.
lubomir.benes
 
Posts: 6
Joined: Thu Jun 27, 2013 9:23 am

Re: Search and replace behavior - SearchEngine

Postby robert » Tue Nov 12, 2013 2:06 pm

Hi lubomir,

As of the 2.5.1 release, the bug with the application hanging should be fixed. I had to roll back the "select the replacement text" that was in Git as that was part of the problem.

If you want to send me a patch for #1 and #2 (preferably against the 2.5.1 source) you can either PM it to me in this forum, or either attach it to a new GitHub issue or create a pull request.

I'm a little confused about #2 though, since "replace all" will replace all occurrences in the document, whether before or after the caret.
User avatar
robert
 
Posts: 798
Joined: Sat May 10, 2008 5:16 pm

Re: Search and replace behavior - SearchEngine

Postby lubomir.benes » Tue Nov 12, 2013 2:54 pm

Regarding 2: I am using Far Manager, this is like its built in editor behaves. Just replaces all without confirmation.
lubomir.benes
 
Posts: 6
Joined: Thu Jun 27, 2013 9:23 am

Re: Search and replace behavior - SearchEngine

Postby lubomir.benes » Tue Nov 12, 2013 2:58 pm

Code: Select all
/*
 * 02/19/2006
 *
 * SearchEngine.java - Handles find/replace operations in an RTextArea.
 *
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rtextarea;

import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
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;

import org.fife.ui.rsyntaxtextarea.DocumentRange;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.folding.FoldManager;

/**
 * A singleton class that can perform advanced find/replace operations
 * in an {@link RTextArea}. Simply create a {@link SearchContext} and call
 * one of the following methods:
 *
 * <ul>
 * <li>{@link #find(JTextArea, SearchContext)}
 * <li>{@link #replace(RTextArea, SearchContext)}
 * <li>{@link #replaceAll(RTextArea, SearchContext)}
 * <li>{@link #markAll(RTextArea, SearchContext)}
 * </ul>
 *
 * @author Robert Futrell
 * @version 1.0
 * @see SearchContext
 */
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 context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     * but the search text is an invalid regular expression.
     * @see #replace(RTextArea, SearchContext)
     * @see #replaceAll(RTextArea, SearchContext)
     */
    public static SearchResult find(JTextArea textArea, SearchContext context) {

        // Always clear previous "mark all" highlights
        if (textArea instanceof RTextArea || context.getMarkAll()) {
            ((RTextArea) textArea).clearMarkAllHighlights();
        }
        boolean doMarkAll = textArea instanceof RTextArea && context.getMarkAll();

        String text = context.getSearchFor();
        if (text == null || text.length() == 0) {
            return new SearchResult();
        }

        // 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();
        boolean forward = context.getSearchForward();
        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 new SearchResult();
        }

        int markAllCount = 0;
        if (doMarkAll) {
            markAllCount = markAllImpl((RTextArea) textArea, context).
                    getMarkedCount();
        }

        SearchResult result = SearchEngine.findImpl(findIn, context);
        if (result.wasFound()) {
            // Without this, if JTextArea isn't in focus, selection
            // won't appear selected.
            textArea.getCaret().setSelectionVisible(true);
            if (forward && start > -1) {
                result.getMatchRange().translate(start);
            }
            selectAndPossiblyCenter(textArea, result.getMatchRange());
        }
        result.setMarkedCount(markAllCount);
        return result;

    }

    /**
     * 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 findIn The text to search in.
     * @param context The search context.
     * @return The result of the operation. "Mark all" will always be zero,
     * since this method does not perform that operation.
     * @throws PatternSyntaxException If this is a regular expression search
     * but the search text is an invalid regular expression.
     */
    private static SearchResult findImpl(String findIn, SearchContext context) {

        String text = context.getSearchFor();
        boolean forward = context.getSearchForward();

        // Find the next location of the text we're searching for.
        DocumentRange range = null;
        if (!context.isRegularExpression()) {
            int pos = getNextMatchPos(text, findIn, forward,
                    context.getMatchCase(), context.getWholeWord());
            findIn = null; // May help garbage collecting.
            if (pos != -1) {
                range = new DocumentRange(pos, pos + text.length());
            }
        } 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,
                    context.getMatchCase(), context.getWholeWord());
            findIn = null; // May help garbage collecting.
            if (regExPos != null) {
                range = new DocumentRange(regExPos.x, regExPos.y);
            }
        }

        int count = range != null ? 1 : 0;
        return new SearchResult(range, count, 0);

    }

    /**
     * Returns a
     * <code>CharSequence</code> for a text area that doesn't make a
     * copy of its contents for iteration. This conserves memory but is likely
     * just a tad slower.
     *
     * @param textArea The text area whose document is the basis for the
     * <code>CharSequence</code>.
     * @param start The starting offset of the sequence (or ending offset if
     * <code>forward</code> is <code>false</code>).
     * @param forward Whether we're searching forward or backward.
     * @return The character sequence.
     */
    private static CharSequence getFindInCharSequence(RTextArea textArea,
            int start, boolean forward) {
        RDocument doc = (RDocument) textArea.getDocument();
        int csStart = 0;
        int csEnd = 0;
        if (forward) {
            csStart = start;
            csEnd = doc.getLength();
        } else {
            csStart = 0;
            csEnd = start;
        }
        return new RDocumentCharSequence(doc, csStart, csEnd);
    }

    /**
     * Returns the text in which to search, as a string. This is used
     * internally to grab the smallest buffer possible in which to search.
     */
    private 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 multi-line results.
        // We copy only the chars that will be searched through.
        String findIn = null;
        try {
            if (forward) {
                findIn = textArea.getText(start,
                        textArea.getDocument().getLength() - start);
            } else { // backward
                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).
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    private 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.<p>
     *
     * Most clients will have no need to call this method directly.
     *
     * @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.
     */
    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.
     */
    private 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
     */
    private 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
     */
    private static Object getNextMatchPosRegExImpl(String regEx,
            CharSequence searchIn, boolean goForward,
            boolean matchCase, boolean wholeWord,
            String replaceStr) {

        if (wholeWord) {
            regEx = "\\b" + regEx + "\\b";
        }

        // 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 = null;
        try {
            pattern = Pattern.compile(regEx, flags);
        } catch (PatternSyntaxException pse) {
            return null; // e.g. a "mark all" request with incomplete regex
        }

        // Make a Matcher to find the regEx instances.
        Matcher m = pattern.matcher(searchIn);

        // Search forwards
        if (goForward) {
            if (m.find()) {
                if (replaceStr == null) { // Find, not replace.
                    return new Point(m.start(), m.end());
                }
                // Otherwise, replace
                return new RegExReplaceInfo(m.group(0),
                        m.start(), m.end(),
                        getReplacementText(m, replaceStr));
            }
        } // Search backwards
        else {
            List<?> matches = getMatches(m, replaceStr);
            if (!matches.isEmpty()) {
                return matches.get(matches.size() - 1);
            }
        }

        return null; // No match found

    }

    /**
     * Returns information on how to implement a regular expression "replace"
     * action in the specified text with the specified replacement string.
     *
     * @param searchIn The string to search in.
     * @param context The search options.
     * @return A <code>RegExReplaceInfo</code> object describing how to
     * implement the replace.
     * @throws PatternSyntaxException If the search text is an invalid regular
     * expression.
     * @throws IndexOutOfBoundsException If the replacement text references an
     * invalid group (less than zero or greater than the number of
     * groups matched).
     * @see #getNextMatchPos
     */
    private static RegExReplaceInfo getRegExReplaceInfo(CharSequence searchIn,
            SearchContext context) {
        // Can't pass null to getNextMatchPosRegExImpl or it'll think
        // you're doing a "find" operation instead of "replace, and return a
        // Point.
        String replacement = context.getReplaceWith();
        if (replacement == null) {
            replacement = "";
        }
        String regex = context.getSearchFor();
        boolean goForward = context.getSearchForward();
        boolean matchCase = context.getMatchCase();
        boolean wholeWord = context.getWholeWord();
        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.<p>
     *
     * Escapes simply insert the escaped character, except for
     * <code>\n</code>
     * and
     * <code>\t</code>, which insert a newline and tab respectively.
     * Substrings of the form
     * <code>$\d+</code> are considered to be matched
     * groups. To include a literal dollar sign in your template, escape it
     * (i.e.
     * <code>\$</code>).<p>
     *
     * Most clients will have no need to call this method directly.
     *
     * @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;
        StringBuilder result = new StringBuilder();

        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 <em>not</em> letters or digits.
     */
    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.
     */
    private static final 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;
    }

    /**
     * Marks all instances of the specified text in this text area. This
     * method is typically only called directly in response to search events
     * of type
     * <code>SearchEvent.Type.MARK_ALL</code>. "Mark all" behavior
     * is automatically performed when {@link #find(JTextArea, SearchContext)
     * or #replace(RTextArea, SearchContext) is called.
     *
     * @param textArea The text area in which to mark occurrences.
     * @param context The search context specifying the text to search for.
     * It is assumed that <code>context.getMarkAll()</code> has already
     * been checked and returns <code>true</code>.
     * @return The results of the operation.
     */
    public static final SearchResult markAll(RTextArea textArea,
            SearchContext context) {
        textArea.clearMarkAllHighlights();
        if (context.getMarkAll()) {
            return markAllImpl(textArea, context);
        }
        return new SearchResult();
    }

    /**
     * Marks all instances of the specified text in this text area. This
     * method is typically only called directly in response to search events
     * of type
     * <code>SearchEvent.Type.MARK_ALL</code>. "Mark all" behavior
     * is automatically performed when {@link #find(JTextArea, SearchContext)
     * or #replace(RTextArea, SearchContext) is called.
     *
     * @param textArea The text area in which to mark occurrences.
     * @param context The search context specifying the text to search for.
     * It is assumed that <code>context.getMarkAll()</code> has already
     * been checked and returns <code>true</code>.
     * @return The results of the operation.
     */
    private static final SearchResult markAllImpl(RTextArea textArea,
            SearchContext context) {

        String toMark = context.getSearchFor();
        int markAllCount = 0;

        if (toMark != null && toMark.length() > 0 /*&& !toMark.equals(markedWord)*/) {

            List<DocumentRange> highlights = new ArrayList<DocumentRange>();
            context = context.clone();
            context.setSearchForward(true);
            context.setMarkAll(false);

            String findIn = textArea.getText();
            int start = 0;

            // Optimization for pedantic worst-case of thousands of matches
            // while doing a case-insensitive search.  Go ahead and normalize
            // case just once, so that toLowerCase() isn't done for toMark and
            // findIn for each match found.
            if (!context.getMatchCase()) {
                context.setMatchCase(true);
                context.setSearchFor(toMark.toLowerCase());
                findIn = findIn.toLowerCase();
            }

            SearchResult res = SearchEngine.findImpl(findIn, context);
            while (res.wasFound()) {
                DocumentRange match = res.getMatchRange().translate(start);
                highlights.add(match);
                start = match.getEndOffset();
                res = SearchEngine.findImpl(findIn.substring(start), context);
            }
            textArea.markAll(highlights);
            textArea.repaint();
            markAllCount = highlights.size();
        } else {
            // Force a repaint of "mark all" highlights so ErrorStrips can
            // get updated
            List<DocumentRange> empty = Collections.emptyList();
            textArea.markAll(empty);
        }

        return new SearchResult(null, 0, markAllCount);

    }

    /**
     * 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 context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     * but the search text is an invalid regular expression.
     * @throws IndexOutOfBoundsException If this is a regular expression search
     * but the replacement text references an invalid group (less than
     * zero or greater than the number of groups matched).
     * @see #replace(RTextArea, SearchContext)
     * @see #find(JTextArea, SearchContext)
     */
    private static SearchResult regexReplace(RTextArea textArea,
            SearchContext context) 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();
        boolean forward = context.getSearchForward();
        int start = makeMarkAndDotEqual(textArea, forward);

        CharSequence findIn = getFindInCharSequence(textArea, start, forward);
        if (findIn == null) {
            return new SearchResult();
        }

        int markAllCount = 0;
        if (context.getMarkAll()) {
            markAllCount = markAllImpl(textArea, context).getMarkedCount();
        }

        // Find the next location of the text we're searching for.
        RegExReplaceInfo info = getRegExReplaceInfo(findIn, context);


        // If a match was found, do the replace and return!
        DocumentRange range = null;
        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;
            }
            range = new DocumentRange(matchStart, matchEnd);
            textArea.setSelectionStart(range.getStartOffset());
            textArea.setSelectionEnd(range.getEndOffset());
            String replacement = info.getReplacement();
            textArea.replaceSelection(replacement);
            info = getRegExReplaceInfo(findIn, context);
            int dot = textArea.getCaretPosition();
            if (info != null) {
                range.set(info.getStartIndex(), info.getEndIndex());
            } else {
                range.set(dot, dot);
            }
            selectAndPossiblyCenter(textArea, range);

        }
        findIn = null; // May help garbage collecting.

        int count = range != null ? 1 : 0;
        return new SearchResult(range, count, markAllCount);

    }

    /**
     * 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 context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     * but the search text is an invalid regular expression.
     * @throws IndexOutOfBoundsException If this is a regular expression search
     * but the replacement text references an invalid group (less than
     * zero or greater than the number of groups matched).
     * @see #replaceAll(RTextArea, SearchContext)
     * @see #find(JTextArea, SearchContext)
     */
    public static SearchResult replace(RTextArea textArea,
            SearchContext context) throws PatternSyntaxException {

        // Always clear previous "mark all" highlights
        if (context.getMarkAll()) {
            textArea.clearMarkAllHighlights();
        }

        String toFind = context.getSearchFor();
        if (toFind == null || toFind.length() == 0) {
            return new SearchResult();
        }

        textArea.beginAtomicEdit();
        try {

            // Regular expression replacements have their own method.
            /*
             if (context.isRegularExpression()) {
             return regexReplace(textArea, context);
             }
             * */
            // 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, context.getSearchForward());
            SearchResult res = find(textArea, context);
            if (res.wasFound()) {
                String replacement = context.getReplaceWith();
                if (replacement == null) {
                    replacement = "";
                }
                textArea.replaceSelection(replacement);
                if (context.getSearchForward()) {
                    textArea.setCaretPosition(res.getMatchRange().getStartOffset() + replacement.length());
                } else {
                    textArea.setCaretPosition(res.getMatchRange().getStartOffset());
                }
                int dot = textArea.getCaretPosition();
                SearchResult next = find(textArea, context);
                DocumentRange range;
                if (next.wasFound()) {
                    range = next.getMatchRange();
                } else {
                    range = new DocumentRange(dot, dot);
                }
                res.setMatchRange(range);
                selectAndPossiblyCenter(textArea, range);
            }
            return res;

        } finally {
            textArea.endAtomicEdit();
        }

    }

    /**
     * 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 context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     * but the replacement text is an invalid regular expression.
     * @throws IndexOutOfBoundsException If this is a regular expression search
     * but the replacement text references an invalid group (less than
     * zero or greater than the number of groups matched).
     * @see #replace(RTextArea, SearchContext)
     * @see #find(JTextArea, SearchContext)
     */
    public static SearchResult replaceAll(RTextArea textArea,
            SearchContext context) throws PatternSyntaxException {

        // Always clear previous "mark all" highlights
        if (context.getMarkAll()) {
            textArea.clearMarkAllHighlights();
        }

        //context.setSearchForward(true); // Replace all always searches forward
        String toFind = context.getSearchFor();
        if (toFind == null || toFind.length() == 0) {
            return new SearchResult();
        }

        // We call into SearchEngine.replace() multiple times, but we don't
        // want to perform a "mark all" ever.
        if (context.getMarkAll()) {
            context = context.clone();
            context.setMarkAll(false);
        }

        SearchResult lastFound = null;
        int count = 0;
        textArea.beginAtomicEdit();
        try {
            int oldOffs = textArea.getCaretPosition();
            //textArea.setCaretPosition(0);
            SearchResult res = SearchEngine.replace(textArea, context);
            while (res.wasFound()) {
                lastFound = res;
                count++;
                res = SearchEngine.replace(textArea, context);
            }
            if (lastFound == null) { // If nothing was found, don't move the caret
                textArea.setCaretPosition(oldOffs);
                lastFound = new SearchResult();
            }
        } finally {
            textArea.endAtomicEdit();
        }

        lastFound.setCount(count);
        return lastFound;

    }

    /**
     * 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 range The range to select.
     */
    private static void selectAndPossiblyCenter(JTextArea textArea,
            DocumentRange range) {

        int start = range.getStartOffset();
        int end = range.getEndOffset();

        boolean foldsExpanded = false;
        if (textArea instanceof RSyntaxTextArea) {
            RSyntaxTextArea rsta = (RSyntaxTextArea) textArea;
            FoldManager fm = rsta.getFoldManager();
            if (fm.isCodeFoldingSupportedAndEnabled()) {
                foldsExpanded = fm.ensureOffsetNotInClosedFold(start);
                foldsExpanded |= fm.ensureOffsetNotInClosedFold(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 (!foldsExpanded && 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);
    }

}
lubomir.benes
 
Posts: 6
Joined: Thu Jun 27, 2013 9:23 am

Re: Search and replace behavior - SearchEngine

Postby lubomir.benes » Tue Nov 12, 2013 3:05 pm

Regarding 2 again:-) The current behavior have an issue:
If you try to replace eg. "sum" with "sumx" and start with normal "Replace" the first is replaced second etc. then I decide to "Replace All" it replces even the already replaced text. Therefore I have for the first replaced "manually" "sumxx" because of two substitutions.
lubomir.benes
 
Posts: 6
Joined: Thu Jun 27, 2013 9:23 am


Return to Open Discussion

Who is online

Users browsing this forum: No registered users and 3 guests

cron