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.

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.

/** * 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) {

/** * 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) {

/** * 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) {

/** * 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; }

/** * 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) {

/** * 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) {

/** * 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.

// 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++;

/** * 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) {

/** * 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);

/** * 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 {

/** * 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 {

/** * 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) {

// 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; }

/** * 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) {

/** * 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) {

/** * 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) {

/** * 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; }

/** * 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) {

/** * 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) {

/** * 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.

// 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++;

/** * 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) {

/** * 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);

/** * 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 {

/** * 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 {

/** * 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) {

// 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; }

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.

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 [url=https://github.com/bobbylight/RSyntaxTextArea]GitHub issue[/url] 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.

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.

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.

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]var sum = 0;var result = sum;log.info(sum);[/code]

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.