From aa5e173dcdf72d2124d2b1b00f726355d450cb02 Mon Sep 17 00:00:00 2001 From: raghucssit Date: Tue, 28 Jul 2026 09:53:02 +0200 Subject: [PATCH] Filtered tree is implemented for Marker Support Views. Bookmarks View, Problems View and Tasks Views will have a search filter box. see https://github.com/eclipse-platform/eclipse.platform.ui/issues/4204 --- .../org.eclipse.ui.ide/META-INF/MANIFEST.MF | 1 + .../views/markers/ExtendedMarkersView.java | 129 ++++- .../views/markers/MarkerPatternFilter.java | 70 +++ .../views/markers/MarkersFilteredTree.java | 51 ++ .../markers/internal/MarkerMessages.java | 2 + .../markers/internal/messages.properties | 2 + .../ui/tests/internal/InternalTestSuite.java | 4 +- .../markers/MarkersFilteredTreeTest.java | 460 ++++++++++++++++++ 8 files changed, 716 insertions(+), 3 deletions(-) create mode 100644 bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkerPatternFilter.java create mode 100644 bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkersFilteredTree.java create mode 100644 tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/markers/MarkersFilteredTreeTest.java diff --git a/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF index a55ff0186ad..32bc975604a 100644 --- a/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF @@ -63,6 +63,7 @@ Require-Bundle: org.eclipse.core.resources;bundle-version="[3.20.0,4.0.0)";resol org.eclipse.e4.ui.ide;bundle-version="[3.15.0,4.0.0)";visibility:=reexport, org.eclipse.e4.core.di;bundle-version="[1.9.0,2.0.0)", org.eclipse.e4.core.di.extensions;bundle-version="[0.18.0,1.0.0)", + org.eclipse.e4.ui.dialogs;bundle-version="[1.7.0,2.0.0)", org.eclipse.ui.navigator;bundle-version="3.12.0" Import-Package: jakarta.annotation;version="[2.0.0,4.0.0)", jakarta.inject;version="[2.0.0,3.0.0)", diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/ExtendedMarkersView.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/ExtendedMarkersView.java index c77d7387206..3194c0f0fc5 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/ExtendedMarkersView.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/ExtendedMarkersView.java @@ -41,9 +41,12 @@ import org.eclipse.help.IContextProvider; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ContributionManager; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.util.OpenStrategy; import org.eclipse.jface.viewers.ColumnPixelData; @@ -155,6 +158,19 @@ public class ExtendedMarkersView extends ViewPart { private static final String TAG_COLUMN_WIDTHS = "columnWidths"; //$NON-NLS-1$ + /** + * Memento/preference key remembering whether the search box used to filter + * the shown markers is visible. + */ + private static final String TAG_SHOW_FILTER_TEXT = "showFilterText"; //$NON-NLS-1$ + + /** + * Prefix of the preference key under which the visibility of the search box + * is remembered per view id, so that newly opened views of the same kind + * start with the last chosen setting. + */ + private static final String PREF_SHOW_FILTER_TEXT_PREFIX = "MarkersView." + TAG_SHOW_FILTER_TEXT + "."; //$NON-NLS-1$ //$NON-NLS-2$ + private final IMarker[] noMarkers = new IMarker[0]; private MarkerContentGenerator generator; @@ -165,8 +181,25 @@ public class ExtendedMarkersView extends ViewPart { private MarkersTreeViewer viewer; + /** + * Hosts {@link #viewer} and provides the search box used to filter the shown + * markers. + */ + private MarkersFilteredTree filteredTree; + private Action filterAction; + /** + * Toggles the visibility of the search box provided by + * {@link #filteredTree}. + */ + private Action showFilterTextAction; + + /** + * Whether the search box used to filter the shown markers is visible. + */ + private boolean showFilterText = true; + /** * The user can set a custom name when opening a new view. This value has to be * persisted when closing the Eclipse. Otherwise the view would fall back to its @@ -258,8 +291,13 @@ private void addMarkers(MarkerSupportItem markerItem, Collection allMar private void createViewer(Composite parent) { parent.setLayout(new FillLayout()); - viewer = new MarkersTreeViewer(new Tree(parent, SWT.H_SCROLL - /*| SWT.VIRTUAL */| SWT.V_SCROLL | SWT.MULTI | SWT.FULL_SELECTION)); + filteredTree = new MarkersFilteredTree(parent, + SWT.H_SCROLL /* | SWT.VIRTUAL */ | SWT.V_SCROLL | SWT.MULTI | SWT.FULL_SELECTION, + new MarkerPatternFilter(this)); + filteredTree.setInitialText(MarkerMessages.MarkerView_searchFilterInitialText); + filteredTree.setFilterTextVisible(showFilterText); + + viewer = (MarkersTreeViewer) filteredTree.getViewer(); WorkbenchViewerSetup.setupViewer(viewer); viewer.getTree().setLinesVisible(true); viewer.setUseHashlookup(true); @@ -1021,6 +1059,7 @@ public void init(IViewSite site, IMemento m) throws PartInitException { builder.setProgressService(service); } this.memento = m; + restoreShowFilterText(m); if (m == null || m.getString(TAG_PART_NAME) == null) { return; @@ -1215,6 +1254,8 @@ public void saveState(IMemento m) { m.putString(TAG_GENERATOR, builder.getGenerator().getId()); } + m.putBoolean(TAG_SHOW_FILTER_TEXT, showFilterText); + if (!getCategoriesToExpand().isEmpty()) { IMemento expanded = m.createChild(TAG_EXPANDED); Iterator categories = getCategoriesToExpand().iterator(); @@ -1442,6 +1483,79 @@ private void initToolBar() { createFilterAction(); tm.add(new Separator("FilterGroup")); //$NON-NLS-1$ tm.add(filterAction); + + IMenuManager menuManager = bars.getMenuManager(); + menuManager.add(new Separator()); + menuManager.add(createShowFilterTextAction()); + } + + /** + * Creates the view menu action toggling the visibility of the search box + * used to filter the shown markers. + * + * @return the created action + */ + private Action createShowFilterTextAction() { + showFilterTextAction = new Action(MarkerMessages.MarkerView_showFilterText, IAction.AS_CHECK_BOX) { + @Override + public void run() { + setShowFilterText(isChecked()); + } + }; + showFilterTextAction.setChecked(showFilterText); + return showFilterTextAction; + } + + /** + * Shows or hides the search box used to filter the shown markers and + * remembers the choice for views of the same kind opened later on. + * + * @param visible whether the search box should be shown + */ + void setShowFilterText(boolean visible) { + showFilterText = visible; + if (showFilterTextAction != null && showFilterTextAction.isChecked() != visible) { + showFilterTextAction.setChecked(visible); + } + if (filteredTree != null && !filteredTree.isDisposed()) { + filteredTree.setFilterTextVisible(visible); + if (visible) { + if (filteredTree.getFilterControl() != null && !filteredTree.getFilterControl().isDisposed()) { + filteredTree.getFilterControl().setFocus(); + } + } else { + setFocus(); + } + } + getPreferenceStore().setValue(getShowFilterTextPreferenceKey(), visible); + } + + /** + * @return whether the search box used to filter the shown markers is visible + */ + boolean isShowFilterText() { + return showFilterText; + } + + private IPreferenceStore getPreferenceStore() { + return IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + } + + private String getShowFilterTextPreferenceKey() { + return PREF_SHOW_FILTER_TEXT_PREFIX + getViewsPrimaryId(); + } + + /** + * Restores the visibility of the search box: the state saved for this very + * view instance takes precedence, otherwise the last state chosen for views + * of the same kind is used, defaulting to a visible search box. + */ + private void restoreShowFilterText(IMemento m) { + IPreferenceStore store = getPreferenceStore(); + String key = getShowFilterTextPreferenceKey(); + store.setDefault(key, true); + Boolean saved = m != null ? m.getBoolean(TAG_SHOW_FILTER_TEXT) : null; + showFilterText = saved != null ? saved.booleanValue() : store.getBoolean(key); } /** @@ -1485,6 +1599,17 @@ TreeViewer getViewer() { return viewer; } + /** + * Returns the {@link MarkersFilteredTree} hosting {@link #getViewer() the + * viewer}, which provides the search box used to filter the shown markers. + * + * @return the filtered tree, or null if the view controls were + * not created yet + */ + MarkersFilteredTree getFilteredTree() { + return filteredTree; + } + /** * The method should not be called directly, see * {@link MarkerUpdateScheduler} diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkerPatternFilter.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkerPatternFilter.java new file mode 100644 index 00000000000..b6649ce2c6c --- /dev/null +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkerPatternFilter.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2026 Advantest Europe 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: + * Raghunandana Murthappa - initial API and implementation + ******************************************************************************/ + +package org.eclipse.ui.internal.views.markers; + +import org.eclipse.e4.ui.dialogs.filteredtree.PatternFilter; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.ui.views.markers.MarkerField; +import org.eclipse.ui.views.markers.MarkerItem; + +/** + * Matches a marker against the values of every field currently visible in a + * Markers view, so that typing in the view's search box filters the markers by + * any of the values shown in their columns (description, resource, path, + * location, type, ...). + */ +class MarkerPatternFilter extends PatternFilter { + + private final ExtendedMarkersView view; + + MarkerPatternFilter(ExtendedMarkersView view) { + this.view = view; + // so that a marker matches when the typed text occurs anywhere in one of + // its values, not only at the beginning of it + setIncludeLeadingWildcard(true); + } + + @Override + protected boolean isLeafMatch(Viewer viewer, Object element) { + if (!(element instanceof MarkerSupportItem item) || !item.isConcrete()) { + // categories are never a leaf match themselves, they stay visible + // through isParentMatch(...) as long as any of their markers match + return false; + } + for (MarkerField field : view.getVisibleFields()) { + if (matches(field, item)) { + return true; + } + } + return false; + } + + /** + * Returns whether the given item's value for the given field matches the + * current pattern. + */ + private boolean matches(MarkerField field, MarkerItem item) { + String value; + try { + value = field.getValue(item); + } catch (RuntimeException e) { + // a field can fail for markers it does not understand, or whose + // underlying resource vanished meanwhile - it simply does not match + // then, filtering must never break the view + return false; + } + return value != null && wordMatches(value); + } +} diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkersFilteredTree.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkersFilteredTree.java new file mode 100644 index 00000000000..2ed2187a29d --- /dev/null +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/views/markers/MarkersFilteredTree.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2026 Advantest Europe 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: + * Raghunandana Murthappa - initial API and implementation + ******************************************************************************/ + +package org.eclipse.ui.internal.views.markers; + +import org.eclipse.e4.ui.dialogs.filteredtree.FilteredTree; +import org.eclipse.e4.ui.dialogs.filteredtree.PatternFilter; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.widgets.Composite; + +/** + * A {@link FilteredTree} hosting the {@link MarkersTreeViewer} of a Markers + * view, so that the shown markers can be narrowed down with the search box + * displayed above the tree. + */ +class MarkersFilteredTree extends FilteredTree { + + MarkersFilteredTree(Composite parentComposite, int treeStyle, PatternFilter filter) { + super(parentComposite, treeStyle, filter); + } + + @Override + protected TreeViewer doCreateTreeViewer(Composite treeParent, int style) { + return new MarkersTreeViewer(treeParent, style); + } + + /** + * Shows or hides the search box used to filter the shown markers. Hiding the + * box also resets the filter, so that no markers stay hidden by a filter the + * user can no longer see. + * + * @param visible whether the search box should be shown + */ + void setFilterTextVisible(boolean visible) { + if (!visible && getFilterControl() != null && !getFilterControl().isDisposed()) { + clearText(); + } + setShowFilterControls(visible); + } +} diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/MarkerMessages.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/MarkerMessages.java index 5b40e0e6630..9ea6153333b 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/MarkerMessages.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/MarkerMessages.java @@ -203,6 +203,8 @@ public class MarkerMessages extends NLS { public static String MarkerView_refreshing_counts; public static String MarkerView_queueing_updates; public static String MarkerView_processUpdates; + public static String MarkerView_searchFilterInitialText; + public static String MarkerView_showFilterText; public static String MarkerView_18; public static String MarkerView_19; diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/messages.properties b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/messages.properties index 716a5e947ef..153271e8451 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/messages.properties +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/views/markers/internal/messages.properties @@ -185,6 +185,8 @@ MarkerView_waiting_on_changes=Waiting for workspace changes to finish MarkerView_searching_for_markers=Searching for markers MarkerView_refreshing_counts=Refreshing marker counts MarkerView_queueing_updates=Queueing viewer updates +MarkerView_searchFilterInitialText=type filter text +MarkerView_showFilterText=&Show text filter MarkerView_18=Filtering on marker limit MarkerView_19=Refreshing view diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java index bb9e013a2a5..84c7fa596ea 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java @@ -28,6 +28,7 @@ import org.eclipse.ui.tests.markers.MarkerTypeTests; import org.eclipse.ui.tests.markers.MarkerViewTests; import org.eclipse.ui.tests.markers.MarkerViewUtilTest; +import org.eclipse.ui.tests.markers.MarkersFilteredTreeTest; import org.eclipse.ui.tests.markers.ResourceMappingMarkersTest; import org.eclipse.ui.tests.markers.ScopeAreaTest; import org.junit.platform.suite.api.SelectClasses; @@ -69,6 +70,7 @@ LargeFileLimitsPreferenceHandlerTest.class, WorkbookEditorsHandlerTest.class, ScopeAreaTest.class, - MarkerTypeTests.class + MarkerTypeTests.class, + MarkersFilteredTreeTest.class }) public class InternalTestSuite {} diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/markers/MarkersFilteredTreeTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/markers/MarkersFilteredTreeTest.java new file mode 100644 index 00000000000..af307cac7c4 --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/markers/MarkersFilteredTreeTest.java @@ -0,0 +1,460 @@ +/******************************************************************************* + * Copyright (c) 2026 Advantest Europe 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: + * Raghunandana Murthappa - initial API and implementation + ******************************************************************************/ + +package org.eclipse.ui.tests.markers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Predicate; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.e4.ui.dialogs.filteredtree.FilteredTree; +import org.eclipse.jface.action.ActionContributionItem; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IContributionItem; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.views.markers.MarkersTreeViewer; +import org.eclipse.ui.tests.harness.util.DisplayHelper; +import org.eclipse.ui.tests.harness.util.FileUtil; +import org.eclipse.ui.views.markers.internal.MarkerMessages; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for the filtered tree used by all markers views, i.e. for the search + * box narrowing down the shown markers and for the view menu entry + * showing/hiding that search box. + *

+ * Every aspect is verified for the Problems, the Tasks and the Bookmarks view, + * since all of them share the same implementation but show different markers + * with different columns. + *

+ *

+ * The views are driven the way a user drives them: the search box is shown and + * hidden through the check box in the view menu and the markers are filtered by + * typing into the search box. + *

+ */ +public class MarkersFilteredTreeTest { + + /** All views the shared filtered tree implementation is used by. */ + private static final String[] MARKER_VIEW_IDS = { IPageLayout.ID_PROBLEM_VIEW, IPageLayout.ID_TASK_LIST, + IPageLayout.ID_BOOKMARKS }; + + private static final String NOT_MATCHING_PATTERN = "ZzqNoSuchMarkerAtAll"; + + private static final String PROJECT_NAME = "MarkersFilteredTreeTestProject"; + + private static final long TIMEOUT = 20000; + + private IWorkbenchPage page; + + private IProject project; + + /** + * Marker messages are made unique per test, so that markers of a previous + * test - which are removed asynchronously - can never be mistaken for the + * ones a test is waiting for. + */ + private String matchingMessage; + + private String otherMessage; + + /** All views opened by a test, so that they can be reset and closed again. */ + private final Set openedViews = new LinkedHashSet<>(); + + @Before + public void setUp() { + page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + String unique = "Zzq" + System.nanoTime(); + matchingMessage = "AlphaMarker" + unique; + otherMessage = "BetaMarker" + unique; + } + + @After + public void tearDown() throws Exception { + for (IViewPart view : openedViews) { + setFilterText(view, ""); + // restore the default, which is also remembered for views opened by + // subsequent tests + setSearchBoxVisible(view, true); + page.hideView(view); + } + openedViews.clear(); + if (project != null) { + FileUtil.deleteProject(project); + project = null; + } + } + + // --- Problems view --------------------------------------------------- + + @Test + public void testProblemsViewUsesFilteredTree() throws Exception { + verifyFilteredTreeIsUsed(IPageLayout.ID_PROBLEM_VIEW); + } + + @Test + public void testProblemsViewSearchBoxCanBeHiddenAndShown() throws Exception { + verifySearchBoxCanBeHiddenAndShown(IPageLayout.ID_PROBLEM_VIEW); + } + + @Test + public void testProblemsViewFiltersMarkers() throws Exception { + verifyMarkersAreFiltered(IPageLayout.ID_PROBLEM_VIEW, IMarker.PROBLEM); + } + + @Test + public void testProblemsViewHidingSearchBoxResetsFilter() throws Exception { + verifyHidingSearchBoxResetsFilter(IPageLayout.ID_PROBLEM_VIEW, IMarker.PROBLEM); + } + + // --- Tasks view ------------------------------------------------------ + + @Test + public void testTasksViewUsesFilteredTree() throws Exception { + verifyFilteredTreeIsUsed(IPageLayout.ID_TASK_LIST); + } + + @Test + public void testTasksViewSearchBoxCanBeHiddenAndShown() throws Exception { + verifySearchBoxCanBeHiddenAndShown(IPageLayout.ID_TASK_LIST); + } + + @Test + public void testTasksViewFiltersMarkers() throws Exception { + verifyMarkersAreFiltered(IPageLayout.ID_TASK_LIST, IMarker.TASK); + } + + @Test + public void testTasksViewHidingSearchBoxResetsFilter() throws Exception { + verifyHidingSearchBoxResetsFilter(IPageLayout.ID_TASK_LIST, IMarker.TASK); + } + + // --- Bookmarks view -------------------------------------------------- + + @Test + public void testBookmarksViewUsesFilteredTree() throws Exception { + verifyFilteredTreeIsUsed(IPageLayout.ID_BOOKMARKS); + } + + @Test + public void testBookmarksViewSearchBoxCanBeHiddenAndShown() throws Exception { + verifySearchBoxCanBeHiddenAndShown(IPageLayout.ID_BOOKMARKS); + } + + @Test + public void testBookmarksViewFiltersMarkers() throws Exception { + verifyMarkersAreFiltered(IPageLayout.ID_BOOKMARKS, IMarker.BOOKMARK); + } + + @Test + public void testBookmarksViewHidingSearchBoxResetsFilter() throws Exception { + verifyHidingSearchBoxResetsFilter(IPageLayout.ID_BOOKMARKS, IMarker.BOOKMARK); + } + + // --- all marker views ------------------------------------------------ + + @Test + public void testViewMenuProvidesSearchBoxToggle() throws Exception { + for (String viewId : MARKER_VIEW_IDS) { + IViewPart view = openViewInDefaultState(viewId); + IAction toggle = getShowFilterTextAction(view); + + assertTrue(viewId + ": the toggle is expected to be checked while the search box is shown", + toggle.isChecked()); + + // the framework updates the checked state before running the action + toggle.setChecked(false); + toggle.run(); + assertSearchBoxVisibility(viewId + ": running the unchecked toggle must hide the search box", view, false); + + toggle.setChecked(true); + toggle.run(); + assertSearchBoxVisibility(viewId + ": running the checked toggle must show the search box again", view, + true); + } + } + + @Test + public void testVisibilityIsRestoredForNewViewInstances() throws Exception { + for (String viewId : MARKER_VIEW_IDS) { + IViewPart view = openViewInDefaultState(viewId); + setSearchBoxVisible(view, false); + closeView(view); + + assertSearchBoxVisibility(viewId + ": a newly opened view is expected to restore the hidden search box", + openView(viewId), false); + + IViewPart reopened = openView(viewId); + setSearchBoxVisible(reopened, true); + closeView(reopened); + + assertSearchBoxVisibility(viewId + ": a newly opened view is expected to restore the shown search box", + openView(viewId), true); + } + } + + // --- verifications shared by all marker views ------------------------ + + private void verifyFilteredTreeIsUsed(String viewId) throws Exception { + IViewPart view = openViewInDefaultState(viewId); + FilteredTree filteredTree = getFilteredTree(view); + + assertTrue(viewId + " filtered tree is expected to host the markers viewer", + filteredTree.getViewer() instanceof MarkersTreeViewer); + + Text filterControl = getFilterControl(view); + assertEquals(viewId + " is expected to show the markers specific hint", + MarkerMessages.MarkerView_searchFilterInitialText, filterControl.getMessage()); + assertEquals("the hint must not be shown as the field's value", "", filterControl.getText()); + } + + private void verifySearchBoxCanBeHiddenAndShown(String viewId) throws Exception { + IViewPart view = openViewInDefaultState(viewId); + assertSearchBoxVisibility(viewId + " is expected to show the search box by default", view, true); + + setSearchBoxVisible(view, false); + assertSearchBoxVisibility(viewId + ": the search box was expected to be hidden", view, false); + + setSearchBoxVisible(view, true); + assertSearchBoxVisibility(viewId + ": the search box was expected to be shown again", view, true); + } + + private void verifyMarkersAreFiltered(String viewId, String markerType) throws Exception { + IViewPart view = openViewWithTestMarkers(viewId, markerType); + + filterOutAllMarkers(viewId, view); + + setFilterText(view, matchingMessage); + waitUntil(viewId + ": only the matching marker was expected to stay visible", view, + showsMatchingMarkerOnly()); + + // markers must also match by the values of the other visible columns, + // the path column shows the project the markers were created in + setFilterText(view, PROJECT_NAME); + waitUntil(viewId + ": markers were expected to match by their path, too", view, showsBothMarkers()); + + setFilterText(view, ""); + waitUntil(viewId + ": all markers were expected to be shown again", view, showsBothMarkers()); + } + + private void verifyHidingSearchBoxResetsFilter(String viewId, String markerType) throws Exception { + IViewPart view = openViewWithTestMarkers(viewId, markerType); + + filterOutAllMarkers(viewId, view); + + setSearchBoxVisible(view, false); + + assertEquals("hiding the search box must reset the filter text", "", getFilterControl(view).getText()); + waitUntil(viewId + ": markers stayed filtered out although the search box was hidden", view, + showsBothMarkers()); + } + + /** + * Opens the given view and waits until the markers created for the test are + * shown by it. + */ + private IViewPart openViewWithTestMarkers(String viewId, String markerType) throws Exception { + IViewPart view = openViewInDefaultState(viewId); + createTestMarkers(markerType); + waitUntil(viewId + ": markers were not shown", view, showsBothMarkers()); + return view; + } + + /** + * Filters with a pattern no marker can match and waits until the view is + * empty. + */ + private void filterOutAllMarkers(String viewId, IViewPart view) { + setFilterText(view, NOT_MATCHING_PATTERN); + waitUntil(viewId + ": markers were not filtered out", () -> getTree(view).getItemCount() == 0); + } + + private static void assertSearchBoxVisibility(String message, IViewPart view, boolean expectedVisible) { + assertEquals(message, expectedVisible, getShowFilterTextAction(view).isChecked()); + assertEquals(message, expectedVisible, getFilteredTree(view).isShowFilterControls()); + assertEquals(message, expectedVisible, getFilterComposite(view).getVisible()); + } + + // --- helpers --------------------------------------------------------- + + /** + * Opens the view and resets it to the state all tests start from, i.e. with + * a shown and empty search box. + */ + private IViewPart openViewInDefaultState(String viewId) throws Exception { + IViewPart view = openView(viewId); + setSearchBoxVisible(view, true); + setFilterText(view, ""); + return view; + } + + private IViewPart openView(String viewId) throws Exception { + IViewPart view = page.showView(viewId); + assertNotNull(viewId + " is expected to be a markers view", view.getAdapter(MarkersTreeViewer.class)); + openedViews.add(view); + return view; + } + + private void closeView(IViewPart view) { + openedViews.remove(view); + page.hideView(view); + } + + /** + * Shows or hides the search box the way the user does it, i.e. through the + * check box contributed to the view menu. + */ + private static void setSearchBoxVisible(IViewPart view, boolean visible) { + IAction toggle = getShowFilterTextAction(view); + toggle.setChecked(visible); + toggle.run(); + } + + private static IAction getShowFilterTextAction(IViewPart view) { + for (IContributionItem item : view.getViewSite().getActionBars().getMenuManager().getItems()) { + if (item instanceof ActionContributionItem contribution + && MarkerMessages.MarkerView_showFilterText.equals(contribution.getAction().getText())) { + return contribution.getAction(); + } + } + throw new AssertionError( + view.getViewSite().getId() + " is expected to contribute a search box toggle to its view menu"); + } + + /** + * Returns the filtered tree hosting the markers of the given view, found by + * walking up from the markers viewer the view adapts to. + */ + private static FilteredTree getFilteredTree(IViewPart view) { + MarkersTreeViewer viewer = view.getAdapter(MarkersTreeViewer.class); + assertNotNull("the markers view is expected to adapt to its viewer", viewer); + Composite parent = viewer.getTree().getParent(); + while (parent != null && !(parent instanceof FilteredTree)) { + parent = parent.getParent(); + } + assertTrue("the markers viewer is expected to be hosted in a filtered tree", parent instanceof FilteredTree); + return (FilteredTree) parent; + } + + private void createTestMarkers(String markerType) throws Exception { + if (project == null) { + project = FileUtil.createProject(PROJECT_NAME); + } + IFile file = FileUtil.createFile("markers.txt", project); + createMarker(file, markerType, matchingMessage); + createMarker(file, markerType, otherMessage); + } + + private static void createMarker(IFile file, String markerType, String message) throws Exception { + IMarker marker = file.createMarker(markerType); + marker.setAttribute(IMarker.MESSAGE, message); + if (IMarker.PROBLEM.equals(markerType)) { + marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); + } + } + + private static void setFilterText(IViewPart view, String text) { + getFilterControl(view).setText(text); + } + + private static Text getFilterControl(IViewPart view) { + Text filterControl = getFilteredTree(view).getFilterControl(); + assertNotNull("the filtered tree is expected to provide a search box", filterControl); + return filterControl; + } + + private static Composite getFilterComposite(IViewPart view) { + return getFilterControl(view).getParent(); + } + + private static Tree getTree(IViewPart view) { + return getFilteredTree(view).getViewer().getTree(); + } + + /** Condition matching while both markers created for the test are shown. */ + private Predicate> showsBothMarkers() { + return shows(matchingMessage).and(shows(otherMessage)); + } + + /** + * Condition matching while only the marker the tests filter for is shown. + */ + private Predicate> showsMatchingMarkerOnly() { + return shows(matchingMessage).and(shows(otherMessage).negate()); + } + + /** + * Returns a condition on all values currently shown in a view, matching when + * any of them contains the given text. + */ + private static Predicate> shows(String text) { + return values -> values.stream().anyMatch(value -> value.contains(text)); + } + + /** + * Collects all values currently shown in the given view. Categories are + * expanded first, since markers are shown as their children when the view is + * grouped. + */ + private static List getShownValues(IViewPart view) { + TreeViewer viewer = getFilteredTree(view).getViewer(); + viewer.expandAll(); + List values = new ArrayList<>(); + collectValues(getTree(view).getItems(), values); + return values; + } + + private static void collectValues(TreeItem[] items, List values) { + for (TreeItem item : items) { + int columnCount = Math.max(1, item.getParent().getColumnCount()); + for (int column = 0; column < columnCount; column++) { + String value = item.getText(column); + if (value != null) { + values.add(value); + } + } + collectValues(item.getItems(), values); + } + } + + private static void waitUntil(String message, IViewPart view, Predicate> condition) { + waitUntil(message, () -> condition.test(getShownValues(view))); + } + + private static void waitUntil(String message, BooleanSupplier condition) { + Display display = PlatformUI.getWorkbench().getDisplay(); + assertTrue(message, DisplayHelper.waitForCondition(display, TIMEOUT, condition)); + } +}