From b2fad1044911f6b9b2ff98292f7976ec2baeb2f5 Mon Sep 17 00:00:00 2001 From: "Knoll, Vladislav" Date: Thu, 25 Jun 2026 10:15:44 +0200 Subject: [PATCH] FindReplaceOverlay: share search settings per editor stack Editors in the same tab group now share a common find/replace state. Switching between tabs in a stack keeps the overlay in sync: it auto-opens on the newly activated editor and auto-closes when another stack-mate closes it. Independent stacks (e.g. side-by-side editors) maintain their own separate settings. Fixes: https://github.com/eclipse-platform/eclipse.platform.ui/issues/2055 --- .../META-INF/MANIFEST.MF | 1 + .../overlay/FindReplaceOverlay.java | 330 ++++++++++++++++-- .../overlay/OverlaySearchSettings.java | 102 ++++++ .../ui/texteditor/FindReplaceAction.java | 14 +- .../overlay/FindReplaceOverlayTest.java | 90 +++++ 5 files changed, 510 insertions(+), 27 deletions(-) create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/OverlaySearchSettings.java 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 924a82d0fd8..74d07397665 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF @@ -28,6 +28,7 @@ Export-Package: org.eclipse.ui.texteditor.templates Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)", + org.eclipse.e4.ui.model.workbench;bundle-version="2.4.900", org.eclipse.compare.core;bundle-version="[3.5.0,4.0.0)", org.eclipse.core.expressions;bundle-version="[3.4.100,4.0.0)", org.eclipse.jface.text;bundle-version="[3.19.0,4.0.0)", diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java index ac389fd96a6..aa0e373a6d3 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java @@ -17,10 +17,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicReference; import org.osgi.framework.FrameworkUtil; +import org.eclipse.e4.ui.model.application.ui.MUIElement; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.model.application.ui.basic.MPartStack; + import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.custom.StyledText; @@ -64,7 +69,11 @@ import org.eclipse.jface.text.ITextViewer; import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IPartService; import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter; import org.eclipse.ui.internal.SearchDecoration; @@ -113,9 +122,16 @@ private static final class KeyboardShortcuts { private static final String IDEAL_WIDTH_TEXT = "THIS TEXT HAS A REASONABLE LENGTH FOR SEARCHING"; //$NON-NLS-1$ private static final int HISTORY_SIZE = 15; + private static final WeakHashMap STACK_SETTINGS = new WeakHashMap<>(); + + private static final WeakHashMap PART_TO_OVERLAY = new WeakHashMap<>(); + private FindReplaceLogic findReplaceLogic; private final IWorkbenchPart targetPart; private boolean replaceBarOpen; + private boolean isRestoringSettings = false; + private boolean partListenerRegistered = false; + private IPartListener2 stackMemberActivationListener; private final Composite targetControl; private Composite containerControl; @@ -299,10 +315,38 @@ public FindReplaceOverlay(Shell parent, IWorkbenchPart part, IFindReplaceTarget createFindReplaceLogic(target); createContainerAndSearchControls(targetControl); containerControl.setVisible(false); + if (targetPart != null) { + PART_TO_OVERLAY.put(targetPart, this); + containerControl.addDisposeListener(__ -> PART_TO_OVERLAY.remove(targetPart)); + } PlatformUI.getWorkbench().getHelpSystem().setHelp(containerControl, IAbstractTextEditorHelpContextIds.FIND_REPLACE_OVERLAY); } + /** + * Returns the existing {@link FindReplaceOverlay} for the given + * {@code workbenchPart} if one already exists, otherwise creates and returns a + * new one. This ensures that an overlay auto-opened during a tab switch can + * later be reclaimed by the part's {@link FindReplaceAction} without creating a + * duplicate. + * + * @param shell the parent shell (used only when creating a new overlay) + * @param workbenchPart the part for which the overlay is needed + * @param target the find/replace target (used only when creating a new + * overlay) + * @return an overlay for the given part + */ + public static FindReplaceOverlay getOrCreateForPart(Shell shell, IWorkbenchPart workbenchPart, + IFindReplaceTarget target) { + if (workbenchPart != null) { + FindReplaceOverlay existing = PART_TO_OVERLAY.get(workbenchPart); + if (existing != null && !existing.containerControl.isDisposed()) { + return existing; + } + } + return new FindReplaceOverlay(shell, workbenchPart, target); + } + private static Composite getTargetControl(Shell targetShell, IWorkbenchPart targetPart) { if (targetPart instanceof StatusTextEditor textEditor) { return textEditor.getAdapter(ITextViewer.class).getTextWidget(); @@ -315,6 +359,143 @@ private boolean insertedInTargetParent() { return targetControl instanceof StyledText; } + private MPartStack getEditorStack() { + if (targetPart == null) { + return null; + } + IWorkbenchPartSite site = targetPart.getSite(); + if (site == null) { + return null; + } + MPart mPart = site.getService(MPart.class); + if (mPart == null) { + return null; + } + MUIElement presentationElement = mPart.getCurSharedRef() != null ? mPart.getCurSharedRef() : mPart; + MUIElement parent = presentationElement.getParent(); + while (parent != null && !(parent instanceof MPartStack)) { + parent = parent.getParent(); + } + return (MPartStack) parent; + } + + private void saveToStackSettings() { + if (isRestoringSettings) { + return; + } + MPartStack stack = getEditorStack(); + if (stack == null) { + return; + } + OverlaySearchSettings settings = STACK_SETTINGS.computeIfAbsent(stack, __ -> new OverlaySearchSettings()); + settings.setFindString(getFindString()); + if (okayToUse(replaceBar)) { + settings.setReplaceString(replaceBar.getText()); + } + settings.setCaseSensitive(findReplaceLogic.isActive(SearchOptions.CASE_SENSITIVE)); + settings.setWholeWord(findReplaceLogic.isActive(SearchOptions.WHOLE_WORD)); + settings.setRegex(findReplaceLogic.isActive(SearchOptions.REGEX)); + settings.setSearchInSelection(!findReplaceLogic.isActive(SearchOptions.GLOBAL)); + settings.setReplaceBarOpen(replaceBarOpen); + } + + private void applyStackSettings(OverlaySearchSettings settings) { + isRestoringSettings = true; + try { + activateInFindReplacerIf(SearchOptions.CASE_SENSITIVE, settings.isCaseSensitive()); + caseSensitiveSearchButton.setSelection(settings.isCaseSensitive()); + + activateInFindReplacerIf(SearchOptions.REGEX, settings.isRegex()); + regexSearchButton.setSelection(settings.isRegex()); + + boolean wholeWordAvailable = findReplaceLogic.isAvailable(SearchOptions.WHOLE_WORD); + activateInFindReplacerIf(SearchOptions.WHOLE_WORD, settings.isWholeWord() && wholeWordAvailable); + wholeWordSearchButton.setSelection(findReplaceLogic.isAvailableAndActive(SearchOptions.WHOLE_WORD)); + wholeWordSearchButton.setEnabled(wholeWordAvailable); + + activateInFindReplacerIf(SearchOptions.GLOBAL, !settings.isSearchInSelection()); + searchInSelectionButton.setSelection(settings.isSearchInSelection()); + + searchBar.setText(settings.getFindString()); + + setReplaceVisible(settings.isReplaceBarOpen()); + if (replaceBarOpen && okayToUse(replaceBar)) { + replaceBar.setText(settings.getReplaceString()); + findReplaceLogic.setReplaceString(settings.getReplaceString()); + } + + updateContentAssistAvailability(); + decorate(); + } finally { + isRestoringSettings = false; + } + } + + private void setOverlayOpenInStack(boolean open) { + MPartStack stack = getEditorStack(); + if (stack == null) { + return; + } + if (open) { + STACK_SETTINGS.computeIfAbsent(stack, __ -> new OverlaySearchSettings()).setOverlayOpen(true); + } else { + OverlaySearchSettings settings = STACK_SETTINGS.get(stack); + if (settings != null) { + settings.setOverlayOpen(false); + } + } + } + + private void registerStackMemberActivationListener() { + if (partListenerRegistered || targetPart == null) { + return; + } + IWorkbenchPartSite site = targetPart.getSite(); + if (site == null) { + return; + } + IPartService partService = site.getWorkbenchWindow().getPartService(); + partService.addPartListener(new StackPartActivationListener()); + partListenerRegistered = true; + containerControl.addDisposeListener(__ -> { + partService.removePartListener(stackMemberActivationListener); + stackMemberActivationListener = null; + partListenerRegistered = false; + }); + } + + private boolean isInSameStack(IWorkbenchPart part, MPartStack myStack) { + if (part == null || myStack == null) { + return false; + } + IWorkbenchPartSite site = part.getSite(); + if (site == null) { + return false; + } + MPart mPart = site.getService(MPart.class); + if (mPart == null) { + return false; + } + MUIElement presentationElement = mPart.getCurSharedRef() != null ? mPart.getCurSharedRef() : mPart; + MUIElement parent = presentationElement.getParent(); + while (parent != null && !(parent instanceof MPartStack)) { + parent = parent.getParent(); + } + return myStack.equals(parent); + } + + private void triggerOverlayForPart(IWorkbenchPart part) { + IFindReplaceTarget target = part.getAdapter(IFindReplaceTarget.class); + if (target == null || !(part instanceof StatusTextEditor)) { + return; + } + Shell shell = part.getSite().getShell(); + FindReplaceOverlay otherOverlay = getOrCreateForPart(shell, part, target); + if (!otherOverlay.containerControl.isDisposed()) { + otherOverlay.openForStack(); + } + } + private void createFindReplaceLogic(IFindReplaceTarget target) { findReplaceLogic = new FindReplaceLogic(); boolean isTargetEditable = false; @@ -333,7 +514,8 @@ public Composite getContainerControl() { } private void performReplaceAll() { - BusyIndicator.showWhile(containerControl.getShell() != null ? containerControl.getShell().getDisplay() : Display.getCurrent(), + BusyIndicator.showWhile( + containerControl.getShell() != null ? containerControl.getShell().getDisplay() : Display.getCurrent(), findReplaceLogic::performReplaceAll); evaluateStatusAfterReplace(); replaceBar.storeHistory(); @@ -341,7 +523,8 @@ private void performReplaceAll() { } private void performSelectAll() { - BusyIndicator.showWhile(containerControl.getShell() != null ? containerControl.getShell().getDisplay() : Display.getCurrent(), + BusyIndicator.showWhile( + containerControl.getShell() != null ? containerControl.getShell().getDisplay() : Display.getCurrent(), findReplaceLogic::performSelectAll); searchBar.storeHistory(); } @@ -359,9 +542,9 @@ private void asyncExecIfOpen(Runnable operation) { } } - private final FocusListener targetFocusListener = FocusListener.focusGainedAdapter(__ -> { - removeSearchScope(); - searchBar.storeHistory(); + private final FocusListener targetFocusListener = FocusListener.focusGainedAdapter(__ -> { + removeSearchScope(); + searchBar.storeHistory(); }); private final KeyListener closeOnTargetEscapeListener = KeyListener.keyPressedAdapter(c -> { @@ -390,6 +573,7 @@ public void close() { if (containerControl.isDisposed() || !containerControl.isVisible()) { return; } + setOverlayOpenInStack(false); if (targetPart != null) { targetPart.setFocus(); } @@ -400,11 +584,24 @@ public void close() { containerControl.setVisible(false); } + private void silentClose() { + if (containerControl.isDisposed() || !containerControl.getVisible()) { + return; + } + setOverlayOpenInStack(false); + storeOverlaySettings(); + findReplaceLogic.activate(SearchOptions.GLOBAL); + unbindListeners(); + containerControl.setVisible(false); + } + public void open() { if (!containerControl.isVisible()) { containerControl.setVisible(true); bindListeners(); restoreOverlaySettings(); + setOverlayOpenInStack(true); + registerStackMemberActivationListener(); } assignIDs(); containerControl.layout(); @@ -416,13 +613,36 @@ public void open() { updateFromTargetSelection(); } + private void openForStack() { + if (!containerControl.isVisible()) { + containerControl.setVisible(true); + bindListeners(); + restoreOverlaySettings(); + setOverlayOpenInStack(true); + registerStackMemberActivationListener(); + } + assignIDs(); + containerControl.layout(); + containerControl.moveAbove(null); + updatePlacementAndVisibility(); + updateContentAssistAvailability(); + + searchBar.setFocus(); + } + private void storeOverlaySettings() { getDialogSettings().put(REPLACE_BAR_OPEN_DIALOG_SETTING, replaceBarOpen); + saveToStackSettings(); } private void restoreOverlaySettings() { - Boolean shouldOpenReplaceBar = getDialogSettings().getBoolean(REPLACE_BAR_OPEN_DIALOG_SETTING); - setReplaceVisible(shouldOpenReplaceBar); + OverlaySearchSettings stackSettings = STACK_SETTINGS.get(getEditorStack()); + if (stackSettings != null) { + applyStackSettings(stackSettings); + } else { + boolean shouldOpenReplaceBar = getDialogSettings().getBoolean(REPLACE_BAR_OPEN_DIALOG_SETTING); + setReplaceVisible(shouldOpenReplaceBar); + } } @SuppressWarnings("nls") @@ -576,14 +796,12 @@ private void createSearchTools() { searchBackwardButton = new AccessibleToolItemBuilder(searchTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_FIND_PREV)) .withToolTipText(FindReplaceMessages.FindReplaceOverlay_upSearchButton_toolTip) - .withOperation(() -> performSearch(false)) - .withShortcuts(KeyboardShortcuts.SEARCH_BACKWARD).build(); + .withOperation(() -> performSearch(false)).withShortcuts(KeyboardShortcuts.SEARCH_BACKWARD).build(); searchForwardButton = new AccessibleToolItemBuilder(searchTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_FIND_NEXT)) .withToolTipText(FindReplaceMessages.FindReplaceOverlay_downSearchButton_toolTip) - .withOperation(() -> performSearch(true)) - .withShortcuts(KeyboardShortcuts.SEARCH_FORWARD).build(); + .withOperation(() -> performSearch(true)).withShortcuts(KeyboardShortcuts.SEARCH_FORWARD).build(); searchForwardButton.setSelection(true); // by default, search down selectAllButton = new AccessibleToolItemBuilder(searchTools).withStyleBits(SWT.PUSH) @@ -600,8 +818,7 @@ private void createCloseTools() { new AccessibleToolItemBuilder(closeTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_CLOSE)) .withToolTipText(FindReplaceMessages.FindReplaceOverlay_closeButton_toolTip) // - .withOperation(this::close) - .withShortcuts(KeyboardShortcuts.CLOSE).build(); + .withOperation(this::close).withShortcuts(KeyboardShortcuts.CLOSE).build(); } private void createAreaSearchButton() { @@ -611,21 +828,21 @@ private void createAreaSearchButton() { .withOperation(() -> { activateInFindReplacerIf(SearchOptions.GLOBAL, !searchInSelectionButton.getSelection()); updateIncrementalSearch(); - }) - .withShortcuts(KeyboardShortcuts.OPTION_SEARCH_IN_SELECTION).build(); + saveToStackSettings(); + }).withShortcuts(KeyboardShortcuts.OPTION_SEARCH_IN_SELECTION).build(); searchInSelectionButton.setSelection(findReplaceLogic.isActive(SearchOptions.WHOLE_WORD)); } private void createRegexSearchButton() { regexSearchButton = new AccessibleToolItemBuilder(searchTools).withStyleBits(SWT.CHECK) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_FIND_REGEX)) - .withToolTipText(FindReplaceMessages.FindReplaceOverlay_regexSearchButton_toolTip) - .withOperation(() -> { + .withToolTipText(FindReplaceMessages.FindReplaceOverlay_regexSearchButton_toolTip).withOperation(() -> { activateInFindReplacerIf(SearchOptions.REGEX, regexSearchButton.getSelection()); wholeWordSearchButton.setEnabled(findReplaceLogic.isAvailable(SearchOptions.WHOLE_WORD)); updateIncrementalSearch(); updateContentAssistAvailability(); decorate(); + saveToStackSettings(); }).withShortcuts(KeyboardShortcuts.OPTION_REGEX).build(); regexSearchButton.setSelection(findReplaceLogic.isActive(SearchOptions.REGEX)); } @@ -637,6 +854,7 @@ private void createCaseSensitiveButton() { .withOperation(() -> { activateInFindReplacerIf(SearchOptions.CASE_SENSITIVE, caseSensitiveSearchButton.getSelection()); updateIncrementalSearch(); + saveToStackSettings(); }).withShortcuts(KeyboardShortcuts.OPTION_CASE_SENSITIVE).build(); caseSensitiveSearchButton.setSelection(findReplaceLogic.isActive(SearchOptions.CASE_SENSITIVE)); } @@ -644,10 +862,10 @@ private void createCaseSensitiveButton() { private void createWholeWordsButton() { wholeWordSearchButton = new AccessibleToolItemBuilder(searchTools).withStyleBits(SWT.CHECK) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_WHOLE_WORD)) - .withToolTipText(FindReplaceMessages.FindReplaceOverlay_wholeWordsButton_toolTip) - .withOperation(() -> { + .withToolTipText(FindReplaceMessages.FindReplaceOverlay_wholeWordsButton_toolTip).withOperation(() -> { activateInFindReplacerIf(SearchOptions.WHOLE_WORD, wholeWordSearchButton.getSelection()); updateIncrementalSearch(); + saveToStackSettings(); }).withShortcuts(KeyboardShortcuts.OPTION_WHOLE_WORD).build(); wholeWordSearchButton.setSelection(findReplaceLogic.isActive(SearchOptions.WHOLE_WORD)); } @@ -660,8 +878,7 @@ private void createReplaceTools() { GridDataFactory.fillDefaults().grab(false, true).align(GridData.CENTER, GridData.END).applyTo(replaceTools); replaceButton = new AccessibleToolItemBuilder(replaceTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_REPLACE)) - .withToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceButton_toolTip) - .withOperation(() -> { + .withToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceButton_toolTip).withOperation(() -> { if (getFindString().isEmpty()) { applyErrorColor(replaceBar); return; @@ -671,8 +888,7 @@ private void createReplaceTools() { replaceAllButton = new AccessibleToolItemBuilder(replaceTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_REPLACE_ALL)) - .withToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceAllButton_toolTip) - .withOperation(() -> { + .withToolTipText(FindReplaceMessages.FindReplaceOverlay_replaceAllButton_toolTip).withOperation(() -> { if (getFindString().isEmpty()) { applyErrorColor(replaceBar); return; @@ -704,12 +920,14 @@ private void createSearchBar() { wholeWordSearchButton.setEnabled(findReplaceLogic.isAvailable(SearchOptions.WHOLE_WORD)); updateIncrementalSearch(); decorate(); + saveToStackSettings(); }); searchBar.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { findReplaceLogic.resetIncrementalBaseLocation(); } + @Override public void focusLost(FocusEvent e) { resetErrorColoring(); @@ -738,6 +956,7 @@ private void createReplaceBar() { replaceBar.addModifyListener(e -> { findReplaceLogic.setReplaceString(replaceBar.getText()); resetErrorColoring(); + saveToStackSettings(); }); replaceBar.addFocusListener(targetActionActivationHandling); replaceBar.addFocusListener(FocusListener.focusLostAdapter(e -> resetErrorColoring())); @@ -774,6 +993,7 @@ private void setReplaceVisible(boolean visible) { replaceToggle.setImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_OPEN_REPLACE_AREA)); } updateContentAssistAvailability(); + saveToStackSettings(); } private void hideReplace() { @@ -1074,4 +1294,68 @@ private void decorate() { } } + private class StackPartActivationListener implements IPartListener2 { + @Override + public void partActivated(IWorkbenchPartReference partRef) { + IWorkbenchPart activatedPart = partRef.getPart(false); + if (containerControl.isDisposed()) { + return; + } + MPartStack myStack = getEditorStack(); + if (myStack == null) { + return; + } + OverlaySearchSettings settings = STACK_SETTINGS.get(myStack); + + if (activatedPart == targetPart) { + containerControl.getDisplay().asyncExec(() -> { + if (containerControl.isDisposed()) { + return; + } + OverlaySearchSettings currentSettings = STACK_SETTINGS.get(getEditorStack()); + if (currentSettings != null && currentSettings.isOverlayOpen() && containerControl.isVisible()) { + applyStackSettings(currentSettings); + } else if ((currentSettings == null || !currentSettings.isOverlayOpen()) + && containerControl.getVisible()) { + silentClose(); + } + }); + } else if (settings != null && settings.isOverlayOpen() && isInSameStack(activatedPart, myStack)) { + containerControl.getDisplay().asyncExec(() -> { + if (!containerControl.isDisposed()) { + triggerOverlayForPart(activatedPart); + } + }); + } + } + + @Override + public void partDeactivated(IWorkbenchPartReference partRef) { + /* unused */ } + + @Override + public void partOpened(IWorkbenchPartReference partRef) { + /* unused */ } + + @Override + public void partClosed(IWorkbenchPartReference partRef) { + /* unused */ } + + @Override + public void partHidden(IWorkbenchPartReference partRef) { + /* unused */ } + + @Override + public void partVisible(IWorkbenchPartReference partRef) { + /* unused */ } + + @Override + public void partBroughtToTop(IWorkbenchPartReference partRef) { + /* unused */ } + + @Override + public void partInputChanged(IWorkbenchPartReference partRef) { + /* unused */ } + } + } \ No newline at end of file diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/OverlaySearchSettings.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/OverlaySearchSettings.java new file mode 100644 index 00000000000..e19ce350acb --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/OverlaySearchSettings.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2026 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 + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace.overlay; + +/** + * Holds the search/replace settings that are shared by all + * {@link FindReplaceOverlay} instances within the same editor stack. Sharing + * within a stack means that navigating between editors in a tab group will + * always show the same search term and options, while independent editor stacks + * (e.g. side-by-side layouts) can carry different, unrelated search sessions. + */ +public class OverlaySearchSettings { + + private String findString = ""; //$NON-NLS-1$ + + private String replaceString = ""; //$NON-NLS-1$ + + private boolean caseSensitive = false; + + private boolean wholeWord = false; + + private boolean regex = false; + + private boolean searchInSelection = false; + + private boolean replaceBarOpen = false; + + private boolean overlayOpen = false; + + public String getFindString() { + return findString; + } + + public void setFindString(String findString) { + this.findString = findString; + } + + public String getReplaceString() { + return replaceString; + } + + public void setReplaceString(String replaceString) { + this.replaceString = replaceString; + } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public boolean isWholeWord() { + return wholeWord; + } + + public void setWholeWord(boolean wholeWord) { + this.wholeWord = wholeWord; + } + + public boolean isRegex() { + return regex; + } + + public void setRegex(boolean regex) { + this.regex = regex; + } + + public boolean isSearchInSelection() { + return searchInSelection; + } + + public void setSearchInSelection(boolean searchInSelection) { + this.searchInSelection = searchInSelection; + } + + public boolean isReplaceBarOpen() { + return replaceBarOpen; + } + + public void setReplaceBarOpen(boolean replaceBarOpen) { + this.replaceBarOpen = replaceBarOpen; + } + + public boolean isOverlayOpen() { + return overlayOpen; + } + + public void setOverlayOpen(boolean overlayOpen) { + this.overlayOpen = overlayOpen; + } + +} 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 4c7a36c65d7..abe22019c63 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 @@ -417,7 +417,8 @@ private void showDialog() { } private void showOverlayInEditor() { - if (overlay == null) { + boolean isNewOverlay = (overlay == null); + if (isNewOverlay) { Shell shellToUse = null; if (fShell == null) { @@ -425,14 +426,19 @@ private void showOverlayInEditor() { } else { shellToUse = fShell; } - overlay = new FindReplaceOverlay(shellToUse, fWorkbenchPart, fTarget); + overlay = FindReplaceOverlay.getOrCreateForPart(shellToUse, fWorkbenchPart, fTarget); } overlay.open(); overlay.setPositionToTop(shouldPositionOverlayOnTop()); - hookDialogPreferenceListener(); - overlay.getContainerControl().addDisposeListener(__ -> removeDialogPreferenceListener()); + if (isNewOverlay) { + hookDialogPreferenceListener(); + overlay.getContainerControl().addDisposeListener(__ -> { + removeDialogPreferenceListener(); + overlay = null; + }); + } } @Override diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java index e74eb92d3b4..ee0f129c9e3 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java @@ -19,6 +19,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import java.lang.reflect.Field; +import java.util.WeakHashMap; + import org.junit.jupiter.api.Test; import org.eclipse.swt.SWT; @@ -215,4 +218,91 @@ public void testSearchTermStoredInHistoryAfterSearchBackward() { assertEquals("foo", dialog.getFindText()); } + @Test + public void testStackSettingsAreAppliedCorrectly() { + initializeTextViewerWithFindReplaceUI("foo bar"); + OverlayAccess dialog= getDialog(); + + OverlaySearchSettings settings= new OverlaySearchSettings(); + settings.setFindString("foo"); + settings.setCaseSensitive(true); + settings.setRegex(false); + settings.setWholeWord(true); + settings.setSearchInSelection(false); + settings.setReplaceBarOpen(false); + + Accessor actionAccessor= new Accessor(getFindReplaceAction(), FindReplaceAction.class); + FindReplaceOverlay overlay= (FindReplaceOverlay) actionAccessor.get("overlay"); + new Accessor(overlay, FindReplaceOverlay.class).invoke("applyStackSettings", + new Class[] { OverlaySearchSettings.class }, new Object[] { settings }); + + assertEquals("foo", dialog.getFindText()); + dialog.assertSelected(SearchOptions.CASE_SENSITIVE); + dialog.assertUnselected(SearchOptions.REGEX); + dialog.assertSelected(SearchOptions.WHOLE_WORD); + dialog.assertSelected(SearchOptions.GLOBAL); + assertFalse(dialog.isReplaceDialogOpen()); + } + + @Test + public void testStackSettingsFromAnotherStackAreNotApplied() throws Exception { + initializeTextViewerWithFindReplaceUI("foo bar"); + OverlayAccess dialog= getDialog(); + + Field stackSettingsField= FindReplaceOverlay.class.getDeclaredField("STACK_SETTINGS"); + stackSettingsField.setAccessible(true); + @SuppressWarnings("rawtypes") + WeakHashMap rawMap= (WeakHashMap) stackSettingsField.get(null); + + Object fakeOtherStack= new Object(); + OverlaySearchSettings otherStackSettings= new OverlaySearchSettings(); + otherStackSettings.setFindString("from-other-stack"); + otherStackSettings.setCaseSensitive(true); + otherStackSettings.setWholeWord(true); + @SuppressWarnings("unchecked") + WeakHashMap typedMap= rawMap; + typedMap.put(fakeOtherStack, otherStackSettings); + Accessor actionAccessor= new Accessor(getFindReplaceAction(), FindReplaceAction.class); + FindReplaceOverlay overlay= (FindReplaceOverlay) actionAccessor.get("overlay"); + new Accessor(overlay, FindReplaceOverlay.class).invoke("restoreOverlaySettings"); + + assertEquals("", dialog.getFindText(), + "Settings from a different stack must not be applied to this overlay"); + dialog.assertUnselected(SearchOptions.CASE_SENSITIVE); + dialog.assertUnselected(SearchOptions.WHOLE_WORD); + + } + + @Test + public void testStackSettingsAreUpdatedCorrectly() { + initializeTextViewerWithFindReplaceUI("foo bar"); + OverlayAccess dialog= getDialog(); + + dialog.setFindText("foo"); + dialog.select(SearchOptions.CASE_SENSITIVE); + assertEquals("foo", dialog.getFindText()); + dialog.assertSelected(SearchOptions.CASE_SENSITIVE); + + OverlaySearchSettings updatedSettings= new OverlaySearchSettings(); + updatedSettings.setFindString("bar"); + updatedSettings.setCaseSensitive(false); + updatedSettings.setWholeWord(true); + updatedSettings.setRegex(false); + updatedSettings.setSearchInSelection(false); + updatedSettings.setReplaceBarOpen(false); + + Accessor actionAccessor= new Accessor(getFindReplaceAction(), FindReplaceAction.class); + FindReplaceOverlay overlay= (FindReplaceOverlay) actionAccessor.get("overlay"); + new Accessor(overlay, FindReplaceOverlay.class).invoke("applyStackSettings", + new Class[] { OverlaySearchSettings.class }, new Object[] { updatedSettings }); + + assertEquals("bar", dialog.getFindText(), + "The find text must be updated to reflect the new stack settings"); + dialog.assertUnselected(SearchOptions.CASE_SENSITIVE); + dialog.assertSelected(SearchOptions.WHOLE_WORD); + dialog.assertUnselected(SearchOptions.REGEX); + dialog.assertSelected(SearchOptions.GLOBAL); + assertFalse(dialog.isReplaceDialogOpen()); + } + }