diff --git a/java/java-tests/testSrc/com/intellij/java/navigation/ChooseByNameTest.groovy b/java/java-tests/testSrc/com/intellij/java/navigation/ChooseByNameTest.groovy index 682a356ff93c..871afd2e42bb 100644 --- a/java/java-tests/testSrc/com/intellij/java/navigation/ChooseByNameTest.groovy +++ b/java/java-tests/testSrc/com/intellij/java/navigation/ChooseByNameTest.groovy @@ -439,6 +439,12 @@ class Intf { assert gotoFile('objc/features/i') == [index, i18n] } + void "test matching file in a matching directory"() { + def file = addEmptyFile("foo/index/index") + assert gotoFile('in') == [file, file.parent] + assert gotoFile('foin') == [file, file.parent] + } + private List gotoClass(String text, boolean checkboxState = false) { return getPopupElements(new GotoClassModel2(project), text, checkboxState) } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java b/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java index 72fe2de80751..10d1fa9c47c1 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java @@ -38,6 +38,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; import java.util.stream.Collectors; /** @@ -69,7 +71,7 @@ class DirectoryPathMatcher { String nextPattern = myPattern + c; if (c == '*' || c == '/' || c == ' ') return new DirectoryPathMatcher(myModel, myFiles, nextPattern); - List> files = myFiles != null ? myFiles : getProjectRoots(myModel); + List> files = getMatchingRoots(); List> nextRoots = new ArrayList<>(); MinusculeMatcher matcher = GotoFileItemProvider.getQualifiedNameMatcher(nextPattern); @@ -77,22 +79,55 @@ class DirectoryPathMatcher { if (containsChar(pair.second, c) && matcher.matches(pair.second)) { nextRoots.add(pair); } else { - processSubdirectoriesContaining(pair.first, c, sub -> { + processProjectFilesUnder(pair.first, sub -> { + if (!sub.isDirectory()) return false; + if (!containsChar(sub.getName(), c)) return true; //go deeper + String fullName = myModel.getFullName(sub); - if (fullName == null) return false; + if (fullName == null) return true; fullName = FileUtil.toSystemIndependentName(fullName); if (matcher.matches(fullName)) { nextRoots.add(Pair.create(sub, fullName)); - return true; + return false; } - return false; + return true; }); } } return nextRoots.isEmpty() ? null : new DirectoryPathMatcher(myModel, nextRoots, nextPattern); } - + + /** return null if not cheap */ + @Nullable + Set findFileNamesMatchingIfCheap(char nextLetter, MinusculeMatcher matcher) { + List> files = getMatchingRoots(); + Set names = new HashSet<>(); + AtomicInteger counter = new AtomicInteger(); + BooleanSupplier tooMany = () -> counter.incrementAndGet() > 1000; + for (Pair pair : files) { + if (containsChar(pair.second, nextLetter) && matcher.matches(pair.second)) { + names.add(pair.first.getName()); + } + processProjectFilesUnder(pair.first, sub -> { + counter.incrementAndGet(); + if (tooMany.getAsBoolean()) return false; + + String name = sub.getName(); + if (containsChar(name, nextLetter) && matcher.matches(name)) { + names.add(name); + } + return true; + }); + } + return tooMany.getAsBoolean() ? null : names; + } + + @NotNull + private List> getMatchingRoots() { + return myFiles != null ? myFiles : getProjectRoots(myModel); + } + @NotNull GlobalSearchScope narrowDown(@NotNull GlobalSearchScope fileSearchScope) { if (myFiles == null) return fileSearchScope; @@ -102,19 +137,13 @@ class DirectoryPathMatcher { } - private void processSubdirectoriesContaining(VirtualFile root, char c, Processor consumer) { + private void processProjectFilesUnder(VirtualFile root, Processor consumer) { GlobalSearchScope scope = GlobalSearchScope.allScope(myModel.getProject()); VfsUtilCore.visitChildrenRecursively(root, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { - if (!file.isDirectory() || !scope.contains(file)) return false; - - String name = file.getName(); - if (containsChar(name, c) && consumer.process(file)) { - return false; - } - return true; + return scope.contains(file) && consumer.process(file); } @Nullable diff --git a/platform/lang-impl/src/com/intellij/ide/actions/GotoFileItemProvider.java b/platform/lang-impl/src/com/intellij/ide/actions/GotoFileItemProvider.java index fdbb9a60d9b1..af272de3397a 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/GotoFileItemProvider.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/GotoFileItemProvider.java @@ -275,7 +275,17 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { DirectoryPathMatcher dirMatcher) { MinusculeMatcher fullMatcher = getQualifiedNameMatcher(sanitizedPattern); - List> groups = groupByMatchingDegree(!pattern.startsWith("*")); + List matchingNames = this.matchingNames; + if (patternSuffix.length() <= 3) { + // just enumerate over files + // otherwise there are too many names matching the remaining few letters, and querying index for all of them with a very constrained scope is expensive + Set existingNames = dirMatcher.findFileNamesMatchingIfCheap(patternSuffix.charAt(0), matcher); + if (existingNames != null) { + matchingNames = ContainerUtil.filter(matchingNames, mr -> existingNames.contains(mr.elementName)); + } + } + + List> groups = groupByMatchingDegree(!pattern.startsWith("*"), matchingNames); for (List group : groups) { List files = getFilesMatchingPath(pattern, everywhere, fullMatcher, group, dirMatcher, indicator); if (!files.isEmpty()) { @@ -300,7 +310,7 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { return ContainerUtil.exists(groups, group -> !getFilesMatchingPath(pattern, true, fullMatcher, group, dirMatcher, indicator).isEmpty()); } - private List> groupByMatchingDegree(boolean preferStartMatches) { + private List> groupByMatchingDegree(boolean preferStartMatches, List matchingNames) { if (matchingNames.isEmpty()) return Collections.emptyList(); List> groups = new ArrayList<>();