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 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 + } + } +}