speedup goto file without slashes by looking for files only in matching directories

This commit is contained in:
peter
2017-09-27 08:26:54 +02:00
parent a8be8ea938
commit 91de89bc3e
5 changed files with 204 additions and 24 deletions
@@ -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"() {
@@ -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<Pair<VirtualFile, String>> myFiles;
@NotNull private final String myPattern;
private DirectoryPathMatcher(@NotNull GotoFileModel model, @Nullable List<Pair<VirtualFile, String>> 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<Pair<VirtualFile, String>> files = myFiles != null ? myFiles : getProjectRoots(myModel);
List<Pair<VirtualFile, String>> nextRoots = new ArrayList<>();
MinusculeMatcher matcher = GotoFileItemProvider.getQualifiedNameMatcher(nextPattern);
for (Pair<VirtualFile, String> 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<VirtualFile> consumer) {
GlobalSearchScope scope = GlobalSearchScope.allScope(myModel.getProject());
VfsUtilCore.visitChildrenRecursively(root, new VirtualFileVisitor<Object>() {
@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<VirtualFile> 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<Pair<VirtualFile, String>> getProjectRoots(GotoFileModel model) {
Set<VirtualFile> 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());
}
}
@@ -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<Object> 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<PsiFileSystemItem> getFilesMatchingPath(@NotNull String pattern,
boolean everywhere,
MinusculeMatcher fullMatcher,
List<String> fileNames) {
List<String> fileNames,
DirectoryPathMatcher dirMatcher) {
GlobalSearchScope scope = dirMatcher.narrowDown(FindSymbolParameters.searchScopeFor(myProject, everywhere));
FindSymbolParameters parameters = new FindSymbolParameters(pattern, pattern, scope, null);
List<PsiFileSystemItem> group = new ArrayList<>();
Map<PsiFileSystemItem, Integer> qualifierMatchingDegrees = new HashMap<>();
Map<PsiFileSystemItem, Integer> dirCloseness = new HashMap<>();
Map<PsiFileSystemItem, Integer> 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<List<String>> 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<String>)new ArrayList<String>()).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<String> 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<? super PsiFileSystemItem> processor) {
boolean processFiles(@NotNull String pattern,
String sanitizedPattern,
boolean everywhere,
Processor<? super PsiFileSystemItem> processor,
DirectoryPathMatcher dirMatcher) {
MinusculeMatcher fullMatcher = getQualifiedNameMatcher(sanitizedPattern);
boolean empty = true;
List<List<String>> groups = groupByMatchingDegree(!pattern.startsWith("*"));
for (List<String> group : groups) {
List<PsiFileSystemItem> files = getFilesMatchingPath(pattern, everywhere, fullMatcher, group);
List<PsiFileSystemItem> 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<List<String>> groups) {
return ContainerUtil.exists(groups, group -> !getFilesMatchingPath(pattern, true, fullMatcher, group).isEmpty());
private boolean hasSuggestionsOutsideProject(@NotNull String pattern,
MinusculeMatcher fullMatcher,
List<List<String>> groups, DirectoryPathMatcher dirMatcher) {
return ContainerUtil.exists(groups, group -> !getFilesMatchingPath(pattern, true, fullMatcher, group, dirMatcher).isEmpty());
}
private List<List<String>> groupByMatchingDegree(boolean preferStartMatches) {
@@ -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;
}
}
@@ -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<FileType> 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) {