Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@
*******************************************************************************/
package org.eclipse.search.internal.ui.text;

import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
Expand Down Expand Up @@ -251,22 +246,6 @@ public boolean hasChildren(Object element) {
return !children.isEmpty();
}

static <T> Stream<T> toStream(Enumeration<T> e) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(e.asIterator(), Spliterator.ORDERED), false);
}

private boolean isUnfiltered(FileMatch m) {
MatchFilter[] filters = fResult.getActiveMatchFilters();
if (filters != null) {
for (MatchFilter filter : filters) {
if (filter.filters(m)) {
return false;
}
}
}
return true;
}

/**
*
* Update the search contents. Screen out any results that are filtered via
Expand All @@ -281,25 +260,7 @@ private boolean isUnfiltered(FileMatch m) {
@Override
public synchronized void elementsChanged(Object[] updatedElements) {
boolean singleElement = updatedElements.length == 1;
Set<LineElement> lineMatches = Collections.emptySet();
// if we have active match filters, we should only use non-filtered FileMatch
// objects to collect LineElements to update
if (hasActiveMatchFilters()) {
lineMatches = Arrays.stream(updatedElements).filter(LineElement.class::isInstance)
// only for distinct files:
.map(u -> ((LineElement) u).getParent()).distinct()
// query matches:
.map(fResult::getMatchSet).flatMap(FileTreeContentProvider::toStream)
.map(m -> ((FileMatch) m)).filter(this::isUnfiltered).map(m -> m.getLineElement())
.collect(Collectors.toSet());
} else {
lineMatches = Arrays.stream(updatedElements).filter(LineElement.class::isInstance)
// only for distinct files:
.map(u -> ((LineElement) u).getParent()).distinct()
// query matches:
.map(fResult::getMatchSet).flatMap(FileTreeContentProvider::toStream)
.map(m -> ((FileMatch) m).getLineElement()).collect(Collectors.toSet());
}
Set<LineElement> lineMatches = getUpdatedLinesWithMatches(updatedElements);
try {
for (Object updatedElement : updatedElements) {
if (!(updatedElement instanceof LineElement lineElement)) {
Expand Down Expand Up @@ -337,6 +298,55 @@ private boolean hasActiveMatchFilters() {
return activeMatchFilters != null && activeMatchFilters.length > 0;
}

/**
* Collects the updated line elements that still have matches. The matches of a
* file are enumerated only once, no matter how many lines of that file have been
* updated, and only the given lines are remembered instead of all lines of the
* touched files.
*
* @param updatedElements the updated elements, may contain elements that are no
* line elements
* @return the line elements of <code>updatedElements</code> that have at least
* one match that is not hidden by an active match filter
*/
private Set<LineElement> getUpdatedLinesWithMatches(Object[] updatedElements) {
// LineElement doesn't implement equals(..)/hashCode(), matches refer to the
// very same instance the update is reported for
Set<LineElement> updatedLines = Collections.newSetFromMap(new IdentityHashMap<>());
Set<IResource> files = new HashSet<>();
for (Object updatedElement : updatedElements) {
if (updatedElement instanceof LineElement lineElement) {
updatedLines.add(lineElement);
files.add(lineElement.getParent());
}
}
if (updatedLines.isEmpty()) {
return Collections.emptySet();
}
// if we have active match filters, we should only use non-filtered FileMatch
// objects to collect LineElements to update. The filter state is evaluated
// once per match by the search result (see AbstractTextSearchResult), it must
// not be computed again here: match filters can be expensive and this code
// runs in the UI thread for every batch of search results.
boolean useFilterState = hasActiveMatchFilters();
Set<LineElement> linesWithMatches = Collections.newSetFromMap(new IdentityHashMap<>());
for (IResource file : files) {
Enumeration<Match> matches = fResult.getMatchSet(file);
while (matches.hasMoreElements()) {
Match match = matches.nextElement();
if (useFilterState && match.isFiltered()) {
continue;
}
LineElement lineElement = ((FileMatch) match).getLineElement();
if (updatedLines.contains(lineElement) && linesWithMatches.add(lineElement)
&& linesWithMatches.size() == updatedLines.size()) {
return linesWithMatches; // all updated lines have matches
Comment on lines +340 to +343
}
}
}
return linesWithMatches;
}

@Override
public void clear() {
initialize(fResult);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
* Copyright (c) 2023, 2026 Red Hat Inc. and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
Expand All @@ -15,35 +15,121 @@

import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.search.internal.ui.SearchMessages;
import org.eclipse.search.ui.text.Match;
import org.eclipse.search.ui.text.MatchFilter;

public class OuterProjectFileFilter extends MatchFilter {

/**
* Remembers for the files of the reported matches whether they are filtered.
* <p>
* The filter state is evaluated for every single match, but
* {@link org.eclipse.core.resources.IWorkspaceRoot#findFilesForLocationURI(URI)}
* iterates over all projects of the workspace and is therefore much too
* expensive to be called once per match: a file usually has many matches.
* </p>
* <p>
* The keys are the file handles held by the matches ({@link Match#getElement()}
* ), and the values don't reference them, so the remembered states are garbage
* collected together with the search result they were computed for: the filter,
* which is shared by all file search results, doesn't keep them alive.
* </p>
* <p>
* Outdated states are discarded by replacing the whole map. A state that is
* computed while the map is replaced is put into the replaced map and is
* therefore never seen again, so an invalidation cannot be lost.
* </p>
*/
private volatile Map<IFile, Boolean> filterStates = newFilterStates();

private final AtomicBoolean isListening = new AtomicBoolean();
Comment on lines +57 to +59

/**
* The files representing a location only change if projects are added, removed,
* opened, closed or moved.
*/
private final IResourceChangeListener projectChangeListener = event -> {
if (affectsProjects(event.getDelta())) {
filterStates = newFilterStates();
}
};

private static Map<IFile, Boolean> newFilterStates() {
return Collections.synchronizedMap(new WeakHashMap<>());
}

private void ensureListeningToProjectChanges() {
if (!isListening.get() && isListening.compareAndSet(false, true)) {
ResourcesPlugin.getWorkspace().addResourceChangeListener(projectChangeListener,
IResourceChangeEvent.POST_CHANGE);
}
}
Comment on lines +75 to +80

private static boolean affectsProjects(IResourceDelta delta) {
if (delta == null) {
return false;
}
for (IResourceDelta projectDelta : delta.getAffectedChildren()) {
if (projectDelta.getKind() != IResourceDelta.CHANGED) {
return true; // project added or removed
}
int flags= projectDelta.getFlags();
if ((flags & (IResourceDelta.OPEN | IResourceDelta.DESCRIPTION | IResourceDelta.MOVED_FROM
| IResourceDelta.MOVED_TO | IResourceDelta.LOCAL_CHANGED | IResourceDelta.REPLACED)) != 0) {
Comment on lines +86 to +92
return true;
}
}
return false;
}

@Override
public boolean filters(Match match) {
if (match instanceof FileMatch) {
IFile file = ((FileMatch) match).getFile();
URI locationUri = file.getLocationURI();

IFile innermostFile = locationUri == null ? file : //
Arrays.stream(file.getWorkspace().getRoot().findFilesForLocationURI(locationUri)) //
// Don't consider the content of a closed project
// for filtering because the matches there cannot be
// shown
.filter(aFile -> aFile.getProject().isAccessible())
.min(Comparator.comparingInt(aFile -> aFile.getFullPath().segments().length))
// shortest workspace (project relative) full path
// means most nested project
.orElse(file);
return !file.equals(innermostFile);
if (!(match instanceof FileMatch fileMatch)) {
return false;
}
return false;
IFile file= fileMatch.getFile();
// the listener is registered before the state of the workspace is read, so
// that every change that invalidates the computed state is reported
ensureListeningToProjectChanges();
Map<IFile, Boolean> states= filterStates;
Boolean isFiltered= states.get(file);
if (isFiltered == null) {
// computed without holding a lock: it may be computed twice for a file,
// but it must not block the other search threads
isFiltered= Boolean.valueOf(computeFilters(file));
states.put(file, isFiltered);
}
return isFiltered.booleanValue();
}

private static boolean computeFilters(IFile file) {
URI locationUri= file.getLocationURI();
if (locationUri == null) {
return false;
}
Optional<IFile> innermostFile= Arrays
.stream(file.getWorkspace().getRoot().findFilesForLocationURI(locationUri)) //
// Don't consider the content of a closed project for filtering
// because the matches there cannot be shown
.filter(aFile -> aFile.getProject().isAccessible())
// shortest workspace (project relative) full path means most
// nested project
.min(Comparator.comparingInt(aFile -> aFile.getFullPath().segments().length));
return innermostFile.isPresent() && !file.equals(innermostFile.get());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
AnnotationManagerTest.class,
FileSearchTests.class,
LineAnnotationManagerTest.class,
NestedProjectFilterTest.class,
PositionTrackerTest.class,
ResultUpdaterTest.class,
SearchResultPageTest.class,
Expand Down
Loading
Loading