From 2025782517a6ead04a7c8c261bd4dcbb092b8548 Mon Sep 17 00:00:00 2001 From: Aung Nanda Oo Date: Sun, 1 Mar 2026 15:53:56 -0800 Subject: [PATCH 1/2] feat: Add global search result navigation shortcuts --- .../search2/internal/ui/SearchMessages.java | 6 + .../internal/ui/SearchMessages.properties | 6 + .../GlobalNextPrevSearchEntryHandler.java | 64 ++++++ bundles/org.eclipse.search/plugin.properties | 5 + bundles/org.eclipse.search/plugin.xml | 40 ++++ .../eclipse/search/tests/AllSearchTests.java | 4 +- ...PrevSearchEntryHandlerIntegrationTest.java | 196 ++++++++++++++++++ .../GlobalNextPrevSearchEntryHandlerTest.java | 88 ++++++++ .../eclipse/search/tests/SearchTestUtil.java | 22 ++ 9 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/basic/views/GlobalNextPrevSearchEntryHandler.java create mode 100644 tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerIntegrationTest.java create mode 100644 tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerTest.java diff --git a/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.java b/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.java index 75adbe4b6a25..15ba5ec40965 100644 --- a/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.java +++ b/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.java @@ -69,6 +69,12 @@ private SearchMessages() { public static String ShowNextResultAction_tooltip; public static String ShowPreviousResultAction_label; public static String ShowPreviousResultAction_tooltip; + public static String GlobalNextSearchEntryAction_label; + public static String GlobalNextSearchEntryAction_tooltip; + public static String GlobalNextSearchEntryAction_description; + public static String GlobalPreviousSearchEntryAction_label; + public static String GlobalPreviousSearchEntryAction_tooltip; + public static String GlobalPreviousSearchEntryAction_description; public static String RemoveMatchAction_label; public static String RemoveMatchAction_tooltip; public static String DefaultSearchViewPage_show_match; diff --git a/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.properties b/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.properties index 0973f594f8af..99dcd4a543a1 100644 --- a/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.properties +++ b/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/SearchMessages.properties @@ -42,6 +42,12 @@ ShowNextResultAction_label=Next Match ShowNextResultAction_tooltip=Show Next Match ShowPreviousResultAction_label=Previous Match ShowPreviousResultAction_tooltip=Show Previous Match +GlobalNextSearchEntryAction_label=Next Search Entry +GlobalNextSearchEntryAction_tooltip=Go to Next Search Result +GlobalNextSearchEntryAction_description=Navigate to the next result in the current search without switching focus to the Search view +GlobalPreviousSearchEntryAction_label=Previous Search Entry +GlobalPreviousSearchEntryAction_tooltip=Go to Previous Search Result +GlobalPreviousSearchEntryAction_description=Navigate to the previous result in the current search without switching focus to the Search view RemoveMatchAction_label=Remove Match RemoveMatchAction_tooltip=Remove Currently Showing Match DefaultSearchViewPage_show_match=Show Match diff --git a/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/basic/views/GlobalNextPrevSearchEntryHandler.java b/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/basic/views/GlobalNextPrevSearchEntryHandler.java new file mode 100644 index 000000000000..b9aaee963650 --- /dev/null +++ b/bundles/org.eclipse.search/newsearch/org/eclipse/search2/internal/ui/basic/views/GlobalNextPrevSearchEntryHandler.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Foundation 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: + * Eclipse Foundation - initial API and implementation + *******************************************************************************/ +package org.eclipse.search2.internal.ui.basic.views; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExecutableExtension; + +import org.eclipse.search.ui.ISearchResultPage; +import org.eclipse.search.ui.ISearchResultViewPart; +import org.eclipse.search.ui.NewSearchUI; +import org.eclipse.search.ui.text.AbstractTextSearchViewPage; + +/** + * Global handler for navigating to next/previous search results without + * requiring focus on the Search view. Navigates directly through the active + * search result page, keeping focus in the editor throughout. + *

+ * Configured via the {@code :next} or {@code :previous} data suffix in + * {@code plugin.xml}, e.g.: + * {@code defaultHandler="...GlobalNextPrevSearchEntryHandler:previous"} + *

+ */ +public class GlobalNextPrevSearchEntryHandler extends AbstractHandler implements IExecutableExtension { + + private boolean navigateNext = true; + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + ISearchResultViewPart viewPart = NewSearchUI.getSearchResultView(); + if (viewPart == null) { + return null; // No search has been run yet + } + ISearchResultPage page = viewPart.getActivePage(); + if (page instanceof AbstractTextSearchViewPage textPage) { + if (navigateNext) { + textPage.gotoNextMatch(); + } else { + textPage.gotoPreviousMatch(); + } + } + return null; + } + + @Override + public void setInitializationData(IConfigurationElement config, String propertyName, Object data) + throws CoreException { + navigateNext = !"previous".equals(data); //$NON-NLS-1$ + } +} diff --git a/bundles/org.eclipse.search/plugin.properties b/bundles/org.eclipse.search/plugin.properties index b7ef25229b50..77de8ce24415 100644 --- a/bundles/org.eclipse.search/plugin.properties +++ b/bundles/org.eclipse.search/plugin.properties @@ -83,3 +83,8 @@ textSearchQueryProvider="Text Search Query Provider" match_highlight.label= Match highlight background color match_highlight.description= The background color used to highlight matches in the Search view when colored labels are enabled. + +GlobalNextSearchEntryAction_label= Next Search Result +GlobalNextSearchEntryAction_description= Navigate to the next search result from anywhere in the workbench without switching focus to the Search view +GlobalPreviousSearchEntryAction_label= Previous Search Result +GlobalPreviousSearchEntryAction_description= Navigate to the previous search result from anywhere in the workbench without switching focus to the Search view diff --git a/bundles/org.eclipse.search/plugin.xml b/bundles/org.eclipse.search/plugin.xml index 7a7739a4da17..5204f4698794 100644 --- a/bundles/org.eclipse.search/plugin.xml +++ b/bundles/org.eclipse.search/plugin.xml @@ -104,6 +104,20 @@ name="%command.performTextSearchFile.name" description="%command.performTextSearchFile.description" /> + + @@ -162,6 +176,32 @@ commandId="org.eclipse.search.ui.performTextSearchWorkspace" schemeId="org.eclipse.ui.defaultAcceleratorConfiguration" sequence="M1+M3+T"/> + + + + + + + + diff --git a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/AllSearchTests.java b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/AllSearchTests.java index 40cb5e035994..6d1af1eea4c1 100644 --- a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/AllSearchTests.java +++ b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/AllSearchTests.java @@ -23,7 +23,9 @@ @SelectClasses({ AllFileSearchTests.class, AllSearchModelTests.class, - TextSearchRegistryTest.class + TextSearchRegistryTest.class, + GlobalNextPrevSearchEntryHandlerTest.class, + GlobalNextPrevSearchEntryHandlerIntegrationTest.class }) public class AllSearchTests { // see @SelectClasses diff --git a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerIntegrationTest.java b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerIntegrationTest.java new file mode 100644 index 000000000000..05a6e28c571e --- /dev/null +++ b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerIntegrationTest.java @@ -0,0 +1,196 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Foundation 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: + * Eclipse Foundation - initial API and implementation + *******************************************************************************/ +package org.eclipse.search.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Table; + +import org.eclipse.jface.viewers.TableViewer; + +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; + +import org.eclipse.search.internal.ui.text.FileSearchPage; +import org.eclipse.search.internal.ui.text.FileSearchQuery; +import org.eclipse.search.tests.filesearch.JUnitSourceSetup; +import org.eclipse.search.ui.ISearchResultViewPart; +import org.eclipse.search.ui.NewSearchUI; +import org.eclipse.search.ui.text.AbstractTextSearchViewPage; +import org.eclipse.search.ui.text.FileTextSearchScope; +import org.eclipse.search2.internal.ui.basic.views.GlobalNextPrevSearchEntryHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * Integration tests for {@link GlobalNextPrevSearchEntryHandler} that verify + * actual navigation behaviour against a real search result in the workbench. + * + *

The navigation logic in {@link AbstractTextSearchViewPage} tracks match + * position via an internal {@code fCurrentMatchIndex} field that is reset to + * {@code -1} only by the JFace selection-changed listener, not by raw SWT + * {@code table.setSelection()}. The tests here use the same approach as + * {@code SearchResultPageTest.testTableNavigation()}: start with the viewer + * selection at row 0 (leaving the internal match index at its initial value of + * 0), then navigate backwards to reliably arrive at the last element, and then + * navigate forwards to reliably arrive back at the first element. + *

+ */ +public class GlobalNextPrevSearchEntryHandlerIntegrationTest { + + @RegisterExtension + static JUnitSourceSetup fgJUnitSource = new JUnitSourceSetup(); + + private FileSearchPage fPage; + private Table fTable; + private GlobalNextPrevSearchEntryHandler fNextHandler; + private GlobalNextPrevSearchEntryHandler fPrevHandler; + + @BeforeEach + public void setUp() throws Exception { + SearchTestUtil.ensureWelcomePageClosed(); + + String[] fileNamePatterns = { "*.java" }; //$NON-NLS-1$ + FileTextSearchScope scope = FileTextSearchScope.newWorkspaceScope(fileNamePatterns, false); + FileSearchQuery query = new FileSearchQuery("Test", false, true, scope); //$NON-NLS-1$ + NewSearchUI.runQueryInForeground(null, query); + + ISearchResultViewPart viewPart = NewSearchUI.getSearchResultView(); + assertNotNull(viewPart, "Search result view must be open after running a query"); + + fPage = (FileSearchPage) viewPart.getActivePage(); + fPage.setLayout(AbstractTextSearchViewPage.FLAG_LAYOUT_FLAT); + fTable = ((TableViewer) fPage.getViewer()).getTable(); + consumeEvents(); + + assertTrue(fTable.getItemCount() > 1, + "JUnit source project must produce at least 2 results for navigation tests"); + + fNextHandler = new GlobalNextPrevSearchEntryHandler(); + // default is already "next" but be explicit + fNextHandler.setInitializationData(null, "command", "next"); //$NON-NLS-1$ //$NON-NLS-2$ + + fPrevHandler = new GlobalNextPrevSearchEntryHandler(); + fPrevHandler.setInitializationData(null, "command", "previous"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @AfterEach + public void tearDown() { + // Drain all pending UpdateUIJobs for this page so they don't fire during + // subsequent tests' consumeEvents() calls and corrupt their table state. + if (fPage != null) { + consumeEvents(); + } + // Close any editors opened by showCurrentMatch() to leave the workbench clean. + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null && window.getActivePage() != null) { + window.getActivePage().closeAllEditors(false); + } + } + + /** + * Going backward from the initial selection (row 0, internal match index 0) + * decrements the match index to -1, which causes + * {@link AbstractTextSearchViewPage#gotoPreviousMatch()} to wrap around to the + * last result. Then going forward exhausts the last result's matches and wraps + * back to the first result. + * + *

This mirrors the logic verified by + * {@code SearchResultPageTest.testTableNavigation()}, but exercises the + * handlers rather than direct page calls. + *

+ */ + @Test + public void testPreviousWrapsToLastThenNextWrapsToFirst() throws CoreException, Exception { + // Start at the first element (initial state after query). + fTable.setSelection(0); + fTable.showSelection(); + consumeEvents(); + + // Previous from initial match index (0) decrements to -1 → wraps to last. + fPrevHandler.execute(new ExecutionEvent()); + consumeEvents(); + + assertEquals(fTable.getItemCount() - 1, fTable.getSelectionIndex(), + "previous handler should wrap from the first result to the last"); + + // Next from the last result's final match increments beyond the end → wraps + // to the first result. + fNextHandler.execute(new ExecutionEvent()); + consumeEvents(); + + assertEquals(0, fTable.getSelectionIndex(), + "next handler should wrap from the last result to the first"); + } + + /** + * Two independent handler instances must not share internal state. Configuring + * one as "previous" must not affect one configured as "next". + */ + @Test + public void testNextAndPreviousHandlersAreIndependent() throws CoreException, Exception { + // Sanity-check: the two handlers are distinct objects. + assertTrue(fNextHandler != fPrevHandler, + "next and previous handlers must be separate instances"); + + // Drive to last element via previous handler. + fTable.setSelection(0); + fTable.showSelection(); + consumeEvents(); + fPrevHandler.execute(new ExecutionEvent()); + consumeEvents(); + int lastIndex = fTable.getItemCount() - 1; + assertEquals(lastIndex, fTable.getSelectionIndex(), + "previous handler should reach last result"); + + // Drive back to first element via next handler. + fNextHandler.execute(new ExecutionEvent()); + consumeEvents(); + assertEquals(0, fTable.getSelectionIndex(), + "next handler should reach first result"); + } + + /** + * Drains the SWT event queue and waits for all pending {@code UpdateUIJob}s + * belonging to the current page to complete. This is necessary because + * {@code UpdateUIJob} can reschedule itself with a 500 ms delay; a plain + * {@code Display.readAndDispatch()} loop would not wait for those deferred + * runs and could leave stale async work that pollutes subsequent tests. + */ + private void consumeEvents() { + IJobManager manager = Job.getJobManager(); + // Drain immediately-queued display events first. + while (Display.getDefault().readAndDispatch()) { + // keep dispatching + } + // Then wait for all UpdateUIJobs that belong to this page to finish + // (they identify themselves via belongsTo(AbstractTextSearchViewPage.this)). + while (fPage != null && manager.find(fPage).length > 0) { + Display.getDefault().readAndDispatch(); + } + // Final drain for any events triggered by the completed jobs. + while (Display.getDefault().readAndDispatch()) { + // keep dispatching + } + } +} diff --git a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerTest.java b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerTest.java new file mode 100644 index 000000000000..fc34b83fa152 --- /dev/null +++ b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/GlobalNextPrevSearchEntryHandlerTest.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Foundation 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: + * Eclipse Foundation - initial API and implementation + *******************************************************************************/ +package org.eclipse.search.tests; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.search2.internal.ui.basic.views.GlobalNextPrevSearchEntryHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link GlobalNextPrevSearchEntryHandler}. + *

+ * These tests cover configuration via {@code setInitializationData} and + * the no-op behaviour when no search results are available. + * For navigation behaviour with real search results see + * {@link GlobalNextPrevSearchEntryHandlerIntegrationTest}. + *

+ */ +public class GlobalNextPrevSearchEntryHandlerTest { + + private GlobalNextPrevSearchEntryHandler handler; + + @BeforeEach + public void setUp() { + handler = new GlobalNextPrevSearchEntryHandler(); + } + + /** + * When no search has been run the handler returns {@code null} silently rather + * than throwing an exception. + */ + @Test + public void testExecuteReturnsNullWhenNoSearchResultViewIsOpen() throws Exception { + // Deliberately close any open search view so the handler finds nothing + SearchTestUtil.ensureSearchViewClosed(); + Object result = handler.execute(new ExecutionEvent()); + assertNull(result, "Handler should return null when no search result view is open"); + } + + /** + * Configuring with {@code "previous"} must not throw and leaves the handler + * ready to navigate backwards. + */ + @Test + public void testSetInitializationDataWithPrevious() throws CoreException { + handler.setInitializationData(null, "command", "previous"); //$NON-NLS-1$ //$NON-NLS-2$ + // Direction verified behaviourally in GlobalNextPrevSearchEntryHandlerIntegrationTest + } + + /** + * Configuring with {@code "next"} must not throw; it is also the default. + */ + @Test + public void testSetInitializationDataWithNext() throws CoreException { + handler.setInitializationData(null, "command", "next"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + /** + * A {@code null} data value must not throw; the handler defaults to navigate-next. + */ + @Test + public void testSetInitializationDataWithNullDefaultsToNext() throws CoreException { + handler.setInitializationData(null, null, null); + // No exception expected; direction defaults to next + } + + /** + * An unrecognised data value must not throw; the handler defaults to navigate-next. + */ + @Test + public void testSetInitializationDataWithUnknownValueDefaultsToNext() throws CoreException { + handler.setInitializationData(null, "command", "sideways"); //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/SearchTestUtil.java b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/SearchTestUtil.java index 0dfd54b4eece..980991a28ad1 100644 --- a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/SearchTestUtil.java +++ b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/SearchTestUtil.java @@ -26,6 +26,8 @@ import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.search.ui.NewSearchUI; + /** * Util class for search tests. @@ -54,4 +56,24 @@ public static IEditorPart openTextEditor(IWorkbenchPage activePage, IFile openFi return IDE.openEditor(activePage, openFile1, EditorsUI.DEFAULT_TEXT_EDITOR_ID, true); } + /** + * Hides the Search result view in the active workbench window page so that + * {@link NewSearchUI#getSearchResultView()} returns {@code null}. Does nothing + * if no such view is currently open. + */ + public static void ensureSearchViewClosed() { + IWorkbenchWindow activeWorkbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (activeWorkbenchWindow == null) { + return; + } + IWorkbenchPage page = activeWorkbenchWindow.getActivePage(); + if (page == null) { + return; + } + IViewPart searchView = page.findView(NewSearchUI.SEARCH_VIEW_ID); + if (searchView != null) { + page.hideView(searchView); + } + } + } From 8fe351bdf45cfd50ac6e569e875d3166e0044aba Mon Sep 17 00:00:00 2001 From: Eclipse Platform Bot Date: Mon, 2 Mar 2026 09:30:02 +0000 Subject: [PATCH 2/2] Version bump(s) for 4.40 stream --- tests/org.eclipse.search.tests/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/org.eclipse.search.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.search.tests/META-INF/MANIFEST.MF index 0b63e961300d..74d53df0f5ca 100644 --- a/tests/org.eclipse.search.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.search.tests/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.search.tests;singleton:=true -Bundle-Version: 3.12.0.qualifier +Bundle-Version: 3.12.100.qualifier Bundle-Vendor: %providerName Bundle-Localization: plugin Export-Package: org.eclipse.search.core.tests;x-internal:=true,