From 2a83f92944aaa7f9e433d153a60f469703cabcd5 Mon Sep 17 00:00:00 2001 From: Maximilian Wittmer Date: Tue, 23 Jan 2024 17:21:25 +0100 Subject: [PATCH] Implement a Modern Find/Replace-Overlay This PR implements and tests a find/replace dialog which can be overlayed on top of the editor. The overlay uses the FindReplaceLogic which is also used by the existing find/replace dialog. The overlay can be enabled and disabled in the preferences. https://github.com/eclipse-platform/eclipse.platform.ui/issues/1090 --- .../META-INF/MANIFEST.MF | 2 +- .../TextEditorDefaultsPreferencePage.java | 13 + .../editors/text/TextEditorMessages.java | 2 + .../text/TextEditorMessages.properties | 2 + ...ecoratedTextEditorPreferenceConstants.java | 26 + .../META-INF/MANIFEST.MF | 3 +- .../icons/full/obj16/case_sensitive.png | Bin 0 -> 418 bytes .../icons/full/obj16/case_sensitive@2x.png | Bin 0 -> 870 bytes .../icons/full/obj16/regex.png | Bin 0 -> 315 bytes .../icons/full/obj16/regex@2x.png | Bin 0 -> 524 bytes .../icons/full/obj16/replace-all@2x.png | Bin 0 -> 1138 bytes .../icons/full/obj16/replace.png | Bin 0 -> 476 bytes .../icons/full/obj16/replace@2x.png | Bin 0 -> 924 bytes .../icons/full/obj16/replace_all.png | Bin 0 -> 625 bytes .../icons/full/obj16/search_all.png | Bin 0 -> 720 bytes .../icons/full/obj16/search_all@2x.png | Bin 0 -> 1509 bytes .../icons/full/obj16/search_in_area.png | Bin 0 -> 237 bytes .../icons/full/obj16/search_in_area@2x.png | Bin 0 -> 323 bytes .../icons/full/obj16/search_next@2x.png | Bin 0 -> 767 bytes .../icons/full/obj16/search_prev@2x.png | Bin 0 -> 730 bytes .../icons/full/obj16/select_next.png | Bin 0 -> 366 bytes .../icons/full/obj16/select_prev.png | Bin 0 -> 364 bytes .../icons/full/obj16/whole_word.png | Bin 0 -> 427 bytes .../icons/full/obj16/whole_word@2x.png | Bin 0 -> 827 bytes .../findandreplace/FindReplaceMessages.java | 15 +- .../FindReplaceMessages.properties | 18 +- .../ui/texteditor/FindReplaceAction.java | 83 +- .../ui/texteditor/FindReplaceOverlay.java | 995 ++++++++++++++++++ .../FindReplaceOverlayFirstTimePopup.java | 135 +++ .../texteditor/FindReplaceOverlayImages.java | 156 +++ .../src/org/eclipse/text/tests/Accessor.java | 12 +- .../tests/FindReplaceOverlayTest.java | 165 +++ .../texteditor/tests/OverlayAccess.java | 328 ++++++ .../tests/WorkbenchTextEditorTestSuite.java | 3 +- 34 files changed, 1948 insertions(+), 10 deletions(-) create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/case_sensitive@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/regex@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace-all@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/replace_all.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_all@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_in_area@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_next@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/search_prev@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_next.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/select_prev.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/obj16/whole_word@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlay.java create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayFirstTimePopup.java create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayImages.java create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/FindReplaceOverlayTest.java create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java 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 f974d911961..1b0e61771ba 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 0000000000000000000000000000000000000000..c2ac8a79a85bd575e3f798542a3056de17403421 GIT binary patch literal 418 zcmV;T0bTxyP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10WV2J zK~y-6&5|)o13?soXLird#&jMiSP1tAY=o#qeuHQs5-qN?u-i>x4s*wOrKm*;dmAzK z`3WLolS-__pAhhNpN)}_U=gp-SI%RGZyt{#f-0jbx&K8#FezAT>j3&?SuPhv@jf$w zh#UauX|4BXCP&K>1>{xn4}91eE?^uEwv zjWO#05QZT|q{y;tOGM6;Qs$=$O_FcMm;P3G&Nvk;O=*+Q@f_g!Xq^WMDA z@6Gqk{Kh14Bh9;^)yDv1fH45j7$qo8)5j&91FnQ&c*r^T*Fgb{)B#Czz&3D8Q4~)d zRPm7j78Vw615N^;0solU+{B zwzs#R1zcTlyWO7jeSaQ!2$%$ZH?!}9Ab8U`*V`R`%?@-rog=`snSHdjw$=}V;8Wl- zFxP6e{N3?sn%?32{w3g5ATYC^fO{pme!qXt%C1~}Gkw~vhskY!l}JOgaxdH%!DFX`RtxJQh6p8wYE zc3*SOT^{N3MNvF$X17XO)s9WW%F4=jaU5>|PbNund$-&Dqb|SQ zZXfbI?|z_ZW{*|?ca02SX3tfp%`{El82XbWK~j&a&-fIuI=q%;Sz|C5oRRc`@B7Wl zy=`WjlBm-_EkGQ{_n6u73gq;DyCzA`?*!=g`)7fdC7t&?FO_sL48xy`qIlTMzOK7R zEr92FvywbXFL8DL_n4V|0UYggI!Bh5moHSBc@el6MbQc8Tse$Q)AY{$7O@8)%Q8>W zEHH?o=mY26W?f<&$M2Zg(P~mIY;A2F27EKy*jXGv(#hKD)J#E96f?lxX14B}`*Xj< zD2hG=dcdhH%Wi5k8ovO4OPX0+T$~1IwOW3XB(DP}Mg|y)uPn>j)(@?Et9?TCfpX3b z%q#|O_B`*$BuOqco6Rf0E0Pwj2jIM<`6!CkuLV%qvZQ(7SAaavSIe?|1UM_{TQkcB wgTZuJmS0MmH?z<8)Eoa7i~+^~|9gOc0TA^l-dVQ@N&o-=07*qoM6N<$g7P4ao&W#< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..43b37ca9b54b444be3a3ce07a5e9624fcd1f717d GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt(4!T^vI!dXG-p$a}~@!1aH- zk}dCKmw*inys2Jn4w4O<7kErgW@1a=lxWb?PGRI?>(H=cxXRea2lWk<*@DxXaB@@Lj2E zQR5QV&nzBQ*)t-)X8hMOY$-@LK3}}8=tJH={)(Ry>x!C<8FqHOS*Ix@1N0Mvr>mdK II;Vst0IHUDbN~PV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..53a95b12a98dfd1cbd3200d92964fb01e18ec95d GIT binary patch literal 524 zcmV+n0`vWeP)oEeMU#jqiZdtI64{^dRYnfW+}f38U>ky8({-rfQH4*(JIBI2_F zEUWAQYi%1v!nIrpwC%O8upRaLEf?}xyds!nd#T1~(hV}KeE@BO)mY%c)a_THZZ z0@O|KpR?LNL`6{yVvHYvLGx*1t$iCp_yQ0@cw()659~Je&%kR{{kReUfU2t2rqk(O zN~r=ifSWk56^mXQ`>-thvUjjRg=P7VBAH!Av>1J>HfI8>;s(N~#S5;4)bA3R8{WQ1pm89Ud zWI}vWuHI?&;fJ+D}RcYRrU#V0KWmjKe8<(k(0at O0000*Y8w%D4RnSC>U6g8311`jki|9r-LT8d0 zCV@1QAcDk&h`3RTv2HXb7Od1ou}E!0p(XxQV=1J@CZ%a&Gc%dIdtBsiGMPB(oOG`@)@cI1oD-p>6%+AivIF92d=Gp*M zb(e@#03Ds3ow4k-SS(h(Fb;>qb&lhNrlzKj_w@9n3+-zP8ML;x?r3gqE(XfxRo6sB zqS5G#BvP-cC#$NedW|u~3qFxZE_#GuvKO zU1Q8vpdWZX8jap}QviWL-~#Xq@S>`|JFk8lcuQ5sRCO%dUPQj`=;#)IX=&+R;Add#ygCH@ZmoT$ySw{xj>|d#V@wFZah&l&0G`^gy}f;7I-ULws8`hs z*4l>b;IUZj0oQehfeGL+@ES0ZNF*N70s#sZAuML%Ip7P&aT;^qJe^J_f$v4+1K?3$ zP(+#v1qYuWqT3;!zXScz>c>^(b>9&bb+xGLK;rL<`IbFpU?5Vn%L>PUyLFIoDUJ#%! za(Ar=hn?cU)w+ELSEQ_vq$dC2*W(|CKtBVX^bLTKa#W2tPIB|MdJvh@>%uwY$bxGA zNT$Xw&P_#vUVvBc0tIY}Z>e1JbA8>&{;v%c-07tZ z0M{o|eA0WG%&cX{wi=8PlF1uj%d$8N-a)(q05-X7I(Y6f=^2+jPlR}?{vH;iy2-5Z zEz5vi0WdK+4Zx-<2ki7*K-Kv=Sg4g-dIZY>D%Z~i8T#`Y4}}~+NM|g8lB{lZS3QCu zp8zanKovxI?BQZgoihv$O)`Ax23BG3?)B8wmSy?m&A=IBGY5PEDA@F}X+f5)R%jW&VBWuBf4Fk18s`V#K`)!E*c5t`e~iD{ zbVS`X0RS95SQW|`XSd(rMr`Ix>D9h?T&)-TJANnp4``RHMSz6M^8f$<07*qoM6N<$ Eg7Qum>i_@% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..59d140ab7d06b8c522d9bae09d486f32ac17946c GIT binary patch literal 476 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 LNkvXXu0mjftzyIr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e820c06d5cbde66aa6da7d8583062093e3557c2e GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt)xaE{-7sWZRk|G zd&QuE_1KD4t9BheaA1MYl6~&Exw)S-*bEE|PVBAyJuUaynVI`JU#?rVs)_xM0v9tI z5F{o(e0$=Y@=XRHJH0FBz<~oF=6TdMMetVq{MC@5Lt z8c`CQpH@mmtT}V`<;yxP|*cX7srr@*0)nPavd@dXxlHW z*xbFZX|w_xh{@yL${b+nj&D@W|X^QH8U0 zCHyK4EFVg_Pi?YEiTuW~;_9siKZXR`V=Mdqv2rdr%`C34gmaobqeODpOR+z!4&g#^ zbBh&yZ&-8sH!weAJjxhwb+g%{P`jgRx0VS;$sDMzf6Z!;=4zhE)WP?Gr6$&{m*0De QGSF`fp00i_>zopr07j>M?*IS* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d46b3a06265f0b1bb443704b4a2db08021556b4c GIT binary patch literal 767 zcmV9~%}Z5xZc4i66#=3)je_JO!0UOB;-$#1IqWBe+08+EPFsEwsTH z<3bW+Bv?byK~akV1GL3rXy@-e=p-2EEv1*8H~A$^d*+;f?{ubZUf%ze#R@ph=K>D% zOweIUFxbl+3ObD!j%@h;L-z_eP3Of2Xfw|Q+DzE^_6)%4zM{)7Ks73ARF9TAlC! z6U{~tHSv~9s}mkzyvYcnZocNy>cj`QY5-B=ueh{2;Q_|37(mq6OD?TWc!1H%1`u`Q zIhR%^JiyaN1BkjFIGhX+Wl4hxa^+zYL5#sEWga%iYt#-!skqTz0&Ry;ZRx!8m0juKE$CY&*Vw?S9n zt&mP}Q8}PLifohnQ!3sgccZYCU zrLfCNr{g3xW?PW_)VU)ba=>Rh1$xa@sgDfgy6tJNj&gneK&7NG7;9oX@um^+xmNfc z7SMA{#9ej#?2-OEqMdkBAt?+zH70hx4WP%&TEFORPq%v6PV|>c3WL8QkIN2uspS%) x@1msil}SsFN{{HAXIcZL#$!EJoi3LizX6#w#jVbO;A{W@002ovPDHLkV1nNSWt;#2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8e26ea4b4d691c3e2e48621b1f03dd2783fcd815 GIT binary patch literal 730 zcmV<00ww*4P)djUnfAcz#iaEst2apS@bkt!e;DW$-)AWBS$ zHW6bNO&mgN3Isu*1x;soJ%>1nCh$?<12<0cY~Y=9-uWgi!D89R6?7{{f^MtT@-MES z`>0c`%ZjLVE-hG}opZ>`mF$r_t}wwSE>W%I9@_HZnyks;S{ME_RO^cQ+y{IT{Jw{3 zlqrHh<1wtxhOoBy32z>q1OJR@2Udcge8{at2u7Pv(DDgvq!XJ`3-2-BS^)kXk$)0i zTJCo%VgK*t%QGX`_!;|4YCeSV2WP<Xdwo@o-Jb+6AGqZV6O7d7VKLePBl&SxQVA8o=6vvsfl7y#@kzX*Z#ZwI zGUcsw(q}sd-@kOhSpBdk$+!=JrhM=$FPA9KphSO#GSy!}%kSo4S)=7Ep?yik2cR~d z0?#Cooll^`0k#xvgR%Tpid4K8=G>hN+@#<()P|EVmd1oQBbu>efxdEwMz9@YQ56#1 z%d_|VozQ#30j7WR3z<3Te+zo9JHYe>uaKFOUZDG$9Za9^5i)b~3tXkaM4#&vGIP=k zyriK_pY;ftIq3zu%IsizqD{!mNiWcO#SW%_eJW(;q!;KYwSnpJCqiaUeu2w0nCP*` zLS{~Sfftu-VCk8MLS{~Sf#)T*b%OMCix6i>T6zIbv3=50Y{UG!Qlzz}EiHt~z!?&e zU!cTR*j{W!CY}}9&L7DC2A+$yLQj!3++Ji&6Ko+$1uj_rU%R+|0qzLG7+wpncmMzZ M07*qoM6N<$f@wxpYybcN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9fcc646d92bc2e4c9c314f23cb9c400105480ca1 GIT binary patch literal 366 zcmV-!0g?WRP){{8*le~=i_2JC7S`~UyX z6BG;*BiewS^`ig(|GbTYL1IK3u)R+7|G)2-P%uc0Xaly@i2VQeABjbPXccNf)c?<(KmJ9!XezNKR-!~ia;RikJP1VxhH&%!zVfDtkLLrZh<>KF;9Vz|)_xS=8{OU{{(CLyu z&0^7b!(wfru*;KSDn!GHiUp7%YfHqwKRuZ9|K+j5 z|LaP`f2=DOjwdNYt;!SfSeYmIAB0yIh(wc>E0<*n+N{hKcEP6x07>yT%9Jy(>i_@% M07*qoM6N<$g3~Lpr2qf` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2e6a039c5d7db063b2d49f61f7ec3f82f6297b0 GIT binary patch literal 364 zcmV-y0h9iTP)G%mB{~x zM_T{>{dDR-KK$~+^#35W7zS*v6#kEl|Gqzng3;yihQ-Em;s1Z$?nJ>LF`}b!eW}p@ zKd&~RV2~Km2CORq8t`HT3I>T0ZNQo$paDpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10XRuS zK~y-6#gj2hLqQOPXU-%NZ3Gdt)KWwnvA0sR@_*Q*2xMOfx#|*#LxSyCZ;7skG*tyCJzM>5=5bU`DbtTyXB4 zWNrlDoZAA>3LzYjeEF7a;}b+lI{@xS9+BjA9LECX7X8Q7t)oQg1po=kTHk)tbe41IRYvR9O;|G{~ VlIvG1W{m&<002ovPDHLkV1iP7vT^_b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7b66f352083a42519c0054e93db5ab25e0e09d5a GIT binary patch literal 827 zcmV-B1H}A^P)(Mieh&?3mSvB<_xFL35KpiI-uu-_l3Xjx@`j{uByE&5 zV`is&2?id|6MF*0p+K+#X7+=m?UKd^1_pl2vg{v#dcD3bilW0IjHH2i1PlWY7cD^2 zZs2yF=STmQT&aB6>$NqrO(B1prXK-Az|~N^*AT|Q@>;F-vTTxMX7wKV0GpE}+284C zG#bv#^10h!`+|$edw&l2Qqo1>IPjjC)qp|ZGvItt6kourMPz0(l7@kwBwZ`Za=D}$ zunV{}IXSt<0%XA!wA<~mIF47E*=`73s4&{084HozqO-)VR z1MV$`_=ROz)&g!WHoxzm-|&Ag;5qQ={Ft?4Thssm002ovPDHLk FV1nK5gysMM literal 0 HcmV?d00001 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..2b848bdf8f0 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,32 @@ 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(); + } + } + + /** + * @since 3.17 + */ + private void showDialog() { final FindReplaceDialog dialog; final boolean isEditable; @@ -352,6 +413,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..2471b514a68 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlay.java @@ -0,0 +1,995 @@ +/******************************************************************************* + * 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.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionEvent; +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 = new KeyListener() { + + 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(); + } + + @Override + public void keyPressed(KeyEvent 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 toggleToolItem(ToolItem toolItem) { + toolItem.setSelection(!toolItem.getSelection()); + toolItem.notifyListeners(SWT.Selection, null); + } + + @Override + public void keyReleased(KeyEvent e) { + // Do nothing + } + }; + + private ControlListener shellMovementListener = new ControlListener() { + @Override + public void controlMoved(ControlEvent e) { + positionToPart(); + } + + @Override + public void controlResized(ControlEvent e) { + positionToPart(); + } + }; + private FocusListener overlayFocusListener = new FocusListener() { + + @Override + public void focusGained(FocusEvent e) { + // do nothing + } + + @Override + public void focusLost(FocusEvent e) { + findReplaceLogic.activate(SearchOptions.GLOBAL); + searchInSelectionButton.setSelection(false); + } + + }; + private PaintListener widgetMovementListener = new PaintListener() { + + @Override + public void paintControl(PaintEvent e) { + 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(new SelectionListener() { + @Override + public void widgetSelected(SelectionEvent e) { + performSearch(false); + evaluateFindReplaceStatus(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do Nothing + } + }); + 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(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + performSearch(true); + evaluateFindReplaceStatus(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do nothing + } + }); + searchAllButton = new ToolItem(searchTools, SWT.PUSH); + searchAllButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_SEARCH_ALL)); + searchAllButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_searchAllButton_toolTip); + searchAllButton.addSelectionListener(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + performSelectAll(); + evaluateFindReplaceStatus(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do nothing + } + + }); + } + + 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(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + activateInFindReplacerIf(SearchOptions.GLOBAL, !searchInSelectionButton.getSelection()); + updateIncrementalSearch(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do Nothing + } + }); + } + + 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(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + activateInFindReplacerIf(SearchOptions.REGEX, ((ToolItem) e.widget).getSelection()); + wholeWordSearchButton.setEnabled(!findReplaceLogic.isActive(SearchOptions.REGEX)); + updateIncrementalSearch(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do nothing + } + }); + } + + 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(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + activateInFindReplacerIf(SearchOptions.CASE_SENSITIVE, caseSensitiveSearchButton.getSelection()); + updateIncrementalSearch(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do Nothing + } + }); + } + + 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(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + activateInFindReplacerIf(SearchOptions.WHOLE_WORD, wholeWordSearchButton.getSelection()); + updateIncrementalSearch(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do Nothing + } + }); + } + + 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(new SelectionListener() { + @Override + public void widgetSelected(SelectionEvent e) { + if (getFindString().isEmpty()) { + showUserFeedback(warningColor, true); + return; + } + performSingleReplace(); + evaluateFindReplaceStatus(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do nothing + } + }); + replaceAllButton = new ToolItem(replaceTools, SWT.PUSH); + replaceAllButton.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.OBJ_REPLACE_ALL)); + replaceAllButton.setToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceAllButton_toolTip); + replaceAllButton.addSelectionListener(new SelectionListener() { + @Override + public void widgetSelected(SelectionEvent e) { + if (getFindString().isEmpty()) { + showUserFeedback(warningColor, true); + return; + } + performReplaceAll(); + evaluateFindReplaceStatus(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do Nothing + } + }); + } + + 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(new ModifyListener() { + @Override + public void modifyText(ModifyEvent 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(new FocusListener() { + + @Override + public void focusGained(FocusEvent e) { + // do nothing + } + + @Override + public void focusLost(FocusEvent e) { + searchBar.setForeground(normalTextForegroundColor); + replaceBar.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(new SelectionListener() { + @Override + public void widgetSelected(SelectionEvent e) { + toggleReplace(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // Do nothing + } + }); + } + + 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..d84267d33a9 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/FindReplaceOverlayFirstTimePopup.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * 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 java.util.function.Function; + +import org.osgi.framework.FrameworkUtil; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +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) != true) { + settings.put(SETTING_POPUP_WAS_SHOWN_BEFORE, true); + + Display displayToUse = Objects.nonNull(shellToUse) ? shellToUse.getDisplay() : Display.getDefault(); + + NotificationPopup.forDisplay(displayToUse).content(constructContentCreator()) + .title(FindReplaceMessages.FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_title, + true) + .delay(POPUP_VANISH_TIME.toMillis()).open(); + } + + } + + private static Function constructContentCreator() { + return new Function<>() { + @Override + public Control apply(Composite t) { + return createFirstTimeNotification(t); + } + }; + } + + 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(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + disableUseOverlayPreference(); + composite.getShell().close(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + }); + + 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 {