From a37b37f24d4ccc4d933c47e8a6f1ea160073cf68 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 11 Aug 2026 15:41:56 +0200 Subject: [PATCH 1/2] File search: don't report shown elements as shown matches FileSearchPage#getLabel() compared the number of shown *leaf elements* with the number of *matches* and rendered the result with the "showing {1} of {2} matches" message. In the tree layout of a text search a leaf is a matching line, and a line can contain more than one match, so the label claimed that matches are hidden although everything was shown, and the reported number was a line count and not a match count. The content provider is the only component that knows what is really shown, since it applies the element limit. It now tells the page whether the element limit hides elements at all, and how many matches are represented by the shown elements. With that the page reports a truncated result only if the element limit really hides something, and reports matches (not lines) in the "showing {1} of {2} matches" message, while the "showing {1} of {2} files" message keeps reporting files. The counts are computed on the model instead of on the SWT widgets, so the label is also correct if the viewer isn't populated yet, and the widget traversal could be removed from the page. As before, the model traversals stop at subtrees hidden by the element limit; truncation is detected without any traversal as long as no element has more children than the element limit allows, and the (linear) count of the shown matches is only computed if elements are really hidden. Matches hidden by match filters are now counted independently from the element limit, so a truncated result is no longer reported as filtered. The query of a search result shown by the file search page is not necessarily a FileSearchQuery: clients (and NullSearchResult in the tests) may show any AbstractTextSearchResult. The unguarded casts in the tree content provider and in the page therefore fail with a ClassCastException as soon as the tree layout is used, so they are replaced by instanceof checks. The added label test restores the element limit and the layout it found, since both are shared by all file search pages via the dialog settings. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/3720 Assisted-by: Github Copilot (Claude Opus 5) --- .../internal/ui/text/FileSearchPage.java | 117 +++----- .../ui/text/FileTableContentProvider.java | 23 +- .../ui/text/FileTreeContentProvider.java | 142 +++++++++- .../ui/text/IFileSearchContentProvider.java | 40 ++- .../tests/filesearch/AllFileSearchTests.java | 1 + .../filesearch/SearchResultPageLabelTest.java | 267 ++++++++++++++++++ 6 files changed, 500 insertions(+), 90 deletions(-) create mode 100644 tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/SearchResultPageLabelTest.java diff --git a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchPage.java b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchPage.java index c894ae2581d..56bb64db5b3 100644 --- a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchPage.java +++ b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchPage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2023 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -28,9 +28,6 @@ import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Item; -import org.eclipse.swt.widgets.Table; -import org.eclipse.swt.widgets.Tree; import org.eclipse.core.runtime.IAdaptable; @@ -296,8 +293,8 @@ protected void fillContextMenu(IMenuManager mgr) { addSortActions(mgr); fActionGroup.setContext(new ActionContext(getSite().getSelectionProvider().getSelection())); fActionGroup.fillContextMenu(mgr); - FileSearchQuery query= (FileSearchQuery) getInput().getQuery(); - if (!query.getSearchString().isEmpty()) { + // the result may be provided by a client that doesn't use a FileSearchQuery + if (getInput().getQuery() instanceof FileSearchQuery query && !query.getSearchString().isEmpty()) { IStructuredSelection selection = getViewer().getStructuredSelection(); if (!selection.isEmpty()) { ReplaceAction replaceSelection= new ReplaceAction(getSite().getShell(), (FileSearchResult)getInput(), selection.toArray()); @@ -445,81 +442,53 @@ private boolean isQueryRunning() { public String getLabel() { String label= super.getLabel(); AbstractTextSearchResult result = getInput(); + if (result == null || fContentProvider == null) { + return label; + } String msg = label; - if (result != null) { - int itemCount = fContentProvider.getLeafCount(result); - if (showLineMatches()) { - int matchCount = result.getMatchCount(); - if (itemCount < matchCount) { - msg = Messages.format(SearchMessages.FileSearchPage_limited_format_matches, - new Object[] { label, Integer.valueOf(itemCount), Integer.valueOf(matchCount) }); - } - } else { - int fileCount = result.getElementsCount(); - if (itemCount < fileCount) { - msg = Messages.format(SearchMessages.FileSearchPage_limited_format_files, - new Object[] { label, Integer.valueOf(itemCount), Integer.valueOf(fileCount) }); - } - } - if (result.getActiveMatchFilters() != null && result.getActiveMatchFilters().length > 0) { - if (isQueryRunning()) { - String message = SearchMessages.FileSearchPage_filtered_message; - return Messages.format(message, new Object[] { msg }); - - } else { - int filteredOut = result.getMatchCount() - getFilteredMatchCount(); - String message = SearchMessages.FileSearchPage_filteredWithCount_message; - return Messages.format(message, new Object[] { msg, String.valueOf(filteredOut) }); - } + if (showLineMatches()) { + // leafs are matching lines, but the user is interested in matches: a leaf + // can represent more than one match, so the number of leafs must not be + // reported as number of matches + if (fContentProvider.isTruncated(result)) { + msg = Messages.format(SearchMessages.FileSearchPage_limited_format_matches, + new Object[] { label, Integer.valueOf(fContentProvider.getShownMatchCount(result)), + Integer.valueOf(result.getMatchCount()) }); } - } - return msg; - } - - private int getFilteredMatchCount() { - StructuredViewer viewer = getViewer(); - if (viewer instanceof TreeViewer) { - ITreeContentProvider tp = (ITreeContentProvider) viewer.getContentProvider(); - return getMatchCount(tp, getRootElements((TreeViewer) getViewer())); } else { - return getMatchCount((TableViewer) viewer); - } - } - - private Object[] getRootElements(TreeViewer viewer) { - Tree t = viewer.getTree(); - Item[] roots = t.getItems(); - Object[] elements = new Object[roots.length]; - for (int i = 0; i < elements.length; i++) { - elements[i] = roots[i].getData(); - } - return elements; - } - - private Object[] getRootElements(TableViewer viewer) { - Table t = viewer.getTable(); - Item[] roots = t.getItems(); - Object[] elements = new Object[roots.length]; - for (int i = 0; i < elements.length; i++) { - elements[i] = roots[i].getData(); + // leafs are files + int shownFileCount = fContentProvider.getLeafCount(result); + int fileCount = result.getElementsCount(); + if (shownFileCount < fileCount) { + msg = Messages.format(SearchMessages.FileSearchPage_limited_format_files, + new Object[] { label, Integer.valueOf(shownFileCount), Integer.valueOf(fileCount) }); + } } - return elements; - } + if (result.getActiveMatchFilters() != null && result.getActiveMatchFilters().length > 0) { + if (isQueryRunning()) { + String message = SearchMessages.FileSearchPage_filtered_message; + return Messages.format(message, new Object[] { msg }); - private int getMatchCount(ITreeContentProvider cp, Object[] elements) { - int count = 0; - for (Object element : elements) { - count += getDisplayedMatchCount(element); - Object[] children = cp.getChildren(element); - count += getMatchCount(cp, children); + } else { + int filteredOut = result.getMatchCount() - getUnfilteredMatchCount(result); + String message = SearchMessages.FileSearchPage_filteredWithCount_message; + return Messages.format(message, new Object[] { msg, String.valueOf(filteredOut) }); + } } - return count; + return msg; } - private int getMatchCount(TableViewer viewer) { + /** + * @param result the search result + * @return the number of matches that are not hidden by the active match + * filters, independently from the element limit + */ + private int getUnfilteredMatchCount(AbstractTextSearchResult result) { int count = 0; - for (Object element : getRootElements(viewer)) { - count += getDisplayedMatchCount(element); + for (Object element : result.getElements()) { + // don't use getDisplayedMatchCount(Object): this page only counts matches + // for line elements if line matches are shown + count += super.getDisplayedMatchCount(element); } return count; } @@ -568,7 +537,9 @@ protected void evaluateChangedElements(Match[] matches, Set changedEleme private boolean showLineMatches() { AbstractTextSearchResult input= getInput(); - return getLayout() == FLAG_LAYOUT_TREE && input != null && !((FileSearchQuery) input.getQuery()).isFileNameSearch(); + // the result may be provided by a client that doesn't use a FileSearchQuery + return getLayout() == FLAG_LAYOUT_TREE && input != null + && input.getQuery() instanceof FileSearchQuery query && !query.isFileNameSearch(); } } diff --git a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTableContentProvider.java b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTableContentProvider.java index 7fa82cd49f5..32375d9c9ce 100644 --- a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTableContentProvider.java +++ b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTableContentProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2023 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -51,6 +51,27 @@ public int getLeafCount(Object parentElement) { return elementsCount; } + @Override + public boolean isTruncated(Object parentElement) { + if (!(parentElement instanceof AbstractTextSearchResult searchResult)) { + return false; + } + int elementLimit = getElementLimit(); + return elementLimit != -1 && searchResult.getElementsCount() > elementLimit; + } + + @Override + public int getShownMatchCount(AbstractTextSearchResult result) { + if (result == null) { + return 0; + } + int count = 0; + for (Object element : getElements(result)) { + count += fPage.getDisplayedMatchCount(element); + } + return count; + } + @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof FileSearchResult fileSearchResult) { diff --git a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTreeContentProvider.java b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTreeContentProvider.java index dc9e000cd91..c46c1d5d5ab 100644 --- a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTreeContentProvider.java +++ b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileTreeContentProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2023 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -17,12 +17,15 @@ *******************************************************************************/ package org.eclipse.search.internal.ui.text; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; @@ -50,6 +53,12 @@ public class FileTreeContentProvider implements ITreeContentProvider, IFileSearc private final FileSearchPage fPage; private final AbstractTreeViewer fTreeViewer; private Map> fChildrenMap; + /** + * Upper bound for the number of children of an element in + * {@link #fChildrenMap}. Only updated while inserting, so it may be bigger than + * the real maximum after elements have been removed. + */ + private int fMaxChildrenCount; FileTreeContentProvider(FileSearchPage page, AbstractTreeViewer viewer) { fPage= page; @@ -81,7 +90,9 @@ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { private synchronized void initialize(AbstractTextSearchResult result) { fResult= result; fChildrenMap= new HashMap<>(); - boolean showLineMatches= !((FileSearchQuery) fResult.getQuery()).isFileNameSearch(); + fMaxChildrenCount= 0; + // the result may be provided by a client that doesn't use a FileSearchQuery + boolean showLineMatches= fResult.getQuery() instanceof FileSearchQuery query && !query.isFileNameSearch(); if (result != null) { Object[] elements= result.getElements(); @@ -137,7 +148,11 @@ private boolean insertChild(Object parent, Object child) { children= new HashSet<>(); fChildrenMap.put(parent, children); } - return children.add(child); + boolean added= children.add(child); + if (added && children.size() > fMaxChildrenCount) { + fMaxChildrenCount= children.size(); + } + return added; } private boolean hasChild(Object parent, Object child) { @@ -226,22 +241,127 @@ public Object[] getChildren(Object parentElement) { @Override public int getLeafCount(Object parentElement) { - Object[] children = getChildren(parentElement); - if (children.length == 0) { + Set children = fChildrenMap.get(parentElement); + if (children == null || children.isEmpty()) { return 0; } + return countShownLeafs(parentElement, getElementLimit()); + } + + /** + * Counts the leafs below the given element that are shown in the viewer. Like + * {@link #getChildren(Object)} only the first elementLimit children + * of an element are considered, subtrees hidden by the element limit are not + * traversed. + * + * @param element the element to count the shown leafs for + * @param elementLimit the element limit, -1 for no limit + * @return number of leafs shown below the given element + */ + private int countShownLeafs(Object element, int elementLimit) { + Set children = fChildrenMap.get(element); + if (children == null || children.isEmpty()) { + return 1; // the element itself is a leaf + } int count = 0; - for (Object object : children) { - boolean leaf = !hasChildren(object); - if (leaf) { - count++; - } else { - count += getLeafCount(object); + int index = 0; + for (Object child : children) { + if (elementLimit != -1 && index >= elementLimit) { + break; // the remaining children are hidden } + count += countShownLeafs(child, elementLimit); + index++; } return count; } + @Override + public boolean isTruncated(Object parentElement) { + int elementLimit = getElementLimit(); + if (elementLimit == -1 || fMaxChildrenCount <= elementLimit) { + // no element can have more children than the element limit allows + return false; + } + return isTruncated(parentElement, elementLimit); + } + + /** + * Elements are hidden if and only if a shown element has more children than the + * element limit allows, so only shown subtrees have to be traversed, and the + * traversal can stop at the first truncated element. + * + * @param element the element to check + * @param elementLimit the element limit, never -1 + * @return true if elements below the given element are hidden + */ + private boolean isTruncated(Object element, int elementLimit) { + Set children = fChildrenMap.get(element); + if (children == null) { + return false; + } + if (children.size() > elementLimit) { + return true; + } + for (Object child : children) { // all children are shown + if (isTruncated(child, elementLimit)) { + return true; + } + } + return false; + } + + @Override + public int getShownMatchCount(AbstractTextSearchResult result) { + if (result == null || !fChildrenMap.containsKey(result)) { + return 0; + } + Map> shownLines = new HashMap<>(); + List shownFiles = new ArrayList<>(); + collectShownLeafs(result, getElementLimit(), shownLines, shownFiles); + + int count = 0; + // count the matches of the shown lines with one pass over the matches of + // each file instead of scanning them once per line + for (Entry> entry : shownLines.entrySet()) { + for (Match match : result.getMatches(entry.getKey())) { + if (isShown(result, match) && entry.getValue().contains(((FileMatch) match).getLineElement())) { + count++; + } + } + } + for (Object file : shownFiles) { + count += fPage.getDisplayedMatchCount(file); + } + return count; + } + + private static boolean isShown(AbstractTextSearchResult result, Match match) { + // see AbstractTextSearchViewPage#getDisplayedMatchCount(Object): if no filters + // are set at all the filter state of a match is ignored + return result.getActiveMatchFilters() == null || !match.isFiltered(); + } + + private void collectShownLeafs(Object element, int elementLimit, Map> shownLines, + List shownFiles) { + Set children = fChildrenMap.get(element); + if (children == null || children.isEmpty()) { + if (element instanceof LineElement line) { + shownLines.computeIfAbsent(line.getParent(), k -> new HashSet<>()).add(line); + } else { + shownFiles.add(element); + } + return; + } + int index = 0; + for (Object child : children) { + if (elementLimit != -1 && index >= elementLimit) { + break; + } + collectShownLeafs(child, elementLimit, shownLines, shownFiles); + index++; + } + } + @Override public boolean hasChildren(Object element) { Set children = fChildrenMap.get(element); diff --git a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/IFileSearchContentProvider.java b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/IFileSearchContentProvider.java index 4404249285d..80b5fd6b0d1 100644 --- a/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/IFileSearchContentProvider.java +++ b/bundles/org.eclipse.search/search/org/eclipse/search/internal/ui/text/IFileSearchContentProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2005 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -13,6 +13,8 @@ *******************************************************************************/ package org.eclipse.search.internal.ui.text; +import org.eclipse.search.ui.text.AbstractTextSearchResult; + public interface IFileSearchContentProvider { public abstract void elementsChanged(Object[] updatedElements); @@ -20,10 +22,38 @@ public interface IFileSearchContentProvider { public abstract void clear(); /** - * @param parentElement - * parent element or input - * @return number of leaf elements in the tree maintained by the provider + * Counts the leaf elements the viewer shows, elements hidden because the element + * limit + * ({@link org.eclipse.search.ui.text.AbstractTextSearchViewPage#getElementLimit()}) + * is exceeded are not counted. Depending on the layout and the kind of the + * search a leaf element is either a file (flat layout or file name search) or a + * matching line (tree layout of a text search). + *

+ * The count is computed on the model and not on the viewer, so it is also + * correct if the viewer isn't populated (yet). + *

+ * + * @param parentElement parent element or input + * @return number of leaf elements shown in the viewer */ public abstract int getLeafCount(Object parentElement); -} \ No newline at end of file + /** + * @param parentElement parent element or input + * @return true if the viewer doesn't show all elements because the + * element limit is exceeded + */ + public abstract boolean isTruncated(Object parentElement); + + /** + * Returns the number of matches represented by the elements currently shown in + * the viewer. Matches are not counted if their element is not shown because the + * element limit is exceeded, or if they are hidden by an active match filter + * ({@link AbstractTextSearchResult#getActiveMatchFilters()}). + * + * @param result the search result shown in the viewer + * @return number of matches represented by the shown elements + */ + public abstract int getShownMatchCount(AbstractTextSearchResult result); + +} diff --git a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/AllFileSearchTests.java b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/AllFileSearchTests.java index 5647b6ac2e9..29103bb47f8 100644 --- a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/AllFileSearchTests.java +++ b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/AllFileSearchTests.java @@ -26,6 +26,7 @@ PositionTrackerTest.class, ResultUpdaterTest.class, SearchResultPageTest.class, + SearchResultPageLabelTest.class, SortingTest.class, TextSearchResultTest.class, RestrictedFilesSearchTest.class, diff --git a/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/SearchResultPageLabelTest.java b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/SearchResultPageLabelTest.java new file mode 100644 index 00000000000..a29f6b1adc8 --- /dev/null +++ b/tests/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/SearchResultPageLabelTest.java @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation 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: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.search.tests.filesearch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.eclipse.swt.widgets.Display; + +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; + +import org.eclipse.search.internal.ui.Messages; +import org.eclipse.search.internal.ui.SearchMessages; +import org.eclipse.search.internal.ui.text.FileSearchPage; +import org.eclipse.search.internal.ui.text.FileSearchQuery; +import org.eclipse.search.internal.ui.text.FileSearchResult; +import org.eclipse.search.tests.ResourceHelper; +import org.eclipse.search.tests.SearchTestUtil; +import org.eclipse.search.ui.ISearchResultViewPart; +import org.eclipse.search.ui.NewSearchUI; +import org.eclipse.search.ui.text.AbstractTextSearchResult; +import org.eclipse.search.ui.text.AbstractTextSearchViewPage; +import org.eclipse.search.ui.text.FileTextSearchScope; +import org.eclipse.search.ui.text.Match; +import org.eclipse.search.ui.text.MatchFilter; + +/** + * Tests the label shown for a file search result, see + * https://github.com/eclipse-platform/eclipse.platform.ui/issues/3720 + */ +public class SearchResultPageLabelTest { + + private static final String PROJECT_NAME= "search-result-label-test"; + + private static final String SEARCH_STRING= "xy"; + + /** + * One line element of the tree, contributing {@link #MATCHES_PER_LINE} matches + * at the offsets 0 and {@link #SECOND_MATCH_OFFSET_IN_LINE}. + */ + private static final String MATCHING_LINE= "xy and xy\n"; + + private static final int SECOND_MATCH_OFFSET_IN_LINE= MATCHING_LINE.indexOf(SEARCH_STRING, 1); + + private static final int MATCHES_PER_LINE= 2; + + private static final int LINE_COUNT= 4; + + private static final int MATCH_COUNT= LINE_COUNT * MATCHES_PER_LINE; + + private static final String FILE_CONTENT= MATCHING_LINE.repeat(LINE_COUNT); + + /** + * Filters the second match of every line, so that every line keeps exactly one + * visible match and no line is removed from the tree. + */ + private static final class SecondMatchInLineFilter extends MatchFilter { + + @Override + public boolean filters(Match match) { + return match.getOffset() % MATCHING_LINE.length() == SECOND_MATCH_OFFSET_IN_LINE; + } + + @Override + public String getName() { + return "Second match in line"; + } + + @Override + public String getDescription() { + return "Filters the second match of every line"; + } + + @Override + public String getActionLabel() { + return getName(); + } + + @Override + public String getID() { + return "org.eclipse.search.tests.secondMatchInLineFilter"; + } + } + + /** Number of matches the {@link SecondMatchInLineFilter} rejects. */ + private static final int FILTERED_MATCH_COUNT= LINE_COUNT; + + /** Number of matches per line that survive the {@link SecondMatchInLineFilter}. */ + private static final int UNFILTERED_MATCHES_PER_LINE= MATCHES_PER_LINE - 1; + + private IProject fProject; + + private FileSearchPage fPage; + + private MatchFilter[] fLastUsedFilters; + + private Integer fPreviousElementLimit; + + private int fPreviousLayout; + + @BeforeEach + public void setUp() throws Exception { + SearchTestUtil.ensureWelcomePageClosed(); + fLastUsedFilters= FileSearchResult.getLastUsedFilters(); + // new search results pick up the last used filters, start without any + FileSearchResult.setLastUsedFilters(new MatchFilter[0]); + fProject= ResourceHelper.createProject(PROJECT_NAME); + IFolder folder= ResourceHelper.createFolder(fProject.getFolder("src")); + ResourceHelper.createFile(folder, "test.txt", FILE_CONTENT); + } + + @AfterEach + public void tearDown() throws Exception { + if (fPage != null) { + // the element limit and the layout are shared by all file search pages + if (fPreviousElementLimit != null) { + fPage.setElementLimit(fPreviousElementLimit); + } + if (fPreviousLayout != 0) { + fPage.setLayout(fPreviousLayout); + } + fPage= null; + } + // setActiveMatchFilters(..) persists the filters in the dialog settings + FileSearchResult.setLastUsedFilters(fLastUsedFilters); + ResourceHelper.deleteProject(PROJECT_NAME); + } + + /** + * If all matches are shown the label must not claim that only some of them are + * shown, even if some lines contain more than one match. + */ + @Test + public void testLabelWithSeveralMatchesPerLine() throws Exception { + AbstractTextSearchResult result= runTextQuery(-1); + + assertEquals(MATCH_COUNT, result.getMatchCount()); + assertEquals(result.getLabel(), fPage.getLabel(), + "nothing is hidden, so the label must not report a limited result"); + } + + /** + * If the element limit hides elements the label must report the number of shown + * matches and not the number of shown elements. + */ + @Test + public void testLabelWithTruncatedResult() throws Exception { + int shownLines= LINE_COUNT - 1; + AbstractTextSearchResult result= runTextQuery(shownLines); + + int shownMatches= shownLines * MATCHES_PER_LINE; + assertEquals(limitedLabel(result, shownMatches), fPage.getLabel()); + } + + /** + * Matches hidden by a match filter are reported by the "filtered" qualifier, and + * the result is not reported as limited as long as the element limit doesn't + * hide anything. + */ + @Test + public void testLabelWithMatchFilter() throws Exception { + AbstractTextSearchResult result= runTextQuery(-1); + activateMatchFilter(result); + + assertEquals(filteredLabel(result.getLabel(), FILTERED_MATCH_COUNT), fPage.getLabel(), + "nothing is hidden by the element limit, so all filtered matches must be reported as filtered"); + } + + /** + * If a match filter and the element limit hide matches, both must be reported + * independently of each other: the "filtered" qualifier counts only the matches + * rejected by the filter, while the "limited" qualifier reports how many of all + * matches are shown. + */ + @Test + public void testLabelWithMatchFilterAndTruncatedResult() throws Exception { + int shownLines= LINE_COUNT - 1; // the element limit hides one line + AbstractTextSearchResult result= runTextQuery(shownLines); + activateMatchFilter(result); + + // only one match per line is visible, the other one is rejected by the filter + int shownMatches= shownLines * UNFILTERED_MATCHES_PER_LINE; + String expected= filteredLabel(limitedLabel(result, shownMatches), FILTERED_MATCH_COUNT); + assertEquals(expected, fPage.getLabel(), + "matches hidden by the element limit must not be counted as filtered from view"); + } + + /** + * A file name search shows files, so nothing is hidden if all files are shown. + */ + @Test + public void testLabelOfFileNameSearch() throws Exception { + AbstractTextSearchResult result= runQuery(new FileSearchQuery("", false, true, createScope()), -1); + + assertEquals(1, result.getElementsCount()); + assertEquals(result.getLabel(), fPage.getLabel()); + } + + private String limitedLabel(AbstractTextSearchResult result, int shownMatches) { + return Messages.format(SearchMessages.FileSearchPage_limited_format_matches, new Object[] { + result.getLabel(), Integer.valueOf(shownMatches), Integer.valueOf(result.getMatchCount()) }); + } + + private String filteredLabel(String label, int filteredOut) { + return Messages.format(SearchMessages.FileSearchPage_filteredWithCount_message, + new Object[] { label, String.valueOf(filteredOut) }); + } + + private void activateMatchFilter(AbstractTextSearchResult result) { + result.setActiveMatchFilters(new MatchFilter[] { new SecondMatchInLineFilter() }); + consumeEvents(); + } + + private AbstractTextSearchResult runTextQuery(int elementLimit) throws Exception { + return runQuery(new FileSearchQuery(SEARCH_STRING, false, true, createScope()), elementLimit); + } + + private FileTextSearchScope createScope() { + return FileTextSearchScope.newSearchScope(new IResource[] { fProject }, new String[] { "*.txt" }, false); + } + + private AbstractTextSearchResult runQuery(FileSearchQuery query, int elementLimit) throws Exception { + NewSearchUI.runQueryInForeground(null, query); + ISearchResultViewPart view= NewSearchUI.getSearchResultView(); + fPage= (FileSearchPage) view.getActivePage(); + fPreviousElementLimit= fPage.getElementLimit(); + fPreviousLayout= fPage.getLayout(); + fPage.setLayout(AbstractTextSearchViewPage.FLAG_LAYOUT_TREE); + fPage.setElementLimit(Integer.valueOf(elementLimit)); + consumeEvents(); + return (AbstractTextSearchResult) query.getSearchResult(); + } + + private void consumeEvents() { + IJobManager manager= Job.getJobManager(); + while (manager.find(fPage).length > 0) { + runEventLoop(); + } + runEventLoop(); + } + + private static void runEventLoop() { + Display display= Display.getCurrent(); + while (display != null && display.readAndDispatch()) { + // process all pending events + } + } +} From ca66172895ca87e54c3a61a4f5f2fc0e96812c6d Mon Sep 17 00:00:00 2001 From: Eclipse Platform Bot Date: Tue, 11 Aug 2026 15:31:21 +0000 Subject: [PATCH 2/2] Version bump(s) for 4.41 stream --- bundles/org.eclipse.search/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.eclipse.search/META-INF/MANIFEST.MF b/bundles/org.eclipse.search/META-INF/MANIFEST.MF index 726e86f7caf..969c9033d31 100644 --- a/bundles/org.eclipse.search/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.search/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.search; singleton:=true -Bundle-Version: 3.19.0.qualifier +Bundle-Version: 3.19.100.qualifier Bundle-Activator: org.eclipse.search.internal.ui.SearchPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %providerName