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 @@ + + + + + + + + + + diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java new file mode 100644 index 00000000000..05e6df9c147 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java @@ -0,0 +1,300 @@ +/******************************************************************************* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ResourceBundle; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Text; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; + +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.handlers.IHandlerService; + +import org.eclipse.ui.texteditor.FindReplaceAction; +import org.eclipse.ui.texteditor.StatusTextEditor; + +/** + * End-to-end test for whether a command acts on the Find/Replace overlay or on + * the editor it is placed on. + *

+ * 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 T getAdapter(Class adapter) { + return null; + } + +} 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 492442c11be..3ae8d48c338 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 @@ -18,6 +18,7 @@ import org.eclipse.ui.internal.findandreplace.FindReplaceLogicTest; import org.eclipse.ui.internal.findandreplace.HistoryStoreTest; +import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayInEditorTest; import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayTest; import org.eclipse.ui.workbench.texteditor.tests.minimap.MinimapPageTest; @@ -48,6 +49,7 @@ TextViewerDeleteLineTargetTest.class, FindReplaceLogicTest.class, FindReplaceOverlayTest.class, + FindReplaceOverlayInEditorTest.class, FindReplaceDialogTest.class, HistoryStoreTest.class, })