diff --git a/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF index 7a34055bd76..a3e278d607e 100644 --- a/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.ui.editors; singleton:=true -Bundle-Version: 3.17.300.qualifier +Bundle-Version: 3.18.0.qualifier Bundle-Activator: org.eclipse.ui.internal.editors.text.EditorsPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %providerName 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 95009b7b315..5fea8e14203 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 @@ -428,6 +428,8 @@ private OverlayPreferenceStore createDialogOverlayStore() { ArrayList overlayKeys= new ArrayList<>(); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_LEADING_SPACES)); + 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_SHOW_ENCLOSED_SPACES)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_TRAILING_SPACES)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_LEADING_IDEOGRAPHIC_SPACES)); @@ -729,6 +731,8 @@ 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_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)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH)); @@ -859,6 +863,15 @@ public void widgetSelected(SelectionEvent e) { IntegerDomain lineSpaceDomain= new IntegerDomain(0, 1000); addTextField(appearanceComposite, lineSpacing, lineSpaceDomain, 15, 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); + + label= TextEditorMessages.TextEditorPreferencePage_showFindReplaceOverlayAtBottom; + Preference findReplaceOverlayAtBottom= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_FIND_REPLACE_OVERLAY_AT_BOTTOM, label, null); + final Button overlayAtBottom= addCheckBox(appearanceComposite, findReplaceOverlayAtBottom, new BooleanDomain(), 0); + createDependency(useOverlay, useFindReplaceOverlay, new Control[] { overlayAtBottom }); + label= TextEditorMessages.TextEditorPreferencePage_enableWordWrap; Preference enableWordWrap= new Preference(AbstractTextEditor.PREFERENCE_WORD_WRAP_ENABLED, label, null); addCheckBox(appearanceComposite, enableWordWrap, 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 5858610848b..2872aea091f 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 @@ -35,6 +35,8 @@ private TextEditorMessages() { public static String LinkedModeConfigurationBlock_DASHED_BOX; public static String TextEditorPreferencePage_displayedTabWidth; public static String TextEditorPreferencePage_lineSpacing; + public static String TextEditorPreferencePage_useFindReplaceOverlay; + public static String TextEditorPreferencePage_showFindReplaceOverlayAtBottom; public static String TextEditorPreferencePage_enableWordWrap; public static String TextEditorPreferencePage_convertTabsToSpaces; public static String TextEditorPreferencePage_undoHistorySize; 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 c2d724c3f8c..cf6d2aac96a 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 @@ -17,6 +17,8 @@ EditorsPlugin_internal_error=Internal Error TextEditorPreferencePage_displayedTabWidth=Displayed &tab width: TextEditorPreferencePage_lineSpacing=Line &spacing (extra % of font height): +TextEditorPreferencePage_useFindReplaceOverlay=Use find/replace overla&y +TextEditorPreferencePage_showFindReplaceOverlayAtBottom=&Display find/replace overlay at bottom of editor TextEditorPreferencePage_enableWordWrap=&Enable word wrap when opening an editor TextEditorPreferencePage_convertTabsToSpaces=&Insert spaces for tabs TextEditorPreferencePage_undoHistorySize=&Undo history size: 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 0bb4b3347c8..20cb709f516 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 @@ -220,6 +220,30 @@ private AbstractDecoratedTextEditorPreferenceConstants() { *

*/ public final static String EDITOR_LINE_NUMBER_RULER= "lineNumberRuler"; //$NON-NLS-1$ + + /** + * A named preference that controls whether the find/replace overlay is used in place of the + * dialog. + * + *

+ * The preference value is of type Boolean + *

+ * + * @since 3.18 + */ + public final static String EDITOR_USE_FIND_REPLACE_OVERLAY= "useFindReplaceOverlay"; //$NON-NLS-1$ + + /** + * A named preference that controls whether the editor overlay to access find and replace + * functionality should be aligned to the bottom of the editor page instead of to the top. + * + *

+ * The preference value is of type Boolean + *

+ * + * @since 3.18 + */ + public final static String EDITOR_FIND_REPLACE_OVERLAY_AT_BOTTOM= "findReplaceOverlayAtBottom"; //$NON-NLS-1$ /** * A named preference that controls if the caret offset is shown in the status line. @@ -723,6 +747,8 @@ private AbstractDecoratedTextEditorPreferenceConstants() { * @param store the preference store to be initialized */ public static void initializeDefaultValues(IPreferenceStore store) { + 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); store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.USE_QUICK_DIFF_PREFERENCE_PAGE, false); diff --git a/bundles/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF index b60f67a1353..557cfe4b1a4 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF @@ -31,6 +31,7 @@ Require-Bundle: org.eclipse.core.expressions;bundle-version="[3.4.100,4.0.0)", org.eclipse.jface.text;bundle-version="[3.19.0,4.0.0)", org.eclipse.swt;bundle-version="[3.107.0,4.0.0)", - org.eclipse.ui;bundle-version="[3.5.0,4.0.0)" + org.eclipse.ui;bundle-version="[3.5.0,4.0.0)", + org.eclipse.jface.notifications Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: org.eclipse.ui.workbench.texteditor diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive.png new file mode 100644 index 00000000000..c2ac8a79a85 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive@2x.png new file mode 100644 index 00000000000..272bc485ada Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex.png new file mode 100644 index 00000000000..43b37ca9b54 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex@2x.png new file mode 100644 index 00000000000..53a95b12a98 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace-all@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace-all@2x.png new file mode 100644 index 00000000000..0d6a96fcd8a Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace-all@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace.png new file mode 100644 index 00000000000..59d140ab7d0 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace@2x.png new file mode 100644 index 00000000000..76652066a0f Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace_all.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace_all.png new file mode 100644 index 00000000000..2511faa1f87 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace_all.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all.png new file mode 100644 index 00000000000..141fdcf3cc4 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all@2x.png new file mode 100644 index 00000000000..fd59741c3df Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area.png new file mode 100644 index 00000000000..e820c06d5cb Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area@2x.png new file mode 100644 index 00000000000..c8c207eeff4 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_next@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_next@2x.png new file mode 100644 index 00000000000..d46b3a06265 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_next@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_prev@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_prev@2x.png new file mode 100644 index 00000000000..8e26ea4b4d6 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_prev@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_next.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_next.png new file mode 100644 index 00000000000..9fcc646d92b Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_next.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_prev.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_prev.png new file mode 100644 index 00000000000..f2e6a039c5d Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_prev.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word.png new file mode 100644 index 00000000000..54afac2abbe Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word@2x.png new file mode 100644 index 00000000000..7b66f352083 Binary files /dev/null and b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word@2x.png differ diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.java index 72f1fe46c4f..1e30529ea75 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.java @@ -60,5 +60,18 @@ private FindReplaceMessages() { public static String FindReplace_SelectAllButton_label; public static String FindReplace_CloseButton_label; - + public static String FindReplaceOverlay_downSearchButton_toolTip; + public static String FindReplaceOverlay_upSearchButton_toolTip; + public static String FindReplaceOverlay_searchAllButton_toolTip; + public static String FindReplaceOverlay_searchInSelectionButton_toolTip; + public static String FindReplaceOverlay_regexSearchButton_toolTip; + public static String FindReplaceOverlay_caseSensitiveButton_toolTip; + public static String FindReplaceOverlay_wholeWordsButton_toolTip; + public static String FindReplaceOverlay_replaceButton_toolTip; + public static String FindReplaceOverlay_replaceAllButton_toolTip; + public static String FindReplaceOverlay_searchBar_message; + public static String FindReplaceOverlay_replaceBar_message; + public static String FindReplaceOverlay_replaceToggle_toolTip; + public static String FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_message; + public static String FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_title; } diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.properties b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.properties index efd2971b780..da945bbceb3 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.properties +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/FindReplaceMessages.properties @@ -43,4 +43,20 @@ FindReplace_ReplaceFindButton_label=Replace/Fin&d FindReplace_ReplaceSelectionButton_label=&Replace FindReplace_ReplaceAllButton_label=Replace &All FindReplace_SelectAllButton_label=&Select All -FindReplace_CloseButton_label=Close \ No newline at end of file +FindReplace_CloseButton_label=Close + +# Messages for the "new" Find-Replace-Overlay +FindReplaceOverlay_upSearchButton_toolTip=Search backward (Shift + Enter) +FindReplaceOverlay_downSearchButton_toolTip=Search forward (Enter) +FindReplaceOverlay_searchAllButton_toolTip=Search all (Ctrl + Enter) +FindReplaceOverlay_searchInSelectionButton_toolTip=Only search in selected area (Ctrl + Shift + A) +FindReplaceOverlay_regexSearchButton_toolTip=Match regular expression pattern (Ctrl + Shift + P) +FindReplaceOverlay_caseSensitiveButton_toolTip=Match case (Ctrl + Shift + C) +FindReplaceOverlay_wholeWordsButton_toolTip=Match whole word (Ctrl + Shift + W) +FindReplaceOverlay_replaceButton_toolTip=Replace (Enter) +FindReplaceOverlay_replaceAllButton_toolTip=Replace all (Ctrl + Enter) +FindReplaceOverlay_searchBar_message=Find +FindReplaceOverlay_replaceBar_message=Replace +FindReplaceOverlay_replaceToggle_toolTip=Toggle input for replace (Ctrl + R) +FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_message=Find and replace can now be done using an overlay embedded inside the editor. If you prefer the dialog, you can disable the overlay in the preferences or disable it now. +FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_title=New Find/Replace Overlay diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceAction.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceAction.java index 5839928d3c8..e253c0902b4 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceAction.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceAction.java @@ -21,6 +21,10 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jface.dialogs.IPageChangedListener; import org.eclipse.jface.dialogs.PageChangedEvent; @@ -54,6 +58,39 @@ */ public class FindReplaceAction extends ResourceAction implements IUpdate { + private static final String INSTANCE_SCOPE_NODE_NAME = "org.eclipse.ui.editors"; //$NON-NLS-1$ + + private static final String USE_FIND_REPLACE_OVERLAY = "useFindReplaceOverlay"; //$NON-NLS-1$ + + private static final String FIND_REPLACE_OVERLAY_AT_BOTTOM = "findReplaceOverlayAtBottom"; //$NON-NLS-1$ + + private boolean shouldUseOverlay() { + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(INSTANCE_SCOPE_NODE_NAME); + boolean overlayPreference = preferences.getBoolean(USE_FIND_REPLACE_OVERLAY, true); + return overlayPreference && fWorkbenchPart instanceof StatusTextEditor; + } + + private static boolean shouldPositionOverlayOnTop() { + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(INSTANCE_SCOPE_NODE_NAME); + return !preferences.getBoolean(FIND_REPLACE_OVERLAY_AT_BOTTOM, false); + } + + private IPreferenceChangeListener overlayDialogPreferenceListener = new IPreferenceChangeListener() { + + @Override + public void preferenceChange(PreferenceChangeEvent event) { + if (overlay == null) { + return; + } + if (event.getKey().equals(USE_FIND_REPLACE_OVERLAY)) { + overlay.close(); + } else if (event.getKey().equals(FIND_REPLACE_OVERLAY_AT_BOTTOM)) { + overlay.setPositionToTop(shouldPositionOverlayOnTop()); + } + } + + }; + /** * Represents the "global" find/replace dialog. It tracks the active * part and retargets the find/replace dialog accordingly. The find/replace @@ -219,7 +256,6 @@ public void checkShell(Shell shell) { } - /** * Listener for disabling the dialog on shell close. *

@@ -246,6 +282,8 @@ public void checkShell(Shell shell) { */ private Shell fShell; + private FindReplaceOverlay overlay; + /** * Creates a new find/replace action for the given workbench part. *

@@ -264,6 +302,8 @@ public FindReplaceAction(ResourceBundle bundle, String prefix, IWorkbenchPart wo Assert.isLegal(workbenchPart != null); fWorkbenchPart= workbenchPart; update(); + + hookDialogPreferenceListener(); } /** @@ -291,6 +331,8 @@ public FindReplaceAction(ResourceBundle bundle, String prefix, Shell shell, IFin fTarget= target; fShell= shell; update(); + + hookDialogPreferenceListener(); } /** @@ -312,13 +354,29 @@ public FindReplaceAction(ResourceBundle bundle, String prefix, IWorkbenchWindow super(bundle, prefix); fWorkbenchWindow= workbenchWindow; update(); + + hookDialogPreferenceListener(); + } + + private void hookDialogPreferenceListener() { + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(INSTANCE_SCOPE_NODE_NAME); + preferences.addPreferenceChangeListener(overlayDialogPreferenceListener); } @Override public void run() { - if (fTarget == null) + if (fTarget == null) { return; + } + + if (shouldUseOverlay()) { + showOverlayInEditor(); + } else { + showDialog(); + } + } + private void showDialog() { final FindReplaceDialog dialog; final boolean isEditable; @@ -352,6 +410,24 @@ public void run() { dialog.open(); } + private void showOverlayInEditor() { + if (overlay == null) { + Shell shellToUse = null; + + if (fShell == null) { + shellToUse = fWorkbenchPart.getSite().getShell(); + } else { + shellToUse = fShell; + } + overlay = new FindReplaceOverlay(shellToUse, fWorkbenchPart, fTarget); + + FindReplaceOverlayFirstTimePopup.displayPopupIfNotAlreadyShown(shellToUse); + } + + overlay.setPositionToTop(shouldPositionOverlayOnTop()); + overlay.open(); + } + @Override public void update() { diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlay.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlay.java new file mode 100644 index 00000000000..5f465d90b56 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlay.java @@ -0,0 +1,866 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.texteditor; + +import org.osgi.framework.FrameworkUtil; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGBA; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Scrollable; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Widget; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.JFaceColors; +import org.eclipse.jface.window.Window; + +import org.eclipse.jface.text.IFindReplaceTarget; +import org.eclipse.jface.text.IFindReplaceTargetExtension; + +import org.eclipse.ui.IPartListener; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.findandreplace.FindReplaceLogic; +import org.eclipse.ui.internal.findandreplace.FindReplaceMessages; +import org.eclipse.ui.internal.findandreplace.SearchOptions; +import org.eclipse.ui.internal.findandreplace.status.IFindReplaceStatus; + +/** + * @since 3.17 + */ +class FindReplaceOverlay extends Dialog { + + private static final String REPLACE_BAR_OPEN_DIALOG_SETTING = "replaceBarOpen"; //$NON-NLS-1$ + private static final double WORST_CASE_RATIO_EDITOR_TO_OVERLAY = 0.95; + private static final double BIG_WIDTH_RATIO_EDITOR_TO_OVERLAY = 0.7; + private static final String MINIMAL_WIDTH_TEXT = "THIS TEXT "; //$NON-NLS-1$ + private static final String COMPROMISE_WIDTH_TEXT = "THIS TEXT HAS A REASONABLE"; //$NON-NLS-1$ + private static final String IDEAL_WIDTH_TEXT = "THIS TEXT HAS A REASONABLE LENGTH FOR SEARCHING"; //$NON-NLS-1$ + private FindReplaceLogic findReplaceLogic; + private IWorkbenchPart targetPart; + private boolean overlayOpen; + private boolean replaceBarOpen; + + private Composite container; + private Button replaceToggle; + + private Composite contentGroup; + + private Composite searchContainer; + private Composite searchBarContainer; + private Text searchBar; + private ToolBar searchTools; + + private ToolItem searchInSelectionButton; + private ToolItem wholeWordSearchButton; + private ToolItem caseSensitiveSearchButton; + private ToolItem regexSearchButton; + private ToolItem searchUpButton; + private ToolItem searchDownButton; + private ToolItem searchAllButton; + + private Composite replaceContainer; + private Composite replaceBarContainer; + private Text replaceBar; + private ToolBar replaceTools; + private ToolItem replaceButton; + private ToolItem replaceAllButton; + + private Color backgroundToUse; + private Color normalTextForegroundColor; + private boolean positionAtTop = true; + + public FindReplaceOverlay(Shell parent, IWorkbenchPart part, IFindReplaceTarget target) { + super(parent); + createFindReplaceLogic(target); + + setShellStyle(SWT.MODELESS); + setBlockOnOpen(false); + targetPart = part; + + } + + @Override + protected boolean isResizable() { + return false; + } + + private void createFindReplaceLogic(IFindReplaceTarget target) { + findReplaceLogic = new FindReplaceLogic(); + boolean isTargetEditable = false; + if (target != null) { + isTargetEditable = target.isEditable(); + } + findReplaceLogic.updateTarget(target, isTargetEditable); + findReplaceLogic.activate(SearchOptions.INCREMENTAL); + findReplaceLogic.activate(SearchOptions.GLOBAL); + findReplaceLogic.activate(SearchOptions.WRAP); + findReplaceLogic.activate(SearchOptions.FORWARD); + } + + private void performReplaceAll() { + BusyIndicator.showWhile(getShell() != null ? getShell().getDisplay() : Display.getCurrent(), + () -> findReplaceLogic.performReplaceAll(getFindString(), getReplaceString())); + } + + private void performSelectAll() { + BusyIndicator.showWhile(getShell() != null ? getShell().getDisplay() : Display.getCurrent(), + () -> findReplaceLogic.performSelectAll(getFindString())); + } + + private KeyListener shortcuts = KeyListener.keyPressedAdapter(e -> { + e.doit = false; + if ((e.stateMask & SWT.CTRL) != 0 && (e.keyCode == 'F' || e.keyCode == 'f')) { + close(); + } else if ((e.stateMask & SWT.CTRL) != 0 && (e.keyCode == 'R' || e.keyCode == 'r')) { + if (findReplaceLogic.getTarget().isEditable()) { + toggleReplace(); + } + } else if ((e.stateMask & SWT.CTRL) != 0 && (e.keyCode == 'W' || e.keyCode == 'w')) { + toggleToolItem(wholeWordSearchButton); + } else if ((e.stateMask & SWT.CTRL) != 0 && (e.keyCode == 'P' || e.keyCode == 'p')) { + toggleToolItem(regexSearchButton); + } else if ((e.stateMask & SWT.CTRL) != 0 && (e.keyCode == 'A' || e.keyCode == 'a')) { + toggleToolItem(searchInSelectionButton); + } else if ((e.stateMask & SWT.CTRL) != 0 && (e.keyCode == 'C' || e.keyCode == 'c')) { + toggleToolItem(caseSensitiveSearchButton); + } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { + performEnterAction(e); + } else if (e.keyCode == SWT.ESC) { + close(); + } else { + e.doit = true; + } + }); + + private void performEnterAction(KeyEvent e) { + boolean isShiftPressed = (e.stateMask & SWT.SHIFT) != 0; + boolean isCtrlPressed = (e.stateMask & SWT.CTRL) != 0; + if (okayToUse(replaceBar) && replaceBar.isFocusControl()) { + if (isCtrlPressed) { + performReplaceAllOnEnter(); + } else { + performReplaceOnEnter(); + } + } else { + if (isCtrlPressed) { + performSearchAltOnEnter(); + } else { + performSearchOnEnter(isShiftPressed); + } + } + } + + private void performReplaceAllOnEnter() { + performReplaceAll(); + evaluateFindReplaceStatus(); + } + + private void performReplaceOnEnter() { + performSingleReplace(); + evaluateFindReplaceStatus(); + } + + private void performSearchAltOnEnter() { + performSelectAll(); + evaluateFindReplaceStatus(); + } + + private void performSearchOnEnter(boolean isShiftPressed) { + performSearch(!isShiftPressed); + evaluateFindReplaceStatus(); + } + + private void toggleToolItem(ToolItem toolItem) { + toolItem.setSelection(!toolItem.getSelection()); + toolItem.notifyListeners(SWT.Selection, null); + } + + private ControlListener shellMovementListener = new ControlListener() { + @Override + public void controlMoved(ControlEvent e) { + positionToPart(); + } + + @Override + public void controlResized(ControlEvent e) { + positionToPart(); + } + }; + + private FocusListener overlayFocusListener = FocusListener.focusLostAdapter(e -> { + findReplaceLogic.activate(SearchOptions.GLOBAL); + searchInSelectionButton.setSelection(false); + }); + + private PaintListener widgetMovementListener = __ -> positionToPart(); + + private IPartListener partListener = new IPartListener() { + @Override + public void partActivated(IWorkbenchPart part) { + if (getShell() != null) { + getShell().setVisible(isPartCurrentlyDisplayedInPartSash()); + } + } + + @Override + public void partDeactivated(IWorkbenchPart part) { + // Do nothing + } + + @Override + public void partBroughtToTop(IWorkbenchPart part) { + if (getShell() != null) { + getShell().setVisible(isPartCurrentlyDisplayedInPartSash()); + } + } + + @Override + public void partClosed(IWorkbenchPart part) { + close(); + } + + @Override + public void partOpened(IWorkbenchPart part) { + // Do nothing + } + }; + + private boolean isPartCurrentlyDisplayedInPartSash() { + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + + // Check if the targetPart is currently displayed on the active page + boolean isPartDisplayed = false; + + if (activePage != null) { + IWorkbenchPart activePart = activePage.getActivePart(); + if (activePart != null && activePart == targetPart) { + isPartDisplayed = true; + } + } + + return isPartDisplayed; + } + + /** + * Returns the dialog settings object used to share state between several + * find/replace overlays. + * + * @return the dialog settings to be used + */ + private static IDialogSettings getDialogSettings() { + IDialogSettings settings = PlatformUI + .getDialogSettingsProvider(FrameworkUtil.getBundle(FindReplaceOverlay.class)).getDialogSettings(); + return settings; + } + + @Override + public boolean close() { + if (!overlayOpen) { + return true; + } + storeOverlaySettings(); + + findReplaceLogic.activate(SearchOptions.GLOBAL); + overlayOpen = false; + replaceBarOpen = false; + unbindListeners(); + container.dispose(); + return super.close(); + } + + @Override + public int open() { + int returnCode = Window.OK; + if (!overlayOpen) { + returnCode = super.open(); + bindListeners(); + restoreOverlaySettings(); + } + overlayOpen = true; + applyOverlayColors(backgroundToUse, true); + initFindStringFromSelection(); + + getShell().layout(); + positionToPart(); + + searchBar.forceFocus(); + return returnCode; + } + + private void storeOverlaySettings() { + getDialogSettings().put(REPLACE_BAR_OPEN_DIALOG_SETTING, replaceBarOpen); + } + + private void restoreOverlaySettings() { + Boolean shouldOpenReplaceBar = getDialogSettings().getBoolean(REPLACE_BAR_OPEN_DIALOG_SETTING); + if (shouldOpenReplaceBar && replaceToggle != null) { + toggleReplace(); + } + } + + private void applyOverlayColors(Color color, boolean tryToColorReplaceBar) { + searchTools.setBackground(color); + searchInSelectionButton.setBackground(color); + wholeWordSearchButton.setBackground(color); + regexSearchButton.setBackground(color); + caseSensitiveSearchButton.setBackground(color); + searchAllButton.setBackground(color); + searchUpButton.setBackground(color); + searchDownButton.setBackground(color); + + searchBarContainer.setBackground(color); + searchBar.setBackground(color); + searchContainer.setBackground(color); + + if (replaceBarOpen && tryToColorReplaceBar) { + replaceContainer.setBackground(color); + replaceBar.setBackground(color); + replaceBarContainer.setBackground(color); + replaceAllButton.setBackground(color); + replaceButton.setBackground(color); + } + } + + private void unbindListeners() { + getShell().removeFocusListener(overlayFocusListener); + if (targetPart != null && targetPart instanceof StatusTextEditor textEditor) { + Control targetWidget = textEditor.getSourceViewer().getTextWidget(); + if (targetWidget != null) { + targetWidget.getShell().removeControlListener(shellMovementListener); + targetWidget.removePaintListener(widgetMovementListener); + targetPart.getSite().getPage().removePartListener(partListener); + } + } + } + + private void bindListeners() { + getShell().addFocusListener(overlayFocusListener); + if (targetPart instanceof StatusTextEditor textEditor) { + Control targetWidget = textEditor.getSourceViewer().getTextWidget(); + + targetWidget.getShell().addControlListener(shellMovementListener); + targetWidget.addPaintListener(widgetMovementListener); + targetPart.getSite().getPage().addPartListener(partListener); + } + } + + @Override + public Control createContents(Composite parent) { + backgroundToUse = new Color(getShell().getDisplay(), new RGBA(0, 0, 0, 0)); + Control ret = createDialog(parent); + + getShell().layout(); + positionToPart(); + return ret; + } + + private Control createDialog(final Composite parent) { + createMainContainer(parent); + + retrieveBackgroundColor(); + + createFindContainer(); + createSearchBar(); + createSearchTools(); + + container.layout(); + + applyDialogFont(container); + return container; + } + + /** + * HACK: In order to not introduce a hard-coded color, we need to retrieve the + * color of the "SWT.SEARCH"-Text. Since that search-bar has a border, we don't + * want to have it in our own form. Instead, we create such a bar at start-up, + * grab it's color and then immediately dispose of that bar. + */ + private void retrieveBackgroundColor() { + Text textBarForRetrievingTheRightColor = new Text(container, SWT.SINGLE | SWT.SEARCH); + container.layout(); + backgroundToUse = textBarForRetrievingTheRightColor.getBackground(); + normalTextForegroundColor = textBarForRetrievingTheRightColor.getForeground(); + textBarForRetrievingTheRightColor.dispose(); + } + + private void createSearchTools() { + searchTools = new ToolBar(searchContainer, SWT.HORIZONTAL); + GridDataFactory.fillDefaults().grab(false, true).align(GridData.CENTER, GridData.END).applyTo(searchTools); + + createWholeWordsButton(); + createCaseSensitiveButton(); + createRegexSearchButton(); + createAreaSearchButton(); + + @SuppressWarnings("unused") + ToolItem separator = new ToolItem(searchTools, SWT.SEPARATOR); + + searchUpButton = new ToolItem(searchTools, SWT.PUSH); + searchUpButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_FIND_PREV)); + searchUpButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_upSearchButton_toolTip); + searchUpButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + performSearch(false); + evaluateFindReplaceStatus(); + })); + searchDownButton = new ToolItem(searchTools, SWT.PUSH); + searchDownButton.setSelection(true); // by default, search down + searchDownButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_FIND_NEXT)); + searchDownButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_downSearchButton_toolTip); + searchDownButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + performSearch(true); + evaluateFindReplaceStatus(); + })); + searchAllButton = new ToolItem(searchTools, SWT.PUSH); + searchAllButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_SEARCH_ALL)); + searchAllButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_searchAllButton_toolTip); + searchAllButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + performSelectAll(); + evaluateFindReplaceStatus(); + })); + } + + private void createAreaSearchButton() { + searchInSelectionButton = new ToolItem(searchTools, SWT.CHECK); + searchInSelectionButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_SEARCH_IN_AREA)); + searchInSelectionButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_searchInSelectionButton_toolTip); + searchInSelectionButton.setSelection(findReplaceLogic.isActive(SearchOptions.WHOLE_WORD)); + searchInSelectionButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + activateInFindReplacerIf(SearchOptions.GLOBAL, !searchInSelectionButton.getSelection()); + updateIncrementalSearch(); + })); + } + + private void createRegexSearchButton() { + regexSearchButton = new ToolItem(searchTools, SWT.CHECK); + regexSearchButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_FIND_REGEX)); + regexSearchButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_regexSearchButton_toolTip); + regexSearchButton.setSelection(findReplaceLogic.isActive(SearchOptions.REGEX)); + regexSearchButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + activateInFindReplacerIf(SearchOptions.REGEX, ((ToolItem) e.widget).getSelection()); + wholeWordSearchButton.setEnabled(!findReplaceLogic.isActive(SearchOptions.REGEX)); + updateIncrementalSearch(); + })); + } + + private void createCaseSensitiveButton() { + caseSensitiveSearchButton = new ToolItem(searchTools, SWT.CHECK); + caseSensitiveSearchButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_CASE_SENSITIVE)); + caseSensitiveSearchButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_caseSensitiveButton_toolTip); + caseSensitiveSearchButton.setSelection(findReplaceLogic.isActive(SearchOptions.CASE_SENSITIVE)); + caseSensitiveSearchButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + activateInFindReplacerIf(SearchOptions.CASE_SENSITIVE, caseSensitiveSearchButton.getSelection()); + updateIncrementalSearch(); + })); + } + + private void createWholeWordsButton() { + wholeWordSearchButton = new ToolItem(searchTools, SWT.CHECK); + wholeWordSearchButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_WHOLE_WORD)); + wholeWordSearchButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_wholeWordsButton_toolTip); + wholeWordSearchButton.setSelection(findReplaceLogic.isActive(SearchOptions.WHOLE_WORD)); + wholeWordSearchButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + activateInFindReplacerIf(SearchOptions.WHOLE_WORD, wholeWordSearchButton.getSelection()); + updateIncrementalSearch(); + })); + } + + private void createReplaceTools() { + Color warningColor = JFaceColors.getErrorText(getShell().getDisplay()); + + replaceTools = new ToolBar(replaceContainer, SWT.HORIZONTAL); + GridDataFactory.fillDefaults().grab(false, true).align(GridData.CENTER, GridData.END).applyTo(replaceTools); + replaceButton = new ToolItem(replaceTools, SWT.PUSH); + replaceButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_REPLACE)); + replaceButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceButton_toolTip); + replaceButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + if (getFindString().isEmpty()) { + showUserFeedback(warningColor, true); + return; + } + performSingleReplace(); + evaluateFindReplaceStatus(); + })); + replaceAllButton = new ToolItem(replaceTools, SWT.PUSH); + replaceAllButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_REPLACE_ALL)); + replaceAllButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceAllButton_toolTip); + replaceAllButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + if (getFindString().isEmpty()) { + showUserFeedback(warningColor, true); + return; + } + performReplaceAll(); + evaluateFindReplaceStatus(); + })); + } + + private void createSearchBar() { + searchBar = new Text(searchBarContainer, SWT.SINGLE); + GridDataFactory.fillDefaults().grab(true, false).align(GridData.FILL, GridData.END).applyTo(searchBar); + searchBar.forceFocus(); + searchBar.selectAll(); + searchBar.addModifyListener(e -> { + wholeWordSearchButton.setEnabled(findReplaceLogic.isWholeWordSearchAvailable(getFindString())); + + showUserFeedback(normalTextForegroundColor, true); + // don't perform incremental search if we are already on the word. + if (!getFindString().equals(findReplaceLogic.getTarget().getSelectionText())) { + updateIncrementalSearch(); + } + }); + searchBar.addFocusListener(new FocusListener() { + + @Override + public void focusGained(FocusEvent e) { + // we want to update the base-location of where we start incremental search + // to the currently selected position in the target + // when coming back into the dialog + findReplaceLogic.deactivate(SearchOptions.INCREMENTAL); + findReplaceLogic.activate(SearchOptions.INCREMENTAL); + } + + @Override + public void focusLost(FocusEvent e) { + showUserFeedback(normalTextForegroundColor, false); + } + + }); + searchBar.addKeyListener(shortcuts); + searchBar.setMessage(FindReplaceMessages.FindReplaceOverlay_searchBar_message); + } + + private void updateIncrementalSearch() { + // clear the current incrementally searched selection to avoid having an old + // selection left when incrementally searching for an invalid string + if (findReplaceLogic.getTarget() instanceof IFindReplaceTargetExtension targetExtension) { + targetExtension.setSelection(targetExtension.getLineSelection().x, 0); + } + findReplaceLogic.performIncrementalSearch(getFindString()); + evaluateFindReplaceStatus(); + } + + private void createReplaceBar() { + replaceBar = new Text(replaceBarContainer, SWT.SINGLE); + GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.END).applyTo(replaceBar); + replaceBar.setMessage(FindReplaceMessages.FindReplaceOverlay_replaceBar_message); + replaceBar.addFocusListener(FocusListener.focusLostAdapter(e -> { + replaceBar.setForeground(normalTextForegroundColor); + searchBar.setForeground(normalTextForegroundColor); + })); + replaceBar.addKeyListener(shortcuts); + } + + private void createFindContainer() { + searchContainer = new Composite(contentGroup, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.FILL).applyTo(searchContainer); + GridLayoutFactory.fillDefaults().numColumns(2).extendedMargins(4, 4, 2, 8).equalWidth(false) + .applyTo(searchContainer); + searchContainer.setBackground(getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)); + searchBarContainer = new Composite(searchContainer, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.END).applyTo(searchBarContainer); + GridLayoutFactory.fillDefaults().numColumns(1).applyTo(searchBarContainer); + } + + private void createReplaceContainer() { + replaceContainer = new Composite(contentGroup, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.FILL).applyTo(replaceContainer); + GridLayoutFactory.fillDefaults().margins(0, 1).numColumns(2).extendedMargins(4, 4, 2, 8).equalWidth(false) + .applyTo(replaceContainer); + replaceContainer.setBackground(getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)); + replaceBarContainer = new Composite(replaceContainer, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.END).applyTo(replaceBarContainer); + GridLayoutFactory.fillDefaults().numColumns(1).equalWidth(false).applyTo(replaceBarContainer); + } + + private void createMainContainer(final Composite parent) { + container = new Composite(parent, SWT.NONE); + GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).margins(2, 2).spacing(2, 0).applyTo(container); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.FILL).applyTo(container); + + if (findReplaceLogic.getTarget().isEditable()) { + createReplaceToggle(); + } + + contentGroup = new Composite(container, SWT.NULL); + GridLayoutFactory.fillDefaults().numColumns(1).equalWidth(false).spacing(2, 3).applyTo(contentGroup); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.FILL).applyTo(contentGroup); + } + + private void createReplaceToggle() { + replaceToggle = new Button(container, SWT.PUSH); + GridDataFactory.fillDefaults().grab(false, true).align(GridData.BEGINNING, GridData.FILL) + .applyTo(replaceToggle); + replaceToggle.setToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceToggle_toolTip); + replaceToggle.setText("⯈"); //$NON-NLS-1$ + replaceToggle.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> toggleReplace())); + } + + private void toggleReplace() { + if (!replaceBarOpen) { + createReplaceDialog(); + replaceToggle.setText("⯅"); //$NON-NLS-1$ + } else { + hideReplace(); + replaceToggle.setText("⯈"); //$NON-NLS-1$ + } + replaceToggle.setSelection(false); // We don't want the button to look "locked in", so don't + // use it's selectionState + } + + private void hideReplace() { + if (!replaceBarOpen) { + return; + } + searchBar.forceFocus(); + replaceBarOpen = false; + replaceContainer.dispose(); + positionToPart(); + } + + private void createReplaceDialog() { + if (replaceBarOpen) { + return; + } + replaceBarOpen = true; + createReplaceContainer(); + createReplaceBar(); + createReplaceTools(); + positionToPart(); + applyOverlayColors(backgroundToUse, true); + replaceBar.forceFocus(); + } + + private void enableSearchTools(boolean enable) { + ((GridData) searchTools.getLayoutData()).exclude = !enable; + searchTools.setVisible(enable); + + if (enable) { + ((GridLayout) searchTools.getParent().getLayout()).numColumns = 2; + } else { + ((GridLayout) searchTools.getParent().getLayout()).numColumns = 1; + } + } + + private void enableReplaceToggle(boolean enable) { + if (!okayToUse(replaceToggle)) { + return; + } + ((GridData) replaceToggle.getLayoutData()).exclude = !enable; + replaceToggle.setVisible(enable); + } + + private void enableReplaceTools(boolean enable) { + if (!okayToUse(replaceTools)) { + return; + } + ((GridData) replaceTools.getLayoutData()).exclude = !enable; + replaceTools.setVisible(enable); + + if (enable) { + ((GridLayout) replaceTools.getParent().getLayout()).numColumns = 2; + } else { + ((GridLayout) replaceTools.getParent().getLayout()).numColumns = 1; + } + } + + private int getIdealDialogWidth(Rectangle targetBounds) { + int replaceToggleWidth = 0; + if (okayToUse(replaceToggle)) { + replaceToggleWidth = replaceToggle.getBounds().width; + } + int toolBarWidth = searchTools.getSize().x; + GC gc = new GC(searchBar); + gc.setFont(searchBar.getFont()); + int idealWidth = gc.stringExtent(IDEAL_WIDTH_TEXT).x; // $NON-NLS-1$ + int idealCompromiseWidth = gc.stringExtent(COMPROMISE_WIDTH_TEXT).x; // $NON-NLS-1$ + int worstCompromiseWidth = gc.stringExtent(MINIMAL_WIDTH_TEXT).x; // $NON-NLS-1$ + gc.dispose(); + + int newWidth = idealWidth + toolBarWidth + replaceToggleWidth; + if (newWidth > targetBounds.width * BIG_WIDTH_RATIO_EDITOR_TO_OVERLAY) { + newWidth = (int) (targetBounds.width * BIG_WIDTH_RATIO_EDITOR_TO_OVERLAY); + enableSearchTools(true); + enableReplaceTools(true); + enableReplaceToggle(true); + } + if (newWidth < idealCompromiseWidth + toolBarWidth) { + enableSearchTools(false); + enableReplaceTools(false); + enableReplaceToggle(true); + } + if (newWidth < worstCompromiseWidth + toolBarWidth) { + newWidth = (int) (targetBounds.width * WORST_CASE_RATIO_EDITOR_TO_OVERLAY); + enableReplaceToggle(false); + enableSearchTools(false); + enableReplaceTools(false); + } + return newWidth; + } + + private Point getNewPosition(Widget targetTextWidget, Point targetOrigin, Rectangle targetBounds, + Point expectedSize) { + Point verticalScrollBarSize = ((Scrollable) targetTextWidget).getVerticalBar().getSize(); + Point horizontalScrollBarSize = ((Scrollable) targetTextWidget).getHorizontalBar().getSize(); + + int newX = targetOrigin.x + targetBounds.width - expectedSize.x - verticalScrollBarSize.x + - ((StyledText) targetTextWidget).getRightMargin(); + int newY = targetOrigin.y; + if (!positionAtTop) { + newY += targetBounds.height - expectedSize.y - horizontalScrollBarSize.y; + } + return new Point(newX, newY); + } + + /** + * When making the text-bar 100% small and then regrowing it, we want the text + * to start at the first character again. + */ + private void repositionTextSelection() { + if (okayToUse(searchBar) && !searchBar.isFocusControl()) { + searchBar.setSelection(0, 0); + } + if (okayToUse(replaceBar) && !replaceBar.isFocusControl()) { + replaceBar.setSelection(0, 0); + } + } + + private void positionToPart() { + getShell().requestLayout(); + if (!(targetPart instanceof StatusTextEditor)) { + return; + } + + StatusTextEditor textEditor = (StatusTextEditor) targetPart; + Control targetWidget = textEditor.getSourceViewer().getTextWidget(); + if (!okayToUse(targetWidget)) { + this.close(); + return; + } + + Point targetOrigin = targetWidget.toDisplay(0, 0); + Rectangle targetBounds = targetWidget.getBounds(); + + int newWidth = getIdealDialogWidth(targetBounds); + int newHeight = container.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; + + Point newPosition = getNewPosition(targetWidget, targetOrigin, targetBounds, new Point(newWidth, newHeight)); + + getShell().setSize(new Point(newWidth, newHeight)); + getShell().setLocation(newPosition); + getShell().layout(true); + + repositionTextSelection(); + } + + private String getFindString() { + return searchBar.getText(); + } + + private String getReplaceString() { + if (!okayToUse(replaceBar)) { + return ""; //$NON-NLS-1$ + } + return replaceBar.getText(); + + } + + private void performSingleReplace() { + findReplaceLogic.performReplaceAndFind(getFindString(), getReplaceString()); + } + + private void performSearch(boolean forward) { + boolean oldForwardSearchSetting = findReplaceLogic.isActive(SearchOptions.FORWARD); + activateInFindReplacerIf(SearchOptions.FORWARD, forward); + findReplaceLogic.deactivate(SearchOptions.INCREMENTAL); + findReplaceLogic.performSearch(getFindString()); + activateInFindReplacerIf(SearchOptions.FORWARD, oldForwardSearchSetting); + findReplaceLogic.activate(SearchOptions.INCREMENTAL); + } + + private void initFindStringFromSelection() { + String initText = findReplaceLogic.getTarget().getSelectionText(); + if (initText.isEmpty()) { + return; + } + if (initText.contains(System.lineSeparator())) { // $NON-NLS-1$ + findReplaceLogic.deactivate(SearchOptions.GLOBAL); + searchInSelectionButton.setSelection(true); + } else { + searchBar.setText(initText); + searchBar.setSelection(0, initText.length()); + } + } + + private void evaluateFindReplaceStatus() { + Color warningColor = JFaceColors.getErrorText(getShell().getDisplay()); + IFindReplaceStatus status = findReplaceLogic.getStatus(); + + if (!status.wasSuccessful()) { + boolean colorReplaceBar = okayToUse(replaceBar) && replaceBar.isFocusControl(); + showUserFeedback(warningColor, colorReplaceBar); + } else { + showUserFeedback(normalTextForegroundColor, false); + } + } + + private void showUserFeedback(Color feedbackColor, boolean colorReplaceBar) { + searchBar.setForeground(feedbackColor); + if (colorReplaceBar && okayToUse(replaceBar)) { + replaceBar.setForeground(feedbackColor); + } + } + + private void activateInFindReplacerIf(SearchOptions option, boolean shouldActivate) { + if (shouldActivate) { + findReplaceLogic.activate(option); + } else { + findReplaceLogic.deactivate(option); + } + } + + private static boolean okayToUse(Widget widget) { + return widget != null && !widget.isDisposed(); + } + + public void setPositionToTop(boolean shouldPositionOverlayOnTop) { + positionAtTop = shouldPositionOverlayOnTop; + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayFirstTimePopup.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayFirstTimePopup.java new file mode 100644 index 00000000000..e274a7db02f --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayFirstTimePopup.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.texteditor; + +import java.time.Duration; +import java.util.Objects; + +import org.osgi.framework.FrameworkUtil; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; + +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.notifications.NotificationPopup; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.findandreplace.FindReplaceMessages; + +/** + * Utility class to display a popup the first time the FindReplaceOverlay is + * shown, informing the user about the new functionality. This class will track + * whether the popup was already shown and will only show the Overlay on the + * first time the popup was shown. + */ +class FindReplaceOverlayFirstTimePopup { + + private FindReplaceOverlayFirstTimePopup() { + } + + private static final String PREFERENCE_NODE_NAME = "org.eclipse.ui.editors"; //$NON-NLS-1$ + private static final String SETTING_POPUP_WAS_SHOWN_BEFORE = "hasShownOverlayPopupBefore"; //$NON-NLS-1$ + /** + * How long to wait until the pop up should vanish in Ms. + */ + private static final Duration POPUP_VANISH_TIME = Duration.ofSeconds(6); + private static final String USE_FIND_REPLACE_OVERLAY = "useFindReplaceOverlay"; //$NON-NLS-1$ + + /** + * Returns the dialog settings object used to remember whether the popup was + * already shown or not. + * + * @return the dialog settings to be used + */ + private static IDialogSettings getDialogSettings() { + IDialogSettings settings = PlatformUI + .getDialogSettingsProvider(FrameworkUtil.getBundle(FindReplaceOverlayFirstTimePopup.class)) + .getDialogSettings(); + return settings; + } + + private static void disableUseOverlayPreference() { + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(PREFERENCE_NODE_NAME); // $NON-NLS-1$ + preferences.putBoolean(USE_FIND_REPLACE_OVERLAY, false); + } + + /** + * Displays a popup indicating that instead of the FindReplaceDialog, the + * FindReplaceOverlay is currently being used. Only displays the popup on the + * first time use of FindReplaceOverlay. + * + * The popup is bound to the bottom right corner of the principal computer + * Monitor. + * + * @param shellToUse the shell to bind the popup to + */ + public static void displayPopupIfNotAlreadyShown(Shell shellToUse) { + IDialogSettings settings = getDialogSettings(); + + if (!settings.getBoolean(SETTING_POPUP_WAS_SHOWN_BEFORE)) { + settings.put(SETTING_POPUP_WAS_SHOWN_BEFORE, true); + + Display displayToUse = Objects.nonNull(shellToUse) ? shellToUse.getDisplay() : Display.getDefault(); + + NotificationPopup.forDisplay(displayToUse).content(t -> createFirstTimeNotification(t)) + .title(FindReplaceMessages.FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_title, + true) + .delay(POPUP_VANISH_TIME.toMillis()).open(); + } + + } + + private static Control createFirstTimeNotification(Composite composite) { + Link messageBody = new Link(composite, SWT.WRAP); + + messageBody + .setText(FindReplaceMessages.FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_message); + messageBody.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 1, 1)); + messageBody.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + disableUseOverlayPreference(); + composite.getShell().close(); + })); + + return messageBody; + } +} diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayImages.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayImages.java new file mode 100644 index 00000000000..2017e13f966 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayImages.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.texteditor; + +import java.net.URL; + +import org.osgi.framework.Bundle; + +import org.eclipse.swt.graphics.Image; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Platform; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.ImageRegistry; + +import org.eclipse.ui.internal.texteditor.TextEditorPlugin; + +/** + * Provides Icons for the editor overlay used for performing + * find/replace-operations. + */ +class FindReplaceOverlayImages { + static final String PREFIX_OBJ = TextEditorPlugin.PLUGIN_ID + ".obj."; //$NON-NLS-1$ + + static final String OBJ_FIND_NEXT = PREFIX_OBJ + "select_next.png"; //$NON-NLS-1$ + + static final String OBJ_FIND_PREV = PREFIX_OBJ + "select_prev.png"; //$NON-NLS-1$ + + static final String OBJ_FIND_REGEX = PREFIX_OBJ + "regex_gear.gif"; //$NON-NLS-1$ + + static final String OBJ_REPLACE = PREFIX_OBJ + "replace.png"; //$NON-NLS-1$ + + static final String OBJ_REPLACE_ALL = PREFIX_OBJ + "replace_all.png"; //$NON-NLS-1$ + + static final String OBJ_WHOLE_WORD = PREFIX_OBJ + "whole_word.png"; //$NON-NLS-1$ + + static final String OBJ_CASE_SENSITIVE = PREFIX_OBJ + "case_sensitive.png"; //$NON-NLS-1$ + + static final String OBJ_SEARCH_ALL = PREFIX_OBJ + "search_all.png"; //$NON-NLS-1$ + + static final String OBJ_SEARCH_IN_AREA = PREFIX_OBJ + "search_in_selection.png"; //$NON-NLS-1$ + + /** + * The image registry containing {@link Image images}. + */ + private static ImageRegistry fgImageRegistry; + + private static String ICONS_PATH = "$nl$/icons/full/"; //$NON-NLS-1$ + + private final static String OBJ = ICONS_PATH + "obj16/"; //$NON-NLS-1$ + + /** + * Declare all images + */ + private static void declareImages() { + declareRegistryImage(OBJ_FIND_NEXT, OBJ + "select_next.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_FIND_PREV, OBJ + "select_prev.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_FIND_REGEX, OBJ + "regex.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_REPLACE_ALL, OBJ + "replace_all.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_REPLACE, OBJ + "replace.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_WHOLE_WORD, OBJ + "whole_word.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_CASE_SENSITIVE, OBJ + "case_sensitive.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_SEARCH_ALL, OBJ + "search_all.png"); //$NON-NLS-1$ + declareRegistryImage(OBJ_SEARCH_IN_AREA, OBJ + "search_in_area.png"); //$NON-NLS-1$ + } + + /** + * Declare an Image in the registry table. + * + * @param key the key to use when registering the image + * @param path the path where the image can be found. This path is relative to + * where this plugin class is found (i.e. typically the packages + * directory) + */ + private final static void declareRegistryImage(String key, String path) { + ImageDescriptor desc = ImageDescriptor.getMissingImageDescriptor(); + Bundle bundle = Platform.getBundle(TextEditorPlugin.PLUGIN_ID); + URL url = null; + if (bundle != null) { + url = FileLocator.find(bundle, IPath.fromOSString(path), null); + desc = ImageDescriptor.createFromURL(url); + } + fgImageRegistry.put(key, desc); + } + + /** + * Returns the ImageRegistry. + * + * @return image registry + */ + public static ImageRegistry getImageRegistry() { + if (fgImageRegistry == null) { + initializeImageRegistry(); + } + return fgImageRegistry; + } + + /** + * Initialize the image registry by declaring all of the required graphics. This + * involves creating JFace image descriptors describing how to create/find the + * image should it be needed. The image is not actually allocated until + * requested. + * + * Prefix conventions Wizard Banners WIZBAN_ Preference Banners PREF_BAN_ + * Property Page Banners PROPBAN_ Color toolbar CTOOL_ Enable toolbar ETOOL_ + * Disable toolbar DTOOL_ Local enabled toolbar ELCL_ Local Disable toolbar + * DLCL_ Object large OBJL_ Object small OBJS_ View VIEW_ Product images PROD_ + * Misc images MISC_ + * + * Where are the images? The images (typically pngs) are found in the same + * location as this plugin class. This may mean the same package directory as + * the package holding this class. The images are declared using this.getClass() + * to ensure they are looked up via this plugin class. + * + * @return the image registry + * @see org.eclipse.jface.resource.ImageRegistry + */ + public static ImageRegistry initializeImageRegistry() { + fgImageRegistry = TextEditorPlugin.getDefault().getImageRegistry(); + declareImages(); + return fgImageRegistry; + } + + /** + * Returns the image managed under the given key in this registry. + * + * @param key the image's key + * @return the image managed under the given key + */ + public static Image get(String key) { + return getImageRegistry().get(key); + } + + /** + * Returns the image descriptor for the given key in this registry. + * + * @param key the image's key + * @return the image descriptor for the given key + */ + public static ImageDescriptor getDescriptor(String key) { + return getImageRegistry().getDescriptor(key); + } +} diff --git a/tests/org.eclipse.text.tests/src/org/eclipse/text/tests/Accessor.java b/tests/org.eclipse.text.tests/src/org/eclipse/text/tests/Accessor.java index 262da670f00..15887e83d06 100644 --- a/tests/org.eclipse.text.tests/src/org/eclipse/text/tests/Accessor.java +++ b/tests/org.eclipse.text.tests/src/org/eclipse/text/tests/Accessor.java @@ -161,9 +161,15 @@ public Object invoke(String methodName, Object[] arguments) { public Object invoke(String methodName, Class[] types, Object[] arguments) { Method method= null; try { - method= fClass.getDeclaredMethod(methodName, types); - } catch (SecurityException | NoSuchMethodException e) { - throw (AssertionFailedException) new AssertionFailedException(e.getLocalizedMessage()).initCause(e); + method = fClass.getDeclaredMethod(methodName, types); + Assert.isNotNull(method); + method.setAccessible(true); + } catch (SecurityException | NoSuchMethodException __) { + try { + method = fClass.getMethod(methodName, types); + } catch (SecurityException | NoSuchMethodException e) { + throw (AssertionFailedException) new AssertionFailedException(e.getLocalizedMessage()).initCause(e); + } } Assert.isNotNull(method); method.setAccessible(true); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/FindReplaceOverlayTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/FindReplaceOverlayTest.java new file mode 100644 index 00000000000..d59aee0fee4 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/FindReplaceOverlayTest.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.workbench.texteditor.tests; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; + +import java.util.ResourceBundle; + +import org.junit.Test; + +import org.eclipse.swt.widgets.Shell; + +import org.eclipse.text.tests.Accessor; + +import org.eclipse.jface.text.IFindReplaceTarget; +import org.eclipse.jface.text.TextViewer; + +import org.eclipse.ui.internal.findandreplace.SearchOptions; + +public class FindReplaceOverlayTest extends FindReplaceUITest { + + @Override + public OverlayAccess openUIFromTextViewer(TextViewer viewer) { + OverlayAccess ret; + + Accessor fFindReplaceAction; + fFindReplaceAction= new Accessor("org.eclipse.ui.texteditor.FindReplaceAction", getClass().getClassLoader(), + new Class[] { ResourceBundle.class, String.class, Shell.class, IFindReplaceTarget.class }, + new Object[] { ResourceBundle.getBundle("org.eclipse.ui.texteditor.ConstructedEditorMessages"), "Editor.FindReplace.", viewer.getControl().getShell(), + getTextViewer().getFindReplaceTarget() }); + fFindReplaceAction.invoke("showOverlayInEditor", null); + Accessor overlayAccessor= new Accessor(fFindReplaceAction.get("overlay"), "org.eclipse.ui.texteditor.FindReplaceOverlay", getClass().getClassLoader()); + + ret= new OverlayAccess(overlayAccessor); + return ret; + } + + @Test + public void testDirectionalSearchButtons() { + initializeTextViewerWithFindReplaceUI("line\nline\nline\nline"); + OverlayAccess dialog= getDialog(); + + dialog.setFindText("line"); + IFindReplaceTarget target= dialog.getTarget(); + + assertEquals(0, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.pressSearch(true); + assertEquals(5, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.pressSearch(true); + assertEquals(10, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.pressSearch(false); + assertEquals(5, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.pressSearch(true); + assertEquals(10, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.pressSearch(false); + assertEquals(5, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.pressSearch(false); + assertEquals(0, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + } + + @Test + public void testIncrementalSearchUpdatesAfterChangingOptions() { + initializeTextViewerWithFindReplaceUI("alinee\naLinee\nline\nline"); + OverlayAccess dialog= getDialog(); + IFindReplaceTarget target= dialog.getTarget(); + + dialog.setFindText("Line"); + dialog.select(SearchOptions.CASE_SENSITIVE); + assertThat(dialog.getTarget().getSelectionText(), is("Line")); + + dialog.unselect(SearchOptions.CASE_SENSITIVE); + assertEquals(1, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.select(SearchOptions.WHOLE_WORD); + assertEquals(14, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + + dialog.unselect(SearchOptions.CASE_SENSITIVE); + dialog.unselect(SearchOptions.WHOLE_WORD); + assertEquals(1, (target.getSelection()).x); + assertEquals(4, (target.getSelection()).y); + assertThat(dialog.getTarget().getSelectionText(), is("line")); + } + + @Test + public void testCantOpenReplaceDialogInReadOnlyEditor() { + openTextViewer("text"); + getTextViewer().setEditable(false); + initializeFindReplaceUIForTextViewer(); + OverlayAccess dialog= getDialog(); + + dialog.openReplaceDialog(); + reopenFindReplaceUIForTextViewer(); + dialog= getDialog(); + assertThat(dialog.isReplaceDialogOpen(), is(false)); + } + + @Test + public void testRememberReplaceExpandState() { + initializeTextViewerWithFindReplaceUI("text"); + OverlayAccess dialog= getDialog(); + + dialog.openReplaceDialog(); + assertThat(dialog.isReplaceDialogOpen(), is(true)); + reopenFindReplaceUIForTextViewer(); + dialog= getDialog(); + assertThat(dialog.isReplaceDialogOpen(), is(true)); + + dialog.closeReplaceDialog(); + reopenFindReplaceUIForTextViewer(); + dialog= getDialog(); + assertThat(dialog.isReplaceDialogOpen(), is(false)); + + dialog.openReplaceDialog(); + getTextViewer().setEditable(false); + reopenFindReplaceUIForTextViewer(); + dialog= getDialog(); + assertThat(dialog.isReplaceDialogOpen(), is(false)); + } + + @Test + public void testSearchBackwardsWithRegEx() { + initializeTextViewerWithFindReplaceUI("text text text"); + + OverlayAccess dialog= getDialog(); + dialog.select(SearchOptions.REGEX); + dialog.setFindText("text"); // with RegEx enabled, there is no incremental search! + dialog.pressSearch(true); + assertThat(dialog.getTarget().getSelection().y, is(4)); + dialog.pressSearch(true); + assertThat(dialog.getTarget().getSelection().x, is("text ".length())); + dialog.pressSearch(true); + assertThat(dialog.getTarget().getSelection().x, is("text text ".length())); + dialog.pressSearch(false); + assertThat(dialog.getTarget().getSelection().x, is("text ".length())); + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java new file mode 100644 index 00000000000..575d4e67c87 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java @@ -0,0 +1,328 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.workbench.texteditor.tests; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolItem; + +import org.eclipse.text.tests.Accessor; + +import org.eclipse.jface.text.IFindReplaceTarget; +import org.eclipse.jface.text.IFindReplaceTargetExtension; + +import org.eclipse.ui.internal.findandreplace.FindReplaceLogic; +import org.eclipse.ui.internal.findandreplace.IFindReplaceLogic; +import org.eclipse.ui.internal.findandreplace.SearchOptions; + +class OverlayAccess implements IFindReplaceUIAccess { + FindReplaceLogic findReplaceLogic; + + Text find; + + Text replace; + + ToolItem inSelection; + + ToolItem caseSensitive; + + ToolItem wholeWord; + + ToolItem regEx; + + ToolItem searchForward; + + ToolItem searchBackward; + + Button openReplaceDialog; + + ToolItem replaceButton; + + ToolItem replaceAllButton; + + private Runnable closeOperation; + + Accessor dialogAccessor; + + private Supplier shellRetriever; + + OverlayAccess(Accessor findReplaceOverlayAccessor) { + dialogAccessor= findReplaceOverlayAccessor; + findReplaceLogic= (FindReplaceLogic) findReplaceOverlayAccessor.get("findReplaceLogic"); + find= (Text) findReplaceOverlayAccessor.get("searchBar"); + replace= (Text) findReplaceOverlayAccessor.get("replaceBar"); + caseSensitive= (ToolItem) findReplaceOverlayAccessor.get("caseSensitiveSearchButton"); + wholeWord= (ToolItem) findReplaceOverlayAccessor.get("wholeWordSearchButton"); + regEx= (ToolItem) findReplaceOverlayAccessor.get("regexSearchButton"); + searchForward= (ToolItem) findReplaceOverlayAccessor.get("searchDownButton"); + searchBackward= (ToolItem) findReplaceOverlayAccessor.get("searchUpButton"); + closeOperation= () -> findReplaceOverlayAccessor.invoke("close", null); + openReplaceDialog= (Button) findReplaceOverlayAccessor.get("replaceToggle"); + replaceButton= (ToolItem) findReplaceOverlayAccessor.get("replaceButton"); + replaceAllButton= (ToolItem) findReplaceOverlayAccessor.get("replaceAllButton"); + inSelection= (ToolItem) findReplaceOverlayAccessor.get("searchInSelectionButton"); + shellRetriever= () -> ((Shell) findReplaceOverlayAccessor.invoke("getShell", null)); + } + + @Override + public IFindReplaceTarget getTarget() { + return findReplaceLogic.getTarget(); + } + + private void restoreInitialConfiguration() { + find.setText(""); + select(SearchOptions.GLOBAL); + unselect(SearchOptions.REGEX); + unselect(SearchOptions.CASE_SENSITIVE); + unselect(SearchOptions.WHOLE_WORD); + } + + @Override + public void closeAndRestore() { + restoreInitialConfiguration(); + assertInitialConfiguration(); + closeOperation.run(); + } + + @Override + public void close() { + closeOperation.run(); + } + + @Override + public void select(SearchOptions option) { + ToolItem button= getButtonForSearchOption(option); + if (button == null) { + return; + } + button.setSelection(true); + if (option == SearchOptions.GLOBAL) { + button.setSelection(false); + } + button.notifyListeners(SWT.Selection, null); + } + + @Override + public void unselect(SearchOptions option) { + ToolItem button= getButtonForSearchOption(option); + if (button == null) { + return; + } + button.setSelection(false); + if (option == SearchOptions.GLOBAL) { + button.setSelection(true); + } + button.notifyListeners(SWT.Selection, null); + } + + @Override + public void simulateEnterInFindInputField(boolean shiftPressed) { + simulateKeyPressInFindInputField(SWT.CR, shiftPressed); + } + + @Override + public void simulateKeyPressInFindInputField(int keyCode, boolean shiftPressed) { + final Event event= new Event(); + event.type= SWT.KeyDown; + event.keyCode= keyCode; + if (shiftPressed) { + event.stateMask= SWT.SHIFT; + } + find.notifyListeners(SWT.KeyDown, event); + find.traverse(SWT.TRAVERSE_RETURN, event); + FindReplaceTestUtil.runEventQueue(); + } + + @Override + public String getFindText() { + return find.getText(); + } + + @Override + public String getReplaceText() { + return replace.getText(); + } + + @Override + public void setFindText(String text) { + find.setText(text); + find.notifyListeners(SWT.Modify, null); + } + + @Override + public void setReplaceText(String text) { + openReplaceDialog(); + replace.setText(text); + } + + @Override + public ToolItem getButtonForSearchOption(SearchOptions option) { + switch (option) { + case CASE_SENSITIVE: + return caseSensitive; + case REGEX: + return regEx; + case WHOLE_WORD: + return wholeWord; + case GLOBAL: + return inSelection; + //$CASES-OMITTED$ + default: + return null; + } + } + + private Set getEnabledOptions() { + return Arrays.stream(SearchOptions.values()) + .filter(option -> (getButtonForSearchOption(option) != null && getButtonForSearchOption(option).getEnabled())) + .collect(Collectors.toSet()); + } + + private Set getSelectedOptions() { + return Arrays.stream(SearchOptions.values()) + .filter(isOptionSelected()) + .collect(Collectors.toSet()); + } + + private Predicate isOptionSelected() { + return option -> { + ToolItem buttonForSearchOption= getButtonForSearchOption(option); + if (option == SearchOptions.GLOBAL) { + return !buttonForSearchOption.getSelection();// The "Global" option is mapped to a button that + // selects whether to search in the selection, thus inverting the semantic + } + return buttonForSearchOption != null && buttonForSearchOption.getSelection(); + }; + } + + public void pressSearch(boolean forward) { + if (forward) { + searchForward.notifyListeners(SWT.Selection, null); + } else { + searchBackward.notifyListeners(SWT.Selection, null); + } + } + + @Override + public IFindReplaceLogic getFindReplaceLogic() { + return findReplaceLogic; + } + + @Override + public void performReplaceAll() { + openReplaceDialog(); + replaceAllButton.notifyListeners(SWT.Selection, null); + } + + @Override + public void performReplace() { + openReplaceDialog(); + replaceButton.notifyListeners(SWT.Selection, null); + } + + public boolean isReplaceDialogOpen() { + return dialogAccessor.getBoolean("replaceBarOpen"); + } + + public void openReplaceDialog() { + if (!isReplaceDialogOpen() && Objects.nonNull(openReplaceDialog)) { + openReplaceDialog.notifyListeners(SWT.Selection, null); + replace= (Text) dialogAccessor.get("replaceBar"); + replaceButton= (ToolItem) dialogAccessor.get("replaceButton"); + replaceAllButton= (ToolItem) dialogAccessor.get("replaceAllButton"); + } + } + + public void closeReplaceDialog() { + if (isReplaceDialogOpen() && Objects.nonNull(openReplaceDialog)) { + openReplaceDialog.notifyListeners(SWT.Selection, null); + replace= null; + replaceButton= null; + replaceAllButton= null; + } + } + + @Override + public void performReplaceAndFind() { + performReplace(); + } + + @Override + public void assertInitialConfiguration() { + assertUnselected(SearchOptions.REGEX); + assertUnselected(SearchOptions.WHOLE_WORD); + assertUnselected(SearchOptions.CASE_SENSITIVE); + if (!doesTextViewerHaveMultiLineSelection(findReplaceLogic.getTarget())) { + assertSelected(SearchOptions.GLOBAL); + assertTrue(findReplaceLogic.isActive(SearchOptions.GLOBAL)); + } else { + assertUnselected(SearchOptions.GLOBAL); + assertFalse(findReplaceLogic.isActive(SearchOptions.GLOBAL)); + } + assertEnabled(SearchOptions.GLOBAL); + assertEnabled(SearchOptions.REGEX); + assertEnabled(SearchOptions.CASE_SENSITIVE); + if (getFindText().equals("") || findReplaceLogic.isWholeWordSearchAvailable(getFindText())) { + assertEnabled(SearchOptions.WHOLE_WORD); + } else { + assertDisabled(SearchOptions.WHOLE_WORD); + } + } + + private boolean doesTextViewerHaveMultiLineSelection(IFindReplaceTarget target) { + if (target instanceof IFindReplaceTargetExtension scopeProvider) { + return scopeProvider.getScope() != null; // null is returned for global scope + } + return false; + } + + @Override + public void assertUnselected(SearchOptions option) { + assertFalse(getSelectedOptions().contains(option)); + } + + @Override + public void assertSelected(SearchOptions option) { + assertTrue(getSelectedOptions().contains(option)); + } + + @Override + public void assertDisabled(SearchOptions option) { + assertFalse(getEnabledOptions().contains(option)); + } + + @Override + public void assertEnabled(SearchOptions option) { + assertTrue(getEnabledOptions().contains(option)); + } + + @Override + public Shell getActiveShell() { + return shellRetriever.get(); + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java index f8e114d0b1e..df7a9d9a56e 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java @@ -34,7 +34,6 @@ */ @RunWith(Suite.class) @SuiteClasses({ - FindReplaceDialogTest.class, HippieCompletionTest.class, RangeTest.class, ChangeRegionTest.class, @@ -47,6 +46,8 @@ MinimapWidgetTest.class, TextEditorPluginTest.class, TextViewerDeleteLineTargetTest.class, + FindReplaceDialogTest.class, + FindReplaceOverlayTest.class, FindReplaceLogicTest.class, }) public class WorkbenchTextEditorTestSuite {