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 8d583ceb6e51..c54000a3aa5a 100644 --- a/java/java-tests/testSrc/com/intellij/java/navigation/ChooseByNameTest.groovy +++ b/java/java-tests/testSrc/com/intellij/java/navigation/ChooseByNameTest.groovy @@ -402,6 +402,7 @@ class Intf { assert (gotoClass('Ыекштп')[0] as PsiClass).name == 'String' assert (gotoSymbol('Ыекштп')[0] as PsiClass).name == 'String' assert (gotoFile('Ыекштп')[0] as PsiFile).name == 'String.class' + assert (gotoFile('дфтпЫекштп')[0] as PsiFile).name == 'String.class' } void "test prefer exact case match"() { diff --git a/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java b/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java new file mode 100644 index 000000000000..a3fd8f4820ff --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/DirectoryPathMatcher.java @@ -0,0 +1,145 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ide.actions; + +import com.intellij.ide.util.gotoByName.GotoFileModel; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.roots.LibraryOrSdkOrderEntry; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.OrderEntry; +import com.intellij.openapi.roots.OrderRootType; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileVisitor; +import com.intellij.openapi.vfs.newvfs.NewVirtualFile; +import com.intellij.psi.codeStyle.MinusculeMatcher; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.GlobalSearchScopesCore; +import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author peter + */ +class DirectoryPathMatcher { + @NotNull private final GotoFileModel myModel; + @Nullable private final List> myFiles; + @NotNull private final String myPattern; + + private DirectoryPathMatcher(@NotNull GotoFileModel model, @Nullable List> files, @NotNull String pattern) { + myModel = model; + myFiles = files; + myPattern = pattern; + } + + static DirectoryPathMatcher root(@NotNull GotoFileModel model) { + return new DirectoryPathMatcher(model, null, ""); + } + + @Nullable + DirectoryPathMatcher appendChar(char c) { + String nextPattern = myPattern + c; + if (c == '*' || c == '/' || c == ' ') return new DirectoryPathMatcher(myModel, myFiles, nextPattern); + + List> files = myFiles != null ? myFiles : getProjectRoots(myModel); + + List> nextRoots = new ArrayList<>(); + MinusculeMatcher matcher = GotoFileItemProvider.getQualifiedNameMatcher(nextPattern); + for (Pair pair : files) { + if (containsChar(pair.second, c) && matcher.matches(pair.second)) { + nextRoots.add(pair); + } else { + processSubdirectoriesContaining(pair.first, c, sub -> { + String fullName = myModel.getFullName(sub); + if (fullName != null && matcher.matches(fullName)) { + nextRoots.add(Pair.create(sub, fullName)); + return true; + } + return false; + }); + } + } + + return nextRoots.isEmpty() ? null : new DirectoryPathMatcher(myModel, nextRoots, nextPattern); + } + + @NotNull + GlobalSearchScope narrowDown(@NotNull GlobalSearchScope fileSearchScope) { + if (myFiles == null) return fileSearchScope; + + VirtualFile[] array = ContainerUtil.map2Array(myFiles, VirtualFile.class, p -> p.first); + return fileSearchScope.intersectWith(GlobalSearchScopesCore.directoriesScope(myModel.getProject(), true, array)); + + } + + private void processSubdirectoriesContaining(VirtualFile root, char c, 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; + } + + @Nullable + @Override + public Iterable getChildrenIterable(@NotNull VirtualFile file) { + return file instanceof NewVirtualFile ? ((NewVirtualFile)file).getCachedChildren() : null; + } + }); + } + + private static boolean containsChar(String name, char c) { + return StringUtil.indexOfIgnoreCase(name, c, 0) >= 0; + } + + @NotNull + private static List> getProjectRoots(GotoFileModel model) { + Set roots = new HashSet<>(); + for (Module module : ModuleManager.getInstance(model.getProject()).getModules()) { + Collections.addAll(roots, ModuleRootManager.getInstance(module).getContentRoots()); + for (OrderEntry entry : ModuleRootManager.getInstance(module).getOrderEntries()) { + if (entry instanceof LibraryOrSdkOrderEntry) { + Collections.addAll(roots, entry.getFiles(OrderRootType.CLASSES)); + Collections.addAll(roots, entry.getFiles(OrderRootType.SOURCES)); + } + } + } + return roots.stream() + .map(root -> { + VirtualFile top = model.getTopLevelRoot(root); + return top != null ? top : root; + }) + .distinct() + .map(r -> Pair.create(r, StringUtil.notNullize(model.getFullName(r)))) + .collect(Collectors.toList()); + } + +} 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 3ffe06d6db17..e1dbb56fc692 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/GotoFileItemProvider.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/GotoFileItemProvider.java @@ -18,6 +18,7 @@ package com.intellij.ide.actions; import com.intellij.ide.util.gotoByName.*; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.util.ProgressIndicatorBase; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.util.TextRange; @@ -26,13 +27,18 @@ import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFileSystemItem; +import com.intellij.psi.PsiManager; import com.intellij.psi.codeStyle.FixingLayoutMatcher; import com.intellij.psi.codeStyle.MinusculeMatcher; import com.intellij.psi.codeStyle.NameUtil; +import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.FList; +import com.intellij.util.indexing.FindSymbolParameters; import one.util.streamex.IntStreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -68,15 +74,30 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { pattern = pattern.substring(1); } + if (!processItemsForPattern(base, pattern, everywhere, consumer)) { + return false; + } + String fixed = FixingLayoutMatcher.fixLayout(pattern); + return fixed == null || processItemsForPattern(base, fixed, everywhere, consumer); + } + + private boolean processItemsForPattern(@NotNull ChooseByNameBase base, + @NotNull String pattern, + boolean everywhere, + @NotNull Processor consumer) { String sanitized = getSanitizedPattern(pattern, myModel); NameGrouper grouper = new NameGrouper(sanitized.substring(sanitized.lastIndexOf('/') + 1)); myModel.processNames(name -> grouper.processName(name), true); + DirectoryPathMatcher dirMatcher = DirectoryPathMatcher.root(myModel); while (true) { + int index = grouper.index; SuffixMatches group = grouper.nextGroup(base); if (group == null) return true; - if (!group.processFiles(pattern, sanitized, everywhere, consumer)) { + if (!group.processFiles(pattern, sanitized, everywhere, consumer, dirMatcher)) { return false; } + dirMatcher = dirMatcher.appendChar(sanitized.charAt(index)); + if (dirMatcher == null) return true; } } @@ -116,13 +137,17 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { private List getFilesMatchingPath(@NotNull String pattern, boolean everywhere, MinusculeMatcher fullMatcher, - List fileNames) { + List fileNames, + DirectoryPathMatcher dirMatcher) { + GlobalSearchScope scope = dirMatcher.narrowDown(FindSymbolParameters.searchScopeFor(myProject, everywhere)); + FindSymbolParameters parameters = new FindSymbolParameters(pattern, pattern, scope, null); + List group = new ArrayList<>(); Map qualifierMatchingDegrees = new HashMap<>(); Map dirCloseness = new HashMap<>(); Map nesting = new HashMap<>(); for (String fileName : fileNames) { - for (Object o : myModel.getElementsByName(fileName, everywhere, pattern)) { + for (Object o : myModel.getElementsByName(fileName, parameters, new ProgressIndicatorBase())) { String fullName = myModel.getFullName(o); if (o instanceof PsiFileSystemItem && fullName != null) { fullName = FileUtilRt.toSystemIndependentName(fullName); @@ -175,7 +200,6 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { private class NameGrouper { private final String namePattern; - @Nullable private final String alternativePattern; /** Names placed into buckets where the index of bucket == {@link #findMatchStartingPosition} */ private final List> candidateNames; @@ -184,16 +208,12 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { NameGrouper(@NotNull String namePattern) { this.namePattern = namePattern; - alternativePattern = FixingLayoutMatcher.fixLayout(namePattern); candidateNames = IntStreamEx.range(0, namePattern.length()).mapToObj(__ -> (List)new ArrayList()).toList(); } boolean processName(String name) { ProgressManager.checkCanceled(); int position = findMatchStartingPosition(name, namePattern); - if (position >= namePattern.length() && alternativePattern != null) { - position = findMatchStartingPosition(name, alternativePattern); - } if (position < namePattern.length()) { List list = candidateNames.get(position); //noinspection SynchronizationOnLocalVariableOrMethodParameter @@ -247,13 +267,17 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { return false; } - boolean processFiles(@NotNull String pattern, String sanitizedPattern, boolean everywhere, Processor processor) { + boolean processFiles(@NotNull String pattern, + String sanitizedPattern, + boolean everywhere, + Processor processor, + DirectoryPathMatcher dirMatcher) { MinusculeMatcher fullMatcher = getQualifiedNameMatcher(sanitizedPattern); boolean empty = true; List> groups = groupByMatchingDegree(!pattern.startsWith("*")); for (List group : groups) { - List files = getFilesMatchingPath(pattern, everywhere, fullMatcher, group); + List files = getFilesMatchingPath(pattern, everywhere, fullMatcher, group, dirMatcher); empty &= files.isEmpty(); if (!ContainerUtil.process(files, processor)) { return false; @@ -264,7 +288,7 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { return false; // don't process expensive worse matches } - if (!everywhere && hasSuggestionsOutsideProject(pattern, fullMatcher, groups)) { + if (!everywhere && hasSuggestionsOutsideProject(pattern, fullMatcher, groups, dirMatcher)) { // let the framework switch to searching outside project to display these well-matching suggestions // instead of worse-matching ones in project (that are very expensive to calculate) return false; @@ -272,8 +296,10 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider { return true; } - private boolean hasSuggestionsOutsideProject(@NotNull String pattern, MinusculeMatcher fullMatcher, List> groups) { - return ContainerUtil.exists(groups, group -> !getFilesMatchingPath(pattern, true, fullMatcher, group).isEmpty()); + private boolean hasSuggestionsOutsideProject(@NotNull String pattern, + MinusculeMatcher fullMatcher, + List> groups, DirectoryPathMatcher dirMatcher) { + return ContainerUtil.exists(groups, group -> !getFilesMatchingPath(pattern, true, fullMatcher, group, dirMatcher).isEmpty()); } private List> groupByMatchingDegree(boolean preferStartMatches) { diff --git a/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ContributorsBasedGotoByModel.java b/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ContributorsBasedGotoByModel.java index 210ed5b3de6a..23e85179c947 100644 --- a/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ContributorsBasedGotoByModel.java +++ b/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ContributorsBasedGotoByModel.java @@ -287,4 +287,9 @@ public abstract class ContributorsBasedGotoByModel implements ChooseByNameModelE public @NotNull String removeModelSpecificMarkup(@NotNull String pattern) { return pattern; } + + @NotNull + public Project getProject() { + return myProject; + } } diff --git a/platform/lang-impl/src/com/intellij/ide/util/gotoByName/GotoFileModel.java b/platform/lang-impl/src/com/intellij/ide/util/gotoByName/GotoFileModel.java index f5263792589f..ef9a07ec7c5e 100644 --- a/platform/lang-impl/src/com/intellij/ide/util/gotoByName/GotoFileModel.java +++ b/platform/lang-impl/src/com/intellij/ide/util/gotoByName/GotoFileModel.java @@ -46,6 +46,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.Collection; import java.util.Comparator; +import java.util.Objects; /** * Model for "Go to | File" action @@ -172,18 +173,20 @@ public class GotoFileModel extends FilteringGotoByModel implements Dum @Override @Nullable public String getFullName(final Object element) { - if (element instanceof PsiFileSystemItem) { - VirtualFile file = ((PsiFileSystemItem)element).getVirtualFile(); - VirtualFile root = getTopLevelRoot(file); - return root != null ? GotoFileCellRenderer.getRelativePathFromRoot(file, root) - : GotoFileCellRenderer.getRelativePath(file, myProject); - } - - return getElementName(element); + return element instanceof PsiFileSystemItem ? getFullName(((PsiFileSystemItem)element).getVirtualFile()) : getElementName(element); } - private VirtualFile getTopLevelRoot(VirtualFile file) { - return JBIterable.generate(getContentRoot(file), r -> getContentRoot(r.getParent())).last(); + @Nullable + public String getFullName(@NotNull VirtualFile file) { + VirtualFile root = getTopLevelRoot(file); + return root != null ? GotoFileCellRenderer.getRelativePathFromRoot(file, root) + : GotoFileCellRenderer.getRelativePath(file, myProject); + } + + @Nullable + public VirtualFile getTopLevelRoot(@NotNull VirtualFile file) { + VirtualFile root = getContentRoot(file); + return root == null ? null : JBIterable.generate(root, r -> getContentRoot(r.getParent())).last(); } private VirtualFile getContentRoot(@Nullable VirtualFile file) {