diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF
index 4aaef1babe3..248700019c5 100644
--- a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF
+++ b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF
@@ -1,7 +1,7 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %Plugin.name
-Bundle-SymbolicName: org.eclipse.ui.workbench.texteditor.tests
+Bundle-SymbolicName: org.eclipse.ui.workbench.texteditor.tests;singleton:=true
Bundle-Version: 3.15.0.qualifier
Bundle-Vendor: %Plugin.providerName
Bundle-Localization: plugin
diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties b/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties
index f9a3ba9a689..7077b165d0e 100644
--- a/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties
+++ b/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties
@@ -13,6 +13,7 @@
# Mickael Istria (Red Hat Inc.) - 419531 Get rid of nested jars
###############################################################################
bin.includes = plugin.properties,\
+ plugin.xml,\
test.xml,\
about.html,\
.,\
diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml b/tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml
new file mode 100644
index 00000000000..8ac744fdebd
--- /dev/null
+++ b/tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml
@@ -0,0 +1,15 @@
+
+
+
+ * The overlay's input fields sit inside the editor's widget tree, so the editor + * remains the active part while a search term is typed, and its key bindings and + * handlers would otherwise carry out keys meant for an input field. How that is + * prevented is an implementation concern and deliberately not observed here; only + * where a command ends up taking effect is. + *
+ * Key strokes are delivered by notifying the widget rather than by posting native + * events, which still runs the display filter the key binding dispatcher installs + * but does not depend on the operating system, so the test also works headless. + * The native editing inside the SWT text widget does not happen that way, so the + * assertions say what must not reach the editor, plus one control + * proving that keys are dispatched at all. + *
+ * Unlike {@link FindReplaceOverlayTest}, which uses a bare viewer, this needs a
+ * real editor part: without one there is nothing to arbitrate.
+ */
+public class FindReplaceOverlayInEditorTest {
+
+ private static final String CONTENT = "word one word two"; //$NON-NLS-1$
+
+ /** Tagged onto the widgets under {@link FindReplaceOverlay#ID_DATA_KEY}. */
+ private static final String SEARCH_FIELD = "searchInput"; //$NON-NLS-1$
+
+ private static final String REPLACE_FIELD = "replaceInput"; //$NON-NLS-1$
+
+ private StatusTextEditor editor;
+
+ private Text searchField;
+
+ @BeforeEach
+ void openEditorWithOverlay() throws PartInitException {
+ PlatformUI.getWorkbench().getWorkbenchWindows()[0].getShell().forceActive();
+ IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
+ editor = (StatusTextEditor) page.openEditor(new TestTextEditorInput(CONTENT), TestTextEditor.ID);
+ processPendingEvents();
+
+ // Opening through the action rather than through its internals also asserts
+ // that an editor of this kind gets the overlay rather than the dialog.
+ new FindReplaceAction(ResourceBundle.getBundle("org.eclipse.ui.texteditor.ConstructedEditorMessages"), //$NON-NLS-1$
+ "Editor.FindReplace.", editor).run(); //$NON-NLS-1$
+ processPendingEvents();
+
+ searchField = focusedInputField("opening the overlay is expected to focus its search field", //$NON-NLS-1$
+ SEARCH_FIELD);
+ }
+
+ @AfterEach
+ void closeEditor() {
+ searchField = null;
+ if (editor != null) {
+ editor.getSite().getPage().closeEditor(editor, false);
+ editor = null;
+ }
+ }
+
+ /**
+ * A key typed into an input field belongs to that field: it must never be
+ * carried out on the editor's document instead.
+ */
+ @Test
+ public void testKeysTypedIntoTheOverlayDoNotReachTheEditor() {
+ focusSearchField();
+ int selectionLengthBefore = editorSelectionLength();
+
+ type(searchField, SWT.MOD1, 'a');
+
+ assertEquals(selectionLengthBefore, editorSelectionLength(),
+ "Select All's key binding must not select the editor's document"); //$NON-NLS-1$
+ assertEquals(CONTENT, documentText(), "the document must not change"); //$NON-NLS-1$
+
+ type(searchField, SWT.MOD1, SWT.DEL);
+
+ assertEquals(CONTENT, documentText(),
+ "delete-next-word's key binding must not delete from the document"); //$NON-NLS-1$
+
+ // Control: a key the overlay does bind must take effect, otherwise the
+ // assertions above would hold simply because no key was dispatched at all.
+ assertTrue(searchField.isVisible(), "precondition: the overlay is open"); //$NON-NLS-1$
+ type(searchField, SWT.NONE, SWT.ESC);
+ assertFalse(searchField.isVisible(), "Escape in the search field must close the overlay"); //$NON-NLS-1$
+ }
+
+ /**
+ * The retargetable global actions must act on the focused input field, so that
+ * Edit > Select All does what the user expects while typing a search term.
+ */
+ @Test
+ public void testSelectAllActsOnTheFocusedInputField() throws Exception {
+ focusSearchField();
+ searchField.setText("abc"); //$NON-NLS-1$
+ searchField.setSelection(0, 0);
+ int editorSelectionBefore = editorSelectionLength();
+
+ executeCommand("org.eclipse.ui.edit.selectAll"); //$NON-NLS-1$
+
+ assertEquals(3, searchField.getSelectionCount(),
+ "Select All must select the text of the focused input field"); //$NON-NLS-1$
+ assertEquals(editorSelectionBefore, editorSelectionLength(),
+ "Select All must not select the editor's document"); //$NON-NLS-1$
+ }
+
+ /**
+ * Enter finds the next match in the search field and replaces in the replace
+ * field, so the two must be told apart although the key is the same.
+ */
+ @Test
+ public void testEnterMeansSomethingElseInEachInputField() throws Exception {
+ focusSearchField();
+ searchField.setText("word"); //$NON-NLS-1$
+ processPendingEvents();
+ int firstMatch = editorSelectionOffset();
+
+ type(searchField, SWT.NONE, SWT.CR);
+
+ assertNotEquals(firstMatch, editorSelectionOffset(),
+ "Enter in the search field must move on to the next match"); //$NON-NLS-1$
+ assertEquals(CONTENT, documentText(), "finding must not change the document"); //$NON-NLS-1$
+
+ Text replaceField = showReplaceField();
+ replaceField.setText("X"); //$NON-NLS-1$
+ processPendingEvents();
+
+ type(replaceField, SWT.NONE, SWT.CR);
+
+ assertNotEquals(CONTENT, documentText(),
+ "Enter in the replace field must replace the current match in the document"); //$NON-NLS-1$
+ }
+
+ /**
+ * Commands of the surrounding window, Save among them, must stay executable
+ * while an input field has focus: only the editor's own are out of place there.
+ */
+ @Test
+ public void testWindowCommandsStillWorkWhileTheOverlayHasFocus() throws Exception {
+ document().set("edited"); //$NON-NLS-1$
+ processPendingEvents();
+ assertTrue(editor.isDirty(), "precondition: the editor has unsaved changes"); //$NON-NLS-1$
+ focusSearchField();
+
+ executeCommand("org.eclipse.ui.file.save"); //$NON-NLS-1$
+
+ assertFalse(editor.isDirty(), "Save must still reach the editor while the overlay has focus"); //$NON-NLS-1$
+ }
+
+ /**
+ * The overlay is not a part of its own as far as the workbench is concerned, so
+ * views tracking the active part or the selection must see nothing change.
+ */
+ @Test
+ public void testTheEditorStaysTheActivePartWhileTheOverlayHasFocus() {
+ focusSearchField();
+ IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
+
+ assertEquals(editor, page.getActiveEditor(), "active editor"); //$NON-NLS-1$
+ assertEquals(editor, page.getActivePart(), "active part"); //$NON-NLS-1$
+ assertNotNull(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService().getSelection(),
+ "the selection service must keep reporting the editor's selection"); //$NON-NLS-1$
+ }
+
+ /**
+ * The editor acts on its keys again once the overlay no longer has focus. Only
+ * asserting the other direction would be satisfied by an overlay that disabled
+ * the editor's keys for good.
+ */
+ @Test
+ public void testTheEditorActsOnKeysAgainOnceTheOverlayLostFocus() {
+ focusSearchField();
+ editor.setFocus();
+ processPendingEvents();
+
+ type(editorWidget(), SWT.MOD1, 'a');
+
+ assertEquals(CONTENT.length(), editorSelectionLength(),
+ "Select All must apply to the document again once the editor has focus"); //$NON-NLS-1$
+ }
+
+ private void focusSearchField() {
+ // Focus the editor first, so that focusing the search field is a real
+ // transition rather than a no-op on an already focused control.
+ editor.setFocus();
+ processPendingEvents();
+ searchField.forceFocus();
+ processPendingEvents();
+ assertTrue(searchField.isFocusControl(), "the search field is expected to have focus"); //$NON-NLS-1$
+ }
+
+ /** Reveals the replace field through the overlay's own command, which focuses it. */
+ private Text showReplaceField() throws Exception {
+ executeCommand(FindReplaceOverlayCommandSupport.CMD_TOGGLE_REPLACE);
+ return focusedInputField("showing the replace field is expected to focus it", //$NON-NLS-1$
+ REPLACE_FIELD);
+ }
+
+ private static Text focusedInputField(String message, String expectedId) {
+ Text field = assertInstanceOf(Text.class, Display.getCurrent().getFocusControl(), message);
+ assertEquals(expectedId, field.getParent().getData(FindReplaceOverlay.ID_DATA_KEY), message);
+ return field;
+ }
+
+ private static void executeCommand(String commandId) throws Exception {
+ PlatformUI.getWorkbench().getService(IHandlerService.class).executeCommand(commandId, null);
+ processPendingEvents();
+ }
+
+ /**
+ * Delivers a key stroke the way the workbench sees it, through the display
+ * filter the key binding dispatcher installs.
+ */
+ private static void type(org.eclipse.swt.widgets.Control target, int stateMask, int keyCode) {
+ Event keyEvent = new Event();
+ keyEvent.widget = target;
+ keyEvent.type = SWT.KeyDown;
+ keyEvent.stateMask = stateMask;
+ keyEvent.keyCode = keyCode;
+ // Control, and only Control, turns a letter into a control character. On macOS
+ // SWT.MOD1 is Command, which does not.
+ keyEvent.character = (char) ((stateMask & SWT.CTRL) != 0 && Character.isLetter(keyCode)
+ ? Character.toUpperCase(keyCode) - 64
+ : keyCode);
+ target.notifyListeners(SWT.KeyDown, keyEvent);
+ processPendingEvents();
+ }
+
+ private org.eclipse.swt.widgets.Control editorWidget() {
+ return editor.getAdapter(org.eclipse.jface.text.ITextViewer.class).getTextWidget();
+ }
+
+ private IDocument document() {
+ return editor.getDocumentProvider().getDocument(editor.getEditorInput());
+ }
+
+ private String documentText() {
+ return document().get();
+ }
+
+ private ITextSelection editorSelection() {
+ return (ITextSelection) editor.getSelectionProvider().getSelection();
+ }
+
+ private int editorSelectionLength() {
+ return editorSelection().getLength();
+ }
+
+ private int editorSelectionOffset() {
+ return editorSelection().getOffset();
+ }
+
+ private static void processPendingEvents() {
+ Display display = Display.getCurrent();
+ while (display != null && !display.isDisposed() && display.readAndDispatch()) {
+ // keep dispatching
+ }
+ }
+
+}
diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java
new file mode 100644
index 00000000000..a7fd00c5767
--- /dev/null
+++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java
@@ -0,0 +1,79 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+import org.eclipse.jface.operation.IRunnableContext;
+
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.source.IAnnotationModel;
+
+import org.eclipse.ui.texteditor.AbstractDocumentProvider;
+import org.eclipse.ui.texteditor.StatusTextEditor;
+
+/**
+ * A minimal text editor for tests that need a real editor part rather than a bare
+ * viewer, without pulling in the workspace or the IDE: its input is held in memory
+ * and nothing is ever saved. Contributed by this bundle's {@code plugin.xml} and
+ * opened by {@link #ID}, with no file name or content type association, so it is
+ * never offered for a real file.
+ */
+public class TestTextEditor extends StatusTextEditor {
+
+ public static final String ID = "org.eclipse.ui.workbench.texteditor.tests.testTextEditor"; //$NON-NLS-1$
+
+ public TestTextEditor() {
+ setDocumentProvider(new InMemoryDocumentProvider());
+ // Normally established by AbstractDecoratedTextEditor, which belongs to a higher
+ // layer. This scope is what makes an editor a text editor for key bindings.
+ setKeyBindingScopes(new String[] { "org.eclipse.ui.textEditorScope" }); //$NON-NLS-1$
+ }
+
+ private static final class InMemoryDocumentProvider extends AbstractDocumentProvider {
+
+ @Override
+ protected IDocument createDocument(Object element) {
+ return new Document(element instanceof TestTextEditorInput input ? input.getContent() : ""); //$NON-NLS-1$
+ }
+
+ @Override
+ protected IAnnotationModel createAnnotationModel(Object element) {
+ return null;
+ }
+
+ @Override
+ protected void doSaveDocument(IProgressMonitor monitor, Object element, IDocument document,
+ boolean overwrite) {
+ // nothing to save, the document only lives for the duration of a test
+ }
+
+ @Override
+ protected IRunnableContext getOperationRunner(IProgressMonitor monitor) {
+ return null;
+ }
+
+ // AbstractDocumentProvider defaults to read-only, for which the overlay hides
+ // its replace field, unlike the editors it is actually used with.
+
+ @Override
+ public boolean isReadOnly(Object element) {
+ return false;
+ }
+
+ @Override
+ public boolean isModifiable(Object element) {
+ return true;
+ }
+ }
+
+}
diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java
new file mode 100644
index 00000000000..0e094e9d7ff
--- /dev/null
+++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/**
+ * Input for {@link TestTextEditor}, holding its content in memory.
+ */
+public class TestTextEditorInput implements IEditorInput {
+
+ private final String content;
+
+ public TestTextEditorInput(String content) {
+ this.content = content;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return ImageDescriptor.getMissingImageDescriptor();
+ }
+
+ @Override
+ public String getName() {
+ return "Test"; //$NON-NLS-1$
+ }
+
+ @Override
+ public IPersistableElement getPersistable() {
+ return null;
+ }
+
+ @Override
+ public String getToolTipText() {
+ return getName();
+ }
+
+ @Override
+ public