IDEA-112841 Navigate to file action should support fuzzier path search terms

This commit is contained in:
peter
2017-09-11 17:54:42 +02:00
parent 44c0c3f456
commit df067cce4e
4 changed files with 161 additions and 24 deletions
@@ -16,7 +16,6 @@
package com.intellij.java.navigation
import com.intellij.codeInsight.JavaProjectCodeInsightSettings
import com.intellij.ide.actions.GotoFileItemProvider
import com.intellij.ide.util.gotoByName.*
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.Disposable
@@ -186,8 +185,7 @@ class Intf {
void "test accept file paths starting with a dot"() {
def file = myFixture.addFileToProject("foo/index.html", "foo")
def model = new GotoFileModel(project)
def popup = ChooseByNamePopup.createPopup(project, model, new GotoFileItemProvider(project, null, model))
def popup = createPopup(new GotoFileModel(project))
assert calcPopupElements(popup, "./foo/in") == [file]
}
@@ -380,6 +378,18 @@ class Intf {
assert calcPopupElements(popup, "List", false) == [foo, bar]
}
void "test file path matching without slashes"() {
def fooBarFile = myFixture.addFileToProject("foo/bar/index_fooBar.html", "")
def fbFile = myFixture.addFileToProject("fb/index_fb.html", "")
def fbSomeFile = myFixture.addFileToProject("fb/some/index_fbSome.html", "")
def someFbFile = myFixture.addFileToProject("some/fb/index_someFb.html", "")
def model = new GotoFileModel(project)
def popup = createPopup(model)
assert calcPopupElements(popup, "barindex") == [fooBarFile]
assert calcPopupElements(popup, "fooindex") == [fooBarFile]
assert calcPopupElements(popup, "fbindex") == [fbFile, someFbFile, fbSomeFile, fooBarFile]
}
private List<Object> getPopupElements(ChooseByNameModel model, String text, boolean checkboxState = false) {
return calcPopupElements(createPopup(model), text, checkboxState)
}
@@ -15,23 +15,29 @@
*/
package com.intellij.ide.actions;
import com.intellij.ide.util.gotoByName.ChooseByNameBase;
import com.intellij.ide.util.gotoByName.ChooseByNamePopup;
import com.intellij.ide.util.gotoByName.DefaultChooseByNameItemProvider;
import com.intellij.ide.util.gotoByName.GotoFileModel;
import com.intellij.ide.util.gotoByName.*;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.PsiManager;
import com.intellij.psi.codeStyle.MinusculeMatcher;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FList;
import com.intellij.util.containers.HashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author peter
*/
@@ -51,26 +57,141 @@ public class GotoFileItemProvider extends DefaultChooseByNameItemProvider {
boolean everywhere,
@NotNull ProgressIndicator indicator,
@NotNull Processor<Object> consumer) {
PsiFileSystemItem absolute = getFileByAbsolutePath(pattern);
if (absolute != null && !consumer.process(absolute)) {
return true;
}
if (pattern.startsWith("./") || pattern.startsWith(".\\")) {
pattern = pattern.substring(1);
}
String sanitized = removeSlashes(base.transformPattern(pattern));
List<List<String>> nameMatches = getFileNameCandidates(base, everywhere, sanitized, !pattern.startsWith("*"));
MinusculeMatcher fullMatcher = NameUtil.buildMatcher("*" + sanitized, NameUtil.MatchingCaseSensitivity.NONE);
PathProximityComparator pathProximityComparator = getPathProximityComparator();
for (List<String> group : nameMatches) {
if (!ContainerUtil.process(getFilesMatchingPath(pattern, everywhere, fullMatcher, pathProximityComparator, group), consumer)) {
return false;
}
}
return true;
}
@NotNull
private static String removeSlashes(String s) {
if (s.startsWith("/") || s.startsWith("\\")) return removeSlashes(s.substring(1));
if (s.endsWith("/") || s.endsWith("\\")) return removeSlashes(s.substring(0, s.length() - 1));
return s;
}
@Nullable
private PsiFileSystemItem getFileByAbsolutePath(@NotNull String pattern) {
if (pattern.contains("/") || pattern.contains("\\")) {
String path = FileUtil.toSystemIndependentName(ChooseByNamePopup.getTransformedPattern(pattern, myModel));
VirtualFile vFile = LocalFileSystem.getInstance().findFileByPathIfCached(path);
if (vFile != null) {
ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(myProject);
if (index.isInContent(vFile) || index.isInLibraryClasses(vFile) || index.isInLibrarySource(vFile)) {
PsiFileSystemItem fileOrDir = vFile.isDirectory() ?
PsiManager.getInstance(myProject).findDirectory(vFile) :
PsiManager.getInstance(myProject).findFile(vFile);
if (fileOrDir != null && !consumer.process(fileOrDir)) {
return false;
return vFile.isDirectory() ? PsiManager.getInstance(myProject).findDirectory(vFile) : PsiManager.getInstance(myProject).findFile(vFile);
}
}
}
return null;
}
@NotNull
private List<PsiFileSystemItem> getFilesMatchingPath(@NotNull String pattern,
boolean everywhere,
MinusculeMatcher fullMatcher,
PathProximityComparator pathProximityComparator, List<String> fileNames) {
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)) {
String fullName = myModel.getFullName(o);
if (o instanceof PsiFileSystemItem && fullName != null) {
FList<TextRange> fragments = fullMatcher.matchingFragments(fullName);
if (fragments != null) {
group.add((PsiFileSystemItem)o);
qualifierMatchingDegrees.put((PsiFileSystemItem)o, -fullMatcher.matchingDegree(fullName, false, fragments));
String matchingArea = fullName.substring(fragments.getHead().getStartOffset(), fragments.get(fragments.size() - 1).getEndOffset());
dirCloseness.put((PsiFileSystemItem)o, StringUtil.countChars(matchingArea, '/'));
nesting.put((PsiFileSystemItem)o, StringUtil.countChars(fullName, '/'));
}
}
}
}
if (pattern.startsWith("./") || pattern.startsWith(".\\")) {
pattern = pattern.substring(1);
if (group.size() > 1) {
Collections.sort(group, Comparator.comparing(nesting::get).thenComparing(dirCloseness::get).thenComparing(qualifierMatchingDegrees::get).thenComparing(pathProximityComparator).thenComparing(myModel::getFullName));
}
return group;
}
@NotNull
private List<List<String>> getFileNameCandidates(@NotNull ChooseByNameBase base,
boolean everywhere,
String sanitized, boolean preferStartMatches) {
String[] names = myModel.getNames(everywhere);
int start = Math.max(sanitized.lastIndexOf('/'), sanitized.lastIndexOf('\\')) + 1;
Set<String> checkedNames = new HashSet<>();
List<List<String>> groups = new ArrayList<>();
for (int i = start; i < sanitized.length() - 1; i++) {
List<MatchResult> nameMatches = new ArrayList<>();
String namePattern = sanitized.substring(i);
MinusculeMatcher matcher = NameUtil.buildMatcher(namePattern, NameUtil.MatchingCaseSensitivity.NONE);
for (String name : names) {
if (!checkedNames.contains(name)) {
MatchResult result = matches(base, namePattern, matcher, name);
if (result != null) {
checkedNames.add(name);
nameMatches.add(result);
}
}
}
groups.addAll(groupByMatchingDegree(nameMatches, namePattern, preferStartMatches));
}
return super.filterElements(base, pattern, everywhere, indicator, consumer);
return groups;
}
private static List<List<String>> groupByMatchingDegree(List<MatchResult> nameMatches,
String namePattern, boolean preferStartMatches) {
if (nameMatches.isEmpty()) return Collections.emptyList();
List<List<String>> groups = new ArrayList<>();
Comparator<MatchResult> comparator = (mr1, mr2) -> {
boolean exactPrefix1 = namePattern.equalsIgnoreCase(mr1.elementName);
boolean exactPrefix2 = namePattern.equalsIgnoreCase(mr2.elementName);
if (exactPrefix1 != exactPrefix2) return exactPrefix1 ? -1 : 1;
return mr1.compareDegrees(mr2, preferStartMatches);
};
Collections.sort(nameMatches, comparator);
List<String> group = ContainerUtil.newArrayList(nameMatches.get(0).elementName);
for (int j = 1; j < nameMatches.size(); j++) {
MatchResult current = nameMatches.get(j);
if (comparator.compare(nameMatches.get(j - 1), current) == 0) {
group.add(current.elementName);
} else {
groups.add(group);
group = ContainerUtil.newArrayList(current.elementName);
}
}
groups.add(group);
return groups;
}
}
@@ -113,9 +113,7 @@ public class DefaultChooseByNameItemProvider implements ChooseByNameItemProvider
Comparator<Object> weightComparator = new Comparator<Object>() {
@SuppressWarnings("unchecked")
Comparator<Object> modelComparator = model instanceof Comparator
? (Comparator<Object>)model
: new PathProximityComparator(myContext == null ? null :myContext.getElement());
Comparator<Object> modelComparator = model instanceof Comparator ? (Comparator<Object>)model : getPathProximityComparator();
@Override
public int compare(Object o1, Object o2) {
@@ -173,6 +171,11 @@ public class DefaultChooseByNameItemProvider implements ChooseByNameItemProvider
return ContainerUtil.process(qualifierMiddleMatched, consumer);
}
@NotNull
protected PathProximityComparator getPathProximityComparator() {
return new PathProximityComparator(myContext == null ? null : myContext.getElement());
}
private static void sortNamesList(@NotNull String namePattern, @NotNull List<MatchResult> namesList, boolean preferStartMatches) {
Collections.sort(namesList, (mr1, mr2) -> {
boolean exactPrefix1 = namePattern.equalsIgnoreCase(mr1.elementName);
@@ -345,7 +348,7 @@ public class DefaultChooseByNameItemProvider implements ChooseByNameItemProvider
}
@Nullable
private static MatchResult matches(@NotNull ChooseByNameBase base,
protected static MatchResult matches(@NotNull ChooseByNameBase base,
@NotNull String pattern,
@NotNull MinusculeMatcher matcher,
@Nullable String name) {
@@ -370,7 +373,7 @@ public class DefaultChooseByNameItemProvider implements ChooseByNameItemProvider
return NameUtil.buildMatcher(pattern, caseSensitivity);
}
private static class PathProximityComparator implements Comparator<Object> {
protected static class PathProximityComparator implements Comparator<Object> {
@NotNull private final PsiProximityComparator myProximityComparator;
private PathProximityComparator(@Nullable final PsiElement context) {
@@ -17,7 +17,7 @@ package com.intellij.ide.util.gotoByName;
import org.jetbrains.annotations.NotNull;
class MatchResult {
public class MatchResult {
public final String elementName;
final int matchingDegree;
final boolean startMatch;
@@ -28,7 +28,7 @@ class MatchResult {
this.startMatch = startMatch;
}
int compareWith(@NotNull MatchResult that, boolean preferStartMatches) {
public int compareDegrees(@NotNull MatchResult that, boolean preferStartMatches) {
boolean start1 = startMatch;
boolean start2 = that.startMatch;
int startMatchResult = start1 == start2 ? 0 : start1 ? -1 : 1;
@@ -39,8 +39,11 @@ class MatchResult {
if (degree2 < degree1) return -1;
if (degree2 > degree1) return 1;
if (startMatchResult != 0) return -startMatchResult;
return -startMatchResult;
}
return elementName.compareToIgnoreCase(that.elementName);
int compareWith(@NotNull MatchResult that, boolean preferStartMatches) {
int result = compareDegrees(that, preferStartMatches);
return result != 0 ? result : elementName.compareToIgnoreCase(that.elementName);
}
}