diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/IFindReplaceTarget.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/IFindReplaceTarget.java index 00042b61a4c..b2804270a34 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/IFindReplaceTarget.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/IFindReplaceTarget.java @@ -100,4 +100,23 @@ public interface IFindReplaceTarget { * @param text the substitution text */ void replaceSelection(String text); + + /** + * @return true, if its able to handle batch replacements. + */ + default boolean canBatchReplace() { + return false; + } + + /** + * @param findString the string to find. + * @param replaceString the string to replace found occurrences. + * @param wholeWordSearch search for whole words. + * @param caseSensitiveSearch case sensitive search. + * @param regexSearch RegEex search. + * @param incrementalSearch search in selected lines. + */ + default int batchReplace(String findString, String replaceString, boolean wholeWordSearch, boolean caseSensitiveSearch, boolean regexSearch, boolean incrementalSearch) { + return -1; + } } diff --git a/bundles/org.eclipse.text/META-INF/MANIFEST.MF b/bundles/org.eclipse.text/META-INF/MANIFEST.MF index cd4309faac7..923f0a1dd86 100644 --- a/bundles/org.eclipse.text/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.text/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Export-Package: org.eclipse.jface.text; text="split"; mandatory:="text", org.eclipse.jface.text.link; text="split"; mandatory:="text", org.eclipse.jface.text.projection, + org.eclipse.jface.text.internal, org.eclipse.jface.text.rules; text="split"; mandatory:="text", org.eclipse.jface.text.source; text="split"; mandatory:="text", org.eclipse.jface.text.templates; text="split"; mandatory:="text", diff --git a/bundles/org.eclipse.text/src/org/eclipse/jface/text/FindReplaceDocumentAdapter.java b/bundles/org.eclipse.text/src/org/eclipse/jface/text/FindReplaceDocumentAdapter.java index 6cbe87da3dc..8c19df2ee23 100644 --- a/bundles/org.eclipse.text/src/org/eclipse/jface/text/FindReplaceDocumentAdapter.java +++ b/bundles/org.eclipse.text/src/org/eclipse/jface/text/FindReplaceDocumentAdapter.java @@ -21,6 +21,8 @@ import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.text.internal.RegExUtils; + /** * Provides search and replace operations on @@ -163,14 +165,14 @@ private IRegion findReplace(final FindReplaceOperationCode operationCode, int st if (regExSearch) { patternFlags |= Pattern.MULTILINE; - findString= substituteLinebreak(findString); + findString= RegExUtils.substituteLinebreak(findString); } if (!caseSensitive) patternFlags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; if (!regExSearch) - findString= asRegPattern(findString); + findString= RegExUtils.asRegPattern(findString); if (wholeWord) findString= "\\b" + findString + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$ @@ -262,85 +264,6 @@ private IRegion findReplace(final FindReplaceOperationCode operationCode, int st return null; } - /** - * Substitutes \R in a regex find pattern with {@code (?>\r\n?|\n)} - * - * @param findString the original find pattern - * @return the transformed find pattern - * @throws PatternSyntaxException if \R is added at an illegal position (e.g. in a character set) - * @since 3.4 - */ - private String substituteLinebreak(String findString) throws PatternSyntaxException { - int length= findString.length(); - StringBuilder buf= new StringBuilder(length); - - int inCharGroup= 0; - int inBraces= 0; - boolean inQuote= false; - for (int i= 0; i < length; i++) { - char ch= findString.charAt(i); - switch (ch) { - case '[': - buf.append(ch); - if (! inQuote) - inCharGroup++; - break; - - case ']': - buf.append(ch); - if (! inQuote) - inCharGroup--; - break; - - case '{': - buf.append(ch); - if (! inQuote && inCharGroup == 0) - inBraces++; - break; - - case '}': - buf.append(ch); - if (! inQuote && inCharGroup == 0) - inBraces--; - break; - - case '\\': - if (i + 1 < length) { - char ch1= findString.charAt(i + 1); - if (inQuote) { - if (ch1 == 'E') - inQuote= false; - buf.append(ch).append(ch1); - i++; - - } else if (ch1 == 'R') { - if (inCharGroup > 0 || inBraces > 0) { - String msg= TextMessages.getString("FindReplaceDocumentAdapter.illegalLinebreak"); //$NON-NLS-1$ - throw new PatternSyntaxException(msg, findString, i); - } - buf.append("(?>\\r\\n?|\\n)"); //$NON-NLS-1$ - i++; - - } else { - if (ch1 == 'Q') { - inQuote= true; - } - buf.append(ch).append(ch1); - i++; - } - } else { - buf.append(ch); - } - break; - - default: - buf.append(ch); - break; - } - - } - return buf.toString(); - } /** * Interprets current Retain Case mode (all upper-case,all lower-case,capitalized or mixed) @@ -556,39 +479,6 @@ else if(Character.isUpperCase(foundText.charAt(0))) // is first character upper- return i; } - /** - * Converts a non-regex string to a pattern - * that can be used with the regex search engine. - * - * @param string the non-regex pattern - * @return the string converted to a regex pattern - */ - private String asRegPattern(String string) { - StringBuilder out= new StringBuilder(string.length()); - boolean quoting= false; - - for (int i= 0, length= string.length(); i < length; i++) { - char ch= string.charAt(i); - if (ch == '\\') { - if (quoting) { - out.append("\\E"); //$NON-NLS-1$ - quoting= false; - } - out.append("\\\\"); //$NON-NLS-1$ - continue; - } - if (!quoting) { - out.append("\\Q"); //$NON-NLS-1$ - quoting= true; - } - out.append(ch); - } - if (quoting) - out.append("\\E"); //$NON-NLS-1$ - - return out.toString(); - } - /** * Substitutes the previous match with the given text. * Sends a DocumentEvent to all registered IDocumentListener. diff --git a/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/Messages.java b/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/Messages.java new file mode 100644 index 00000000000..a4462f7abd2 --- /dev/null +++ b/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/Messages.java @@ -0,0 +1,18 @@ +package org.eclipse.jface.text.internal; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME= Messages.class.getPackageName() + ".messages"; //$NON-NLS-1$ + + public static String RegExUtils_0; + + public static String RegExUtils_IllegalPositionForRegEx; + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/RegExUtils.java b/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/RegExUtils.java new file mode 100644 index 00000000000..1744262d521 --- /dev/null +++ b/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/RegExUtils.java @@ -0,0 +1,148 @@ +package org.eclipse.jface.text.internal; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class RegExUtils { + + /** + * Converts a non-regex string to a pattern + * that can be used with the regex search engine. + * + * @param string the non-regex pattern + * @return the string converted to a regex pattern + */ + public static String asRegPattern(String string) { + StringBuilder out= new StringBuilder(string.length()); + boolean quoting= false; + + for (int i= 0, length= string.length(); i < length; i++) { + char ch= string.charAt(i); + if (ch == '\\') { + if (quoting) { + out.append("\\E"); //$NON-NLS-1$ + quoting= false; + } + out.append("\\\\"); //$NON-NLS-1$ + continue; + } + if (!quoting) { + out.append("\\Q"); //$NON-NLS-1$ + quoting= true; + } + out.append(ch); + } + if (quoting) + out.append("\\E"); //$NON-NLS-1$ + + return out.toString(); + } + + /** + * Substitutes \R in a regex find pattern with {@code (?>\r\n?|\n)} + * + * @param findString the original find pattern + * @return the transformed find pattern + * @throws PatternSyntaxException if \R is added at an illegal position (e.g. in a character set) + * @since 3.4 + */ + public static String substituteLinebreak(String findString) throws PatternSyntaxException { + int length= findString.length(); + StringBuilder buf= new StringBuilder(length); + + int inCharGroup= 0; + int inBraces= 0; + boolean inQuote= false; + for (int i= 0; i < length; i++) { + char ch= findString.charAt(i); + switch (ch) { + case '[': + buf.append(ch); + if (! inQuote) + inCharGroup++; + break; + + case ']': + buf.append(ch); + if (! inQuote) + inCharGroup--; + break; + + case '{': + buf.append(ch); + if (! inQuote && inCharGroup == 0) + inBraces++; + break; + + case '}': + buf.append(ch); + if (! inQuote && inCharGroup == 0) + inBraces--; + break; + + case '\\': + if (i + 1 < length) { + char ch1= findString.charAt(i + 1); + if (inQuote) { + if (ch1 == 'E') + inQuote= false; + buf.append(ch).append(ch1); + i++; + + } else if (ch1 == 'R') { + if (inCharGroup > 0 || inBraces > 0) { + String msg= Messages.RegExUtils_IllegalPositionForRegEx; + throw new PatternSyntaxException(msg, findString, i); + } + buf.append("(?>\\r\\n?|\\n)"); //$NON-NLS-1$ + i++; + + } else { + if (ch1 == 'Q') { + inQuote= true; + } + buf.append(ch).append(ch1); + i++; + } + } else { + buf.append(ch); + } + break; + + default: + buf.append(ch); + break; + } + + } + return buf.toString(); + } + + /** + * Creates the Pattern according to the flags. + * @param findString the find string. + * @param wholeWord find whole words. + * @param caseSensitive search case sensitive. + * @param regExSearch is RegEx search + * @return a Pattern which can directly be used. + */ + public static Pattern createRegexSearchPattern(String findString, boolean wholeWord, boolean caseSensitive, boolean regExSearch) { + int patternFlags = 0; + if (regExSearch) { + patternFlags |= Pattern.MULTILINE; + findString = RegExUtils.substituteLinebreak(findString); + } + + if (!caseSensitive) + patternFlags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; + + if (!regExSearch) + findString = RegExUtils.asRegPattern(findString); + + if (wholeWord) + findString = "\\b" + findString + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$ + + return Pattern.compile(findString, patternFlags); + } + +} diff --git a/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/messages.properties b/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/messages.properties new file mode 100644 index 00000000000..7dd20a1aa79 --- /dev/null +++ b/bundles/org.eclipse.text/src/org/eclipse/jface/text/internal/messages.properties @@ -0,0 +1 @@ +RegExUtils_IllegalPositionForRegEx=Illegal position for \\\\R \ No newline at end of file diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java index 1e2ea237271..4eb738b1912 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java @@ -731,6 +731,7 @@ private OverlayPreferenceStore createOverlayStore() { overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, AbstractTextEditor.PREFERENCE_COLOR_FIND_SCOPE)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_CURRENT_LINE_COLOR)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_BATCH_REPLACE)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_USE_FIND_REPLACE_OVERLAY)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_FIND_REPLACE_OVERLAY_AT_BOTTOM)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_CURRENT_LINE)); @@ -866,6 +867,10 @@ public void widgetSelected(SelectionEvent e) { IntegerDomain lineSpaceDomain= new IntegerDomain(0, 1000); addTextField(appearanceComposite, lineSpacing, lineSpaceDomain, 15, 0); + label= TextEditorMessages.TextEditorDefaultsPreferencePage_batchReplace; + Preference batchReplace= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_BATCH_REPLACE, label, null); + addCheckBox(appearanceComposite, batchReplace, new BooleanDomain(), 0); + label= TextEditorMessages.TextEditorPreferencePage_useFindReplaceOverlay; Preference useFindReplaceOverlay= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_USE_FIND_REPLACE_OVERLAY, label, null); final Button useOverlay= addCheckBox(appearanceComposite, useFindReplaceOverlay, new BooleanDomain(), 0); diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java index 92a3d48a385..89db0066895 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java @@ -138,6 +138,7 @@ private TextEditorMessages() { NLS.initializeMessages(BUNDLE_NAME, TextEditorMessages.class); } + public static String TextEditorDefaultsPreferencePage_batchReplace; public static String TextEditorDefaultsPreferencePage_carriageReturn; public static String TextEditorDefaultsPreferencePage_transparencyLevel; public static String TextEditorDefaultsPreferencePage_codeMinings_description; diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties index c0141258985..cdfbe8f9c9c 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties @@ -40,6 +40,7 @@ TextEditorPreferencePage_findScopeColor=Find scope TextEditorPreferencePage_accessibility_disableCustomCarets= Use &custom caret TextEditorPreferencePage_accessibility_wideCaret= &Enable thick caret TextEditorPreferencePage_accessibility_useSaturatedColorsInOverviewRuler=U&se saturated colors in overview ruler +TextEditorDefaultsPreferencePage_batchReplace=Enable batch replace for search and replace all. TextEditorDefaultsPreferencePage_carriageReturn=Carriage Return ( \u00a4 ) TextEditorDefaultsPreferencePage_transparencyLevel=&Transparency level (0 is transparent and 255 is opaque): TextEditorDefaultsPreferencePage_codeMinings_description=How annotations should be shown in-line in text editors which support code minings diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java index ebae43ed79c..2f1e224be93 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java @@ -221,6 +221,19 @@ private AbstractDecoratedTextEditorPreferenceConstants() { */ public final static String EDITOR_LINE_NUMBER_RULER= "lineNumberRuler"; //$NON-NLS-1$ + + /** + * A named preference that controls the enablement of batch replace. + * + *

+ * The preference value is of type Boolean + *

+ * + * @since 3.18 + */ + public static final String EDITOR_BATCH_REPLACE = "batchReplaceEnabled"; //$NON-NLS-1$ + + /** * A named preference that controls whether the find/replace overlay is used in place of the * dialog. @@ -761,6 +774,7 @@ private AbstractDecoratedTextEditorPreferenceConstants() { * @param store the preference store to be initialized */ public static void initializeDefaultValues(IPreferenceStore store) { + store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_BATCH_REPLACE, false); store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_USE_FIND_REPLACE_OVERLAY, true); store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_FIND_REPLACE_OVERLAY_AT_BOTTOM, false); store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.USE_ANNOTATIONS_PREFERENCE_PAGE, false); diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceLogic.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceLogic.java index 812787f7a86..8041e569820 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceLogic.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceLogic.java @@ -356,6 +356,20 @@ private int replaceAll() { return 0; } + if (target.canBatchReplace() && target instanceof IFindReplaceTargetExtension selectableTarget) { + selectableTarget.setReplaceAllMode(true); + try { + boolean wholeWordSearch = isAvailableAndActive(SearchOptions.WHOLE_WORD); + boolean caseSensitiveSearch = isAvailableAndActive(SearchOptions.CASE_SENSITIVE); + boolean regexSearch = isAvailableAndActive(SearchOptions.REGEX); + boolean incrementalSearch = !isActive(SearchOptions.GLOBAL); + + return target.batchReplace(findString, replaceString, wholeWordSearch, caseSensitiveSearch, + regexSearch, incrementalSearch); + } finally { + selectableTarget.setReplaceAllMode(false); + } + } List replacements = new ArrayList<>(); executeInForwardMode(() -> { executeWithReplaceAllEnabled(() -> { diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceTarget.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceTarget.java index 54d0b49492e..d7c68ce9c87 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceTarget.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceTarget.java @@ -13,14 +13,22 @@ *******************************************************************************/ package org.eclipse.ui.texteditor; +import java.util.regex.Pattern; + import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IFindReplaceTarget; import org.eclipse.jface.text.IFindReplaceTargetExtension; import org.eclipse.jface.text.IFindReplaceTargetExtension3; import org.eclipse.jface.text.IFindReplaceTargetExtension4; import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.internal.RegExUtils; /** @@ -34,7 +42,11 @@ class FindReplaceTarget implements IFindReplaceTarget, IFindReplaceTargetExtensi private AbstractTextEditor fEditor; /** The find/replace target */ private IFindReplaceTarget fTarget; - + /** The preference instance scope of editors to grab preferences */ + private static final String UI_EDITORS_INSTANCE_SCOPE_NODE_NAME = "org.eclipse.ui.editors"; //$NON-NLS-1$ + /** The preference key for batch search and replace */ + private static final String EDITOR_BATCH_REPLACE = "batchReplaceEnabled"; //$NON-NLS-1$ + /** * Creates a new find/replace target. * @@ -195,4 +207,53 @@ public void setReplaceAllMode(boolean replaceAll) { public boolean validateTargetState() { return fEditor.validateEditorInputState(); } + + @Override + public boolean canBatchReplace() { + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(UI_EDITORS_INSTANCE_SCOPE_NODE_NAME); + if (preferences == null) { + return false; + } + return preferences + .getBoolean(EDITOR_BATCH_REPLACE, false); + } + + @Override + public int batchReplace(String findString, String replaceString, boolean wholeWord, boolean caseSensitive, + boolean regExSearch, boolean incrementalSearch) { + // Compile the raw pattern early so it can throw an exception if it's not well + // formed. + // The information in that exception is displayed to the user. + if (regExSearch) { + Pattern.compile(findString); + } + + IDocument document = fEditor.getDocumentProvider().getDocument(fEditor.getEditorInput()); + + Pattern pattern = RegExUtils.createRegexSearchPattern(findString, wholeWord, caseSensitive, regExSearch); + if (incrementalSearch) { + IRegion region = getScope(); + try { + String selectedLines = document.get(region.getOffset(), region.getLength()); + var count = pattern.split(selectedLines, -1).length - 1; + if (count == 0) { + return count; + } + String replacedLines = pattern.matcher(selectedLines).replaceAll(replaceString); + + document.replace(region.getOffset(), region.getLength(), replacedLines); + + return count; + } catch (BadLocationException e) { + return 0; + } + } + + String documentContent = document.get(); + var count = pattern.split(documentContent, -1).length - 1; + document.set(pattern.matcher(documentContent).replaceAll(replaceString)); + + return count; + } + }