IDEA-207325 vcs: unify ChangesBrowser file ordering with Project View

This commit is contained in:
Aleksey Pivovarov
2019-03-04 14:22:52 +03:00
parent ea74c86bcc
commit 099b6645e4
12 changed files with 339 additions and 78 deletions
@@ -59,7 +59,8 @@ class RepositoryChangesBrowserNode(repository: Repository) : ChangesBrowserNode<
override fun getSortWeight(): Int = REPOSITORY_SORT_WEIGHT
override fun compareUserObjects(o2: Repository): Int = getShortRepositoryName(getUserObject()).compareTo(getShortRepositoryName(o2), true)
override fun compareUserObjects(o2: Repository): Int =
compareFileNames(getShortRepositoryName(getUserObject()), getShortRepositoryName(o2))
override fun getTextPresentation(): String = getShortRepositoryName(getUserObject())
@@ -15,8 +15,7 @@
*/
package com.intellij.ide.util.treeView;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.NaturalFileNameComparator;
import java.util.Comparator;
@@ -38,30 +37,6 @@ public class AlphaComparator implements Comparator<NodeDescriptor>{
if (s1 == null) return s2 == null ? 0 : -1;
if (s2 == null) return +1;
//for super natural comparison (IDEA-80435)
Pair<String,String> normalized = normalize(s1, s2);
return StringUtil.naturalCompare(normalized.first, normalized.second);
}
private static Pair<String, String> normalize(String s1, String s2) {
final int minLen = Math.min(s1.length(), s2.length());
final StringBuilder sb1 = new StringBuilder(s1);
final StringBuilder sb2 = new StringBuilder(s2);
for (int i = 0; i < minLen; i++) {
final char ch1 = s1.charAt(i);
final char ch2 = sb2.charAt(i);
if (ch1 == ch2 && ch1 == '-') {
sb1.setCharAt(i, '_');
sb2.setCharAt(i, '_');
} else if (ch1 == '-' && ch2 != '_') {
sb1.setCharAt(i, '_');
} else if (ch2 == '-' && ch1 != '_') {
sb2.setCharAt(i, '_');
}
}
s1 = sb1.toString();
s2 = sb2.toString();
return Pair.create(s1, s2);
return NaturalFileNameComparator.INSTANCE.compare(s1, s2);
}
}
@@ -0,0 +1,41 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.util.text;
import com.intellij.openapi.util.Pair;
import java.util.Comparator;
public class NaturalFileNameComparator implements Comparator<String> {
public static final Comparator<String> INSTANCE = new NaturalFileNameComparator();
@Override
public int compare(String s1, String s2) {
//for super natural comparison (IDEA-80435)
Pair<String, String> normalized = normalize(s1, s2);
return StringUtil.naturalCompare(normalized.first, normalized.second);
}
private static Pair<String, String> normalize(String s1, String s2) {
final int minLen = Math.min(s1.length(), s2.length());
final StringBuilder sb1 = new StringBuilder(s1);
final StringBuilder sb2 = new StringBuilder(s2);
for (int i = 0; i < minLen; i++) {
final char ch1 = s1.charAt(i);
final char ch2 = sb2.charAt(i);
if (ch1 == ch2 && ch1 == '-') {
sb1.setCharAt(i, '_');
sb2.setCharAt(i, '_');
}
else if (ch1 == '-' && ch2 != '_') {
sb1.setCharAt(i, '_');
}
else if (ch2 == '-' && ch1 != '_') {
sb2.setCharAt(i, '_');
}
}
s1 = sb1.toString();
s2 = sb2.toString();
return Pair.create(s1, s2);
}
}
@@ -17,6 +17,7 @@ package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.NaturalFileNameComparator;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import org.jetbrains.annotations.NotNull;
@@ -28,9 +29,17 @@ import java.util.Comparator;
* (like in default sorting method of most file managers).
*/
public class HierarchicalFilePathComparator implements Comparator<FilePath> {
public static final HierarchicalFilePathComparator CASE_SENSITIVE = new HierarchicalFilePathComparator(false);
public static final HierarchicalFilePathComparator CASE_INSENSITIVE = new HierarchicalFilePathComparator(true);
public static final HierarchicalFilePathComparator SYSTEM_CASE_SENSITIVE = SystemInfo.isFileSystemCaseSensitive ? CASE_SENSITIVE
: CASE_INSENSITIVE;
public static final HierarchicalFilePathComparator IGNORE_CASE = new HierarchicalFilePathComparator(true);
public static final HierarchicalFilePathComparator SYSTEM_CASE_SENSITIVE = new HierarchicalFilePathComparator(!SystemInfo.isFileSystemCaseSensitive);
public static final HierarchicalFilePathComparator NATURAL = new HierarchicalFilePathComparator(true) {
@Override
protected int compareFileNames(@NotNull String name1, @NotNull String name2) {
return NaturalFileNameComparator.INSTANCE.compare(name1, name2);
}
};
private final boolean myIgnoreCase;
@@ -73,7 +82,14 @@ public class HierarchicalFilePathComparator implements Comparator<FilePath> {
return isDirectory1 ? -1 : 1;
}
return StringUtil.compare(name1, name2, myIgnoreCase);
return compareFileNames(name1, name2);
}
}
/**
* NB: Overriding methods should not return 0, if base method does not.
*/
protected int compareFileNames(@NotNull String name1, @NotNull String name2) {
return StringUtil.compare(name1, name2, myIgnoreCase);
}
}
@@ -162,7 +162,7 @@ public class ChangeDiffRequestChain extends DiffRequestChainBase implements GoTo
@Override
public int compareTo(@NotNull GenericChangesBrowserNode o) {
return myFilePath.getPath().compareToIgnoreCase(o.myFilePath.getPath());
return compareFilePaths(myFilePath, o.myFilePath);
}
}
}
@@ -136,6 +136,6 @@ public class ChangesBrowserChangeListNode extends ChangesBrowserNode<ChangeList>
@Override
public int compareUserObjects(final ChangeList o2) {
return getUserObject().getName().compareToIgnoreCase(o2.getName());
return compareFileNames(getUserObject().getName(), o2.getName());
}
}
@@ -3,7 +3,6 @@
package com.intellij.openapi.vcs.changes.ui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
@@ -19,7 +18,7 @@ public class ChangesBrowserFileNode extends ChangesBrowserNode<VirtualFile> impl
public ChangesBrowserFileNode(@Nullable Project project, @NotNull VirtualFile userObject) {
super(userObject);
myName = StringUtil.toLowerCase(userObject.getName());
myName = userObject.getName();
myProject = project;
}
@@ -73,12 +72,7 @@ public class ChangesBrowserFileNode extends ChangesBrowserNode<VirtualFile> impl
@Override
public int compareTo(ChangesBrowserFileNode o) {
return myName.compareTo(o.myName);
}
@Override
public int compareUserObjects(final VirtualFile o2) {
return getUserObject().getName().compareToIgnoreCase(o2.getName());
return compareFileNames(myName, o.myName);
}
@NotNull
@@ -7,6 +7,7 @@ import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangesUtil;
import com.intellij.openapi.vcs.changes.HierarchicalFilePathComparator;
import com.intellij.ui.SimpleTextAttributes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -99,6 +100,6 @@ public class ChangesBrowserFilePathNode extends ChangesBrowserNode<FilePath> {
@Override
public int compareUserObjects(final FilePath o2) {
return getUserObject().getPath().compareToIgnoreCase(o2.getPath());
return compareFilePaths(getUserObject(), o2);
}
}
@@ -58,7 +58,7 @@ public class ChangesBrowserModuleNode extends ChangesBrowserNode<Module> {
@Override
public int compareUserObjects(final Module o2) {
return getUserObject().getName().compareToIgnoreCase(o2.getName());
return compareFileNames(getUserObject().getName(), o2.getName());
}
@NotNull
@@ -6,12 +6,10 @@ import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.UserDataHolderEx;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.NaturalFileNameComparator;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListOwner;
import com.intellij.openapi.vcs.changes.LocallyDeletedChange;
import com.intellij.openapi.vcs.changes.LogicalLock;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
@@ -321,6 +319,14 @@ public class ChangesBrowserNode<T> extends DefaultMutableTreeNode implements Use
return 0;
}
protected static int compareFileNames(@NotNull String name1, @NotNull String name2) {
return NaturalFileNameComparator.INSTANCE.compare(name1, name2);
}
protected static int compareFilePaths(@NotNull FilePath path1, @NotNull FilePath path2) {
return HierarchicalFilePathComparator.NATURAL.compare(path1, path2);
}
public void setAttributes(@NotNull SimpleTextAttributes attributes) {
myAttributes = attributes;
}
@@ -15,6 +15,7 @@
*/
package com.intellij.openapi.vcs.changes.ui;
import com.intellij.openapi.util.text.NaturalFileNameComparator;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListChange;
@@ -44,14 +45,11 @@ public class ChangesComparator {
private static int comparePaths(@NotNull FilePath filePath1, @NotNull FilePath filePath2, boolean flattened) {
if (!flattened) {
return HierarchicalFilePathComparator.IGNORE_CASE.compare(filePath1, filePath2);
}
else {
int delta = filePath1.getName().compareToIgnoreCase(filePath2.getName());
if (flattened) {
int delta = NaturalFileNameComparator.INSTANCE.compare(filePath1.getName(), filePath2.getName());
if (delta != 0) return delta;
return filePath1.getPath().compareTo(filePath2.getPath());
}
return HierarchicalFilePathComparator.NATURAL.compare(filePath1, filePath2);
}
private static class VirtualFileComparator implements Comparator<VirtualFile> {
@@ -15,7 +15,6 @@
*/
package com.intellij.openapi.vcs.changes.ui;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.LocalFilePath;
@@ -25,8 +24,10 @@ import com.intellij.util.containers.ContainerUtil;
import junit.framework.TestCase;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
public class HierarchicalFilePathComparatorTest extends TestCase {
public void testInOneDirectory() {
@@ -87,8 +88,174 @@ public class HierarchicalFilePathComparatorTest extends TestCase {
assertEquals(-1, compare("/folder/abx/", "/folder/abd"));
}
public void testCaseSensitiveOrder() {
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_SENSITIVE,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/a/Test1.txt_2",
"/a/Test1.txt_22"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_SENSITIVE,
"/a/TEST3-1.TXT",
"/a/TEST3-2.txt",
"/a/Test3-12.TXT",
"/a/Test3.TXT"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_SENSITIVE,
"/Test2.txt/a",
"/Test2_1.txt/a",
"/Test2_12.txt/a",
"/Test2_2.txt/b",
"/Test2_22.txt/b"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_SENSITIVE,
"/A/Test1.txt_2",
"/A/Test1.txt_22",
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_SENSITIVE,
"/B/Test1.txt_2",
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/b/Test1.txt_22"
);
}
public void testCaseInsensitiveOrder() {
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/a/Test1.txt_2",
"/a/Test1.txt_22"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/a/TEST3-1.TXT",
"/a/Test3-12.TXT",
"/a/TEST3-2.txt",
"/a/Test3.TXT"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/Test2.txt/a",
"/Test2_1.txt/a",
"/Test2_12.txt/a",
"/Test2_2.txt/b",
"/Test2_22.txt/b"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/A/Test1.txt_2",
"/A/Test1.txt_22"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/B/Test1.txt_2",
"/b/Test1.txt_22"
);
assertOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/A/B/a",
"/a/b/a",
"/a/b/a_2",
"/A/B/a_2",
"/A/B/a_3",
"/a/b/a_3"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.CASE_INSENSITIVE,
"/A/B/a_1",
"/a/b/a_2",
"/A/B_1/a_1",
"/a/b_1/a_2",
"/A/B_2/a_1",
"/a/b_2/a_2"
);
}
public void testNaturalOrder() {
assertStrictOrderedPaths(
HierarchicalFilePathComparator.NATURAL,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_2",
"/a/Test1.txt_12",
"/a/Test1.txt_22"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.NATURAL,
"/a/Test3.TXT",
"/a/TEST3-1.TXT",
"/a/TEST3-2.txt",
"/a/Test3-12.TXT"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.NATURAL,
"/Test2.txt/a",
"/Test2_1.txt/a",
"/Test2_2.txt/b",
"/Test2_12.txt/a",
"/Test2_22.txt/b"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.NATURAL,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/A/Test1.txt_2",
"/a/Test1.txt_12",
"/A/Test1.txt_22"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.NATURAL,
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/B/Test1.txt_2",
"/b/Test1.txt_22"
);
assertStrictOrderedPaths(
HierarchicalFilePathComparator.NATURAL,
"/Test2.1.txt/a",
"/Test2.1.txt/b",
"/Test2_1.txt/a",
"/Test2_1.txt/b"
);
}
public void testTransitive() {
Collection<String> paths = ContainerUtil.list(
List<String> paths = ContainerUtil.list(
"",
"/",
"~",
@@ -143,43 +310,105 @@ public class HierarchicalFilePathComparatorTest extends TestCase {
"/folder/aaA/.gitignore",
"/folder/Aaa-qwerty/",
"/folder/aAa-qwerty/qwerty",
"/folder/aAa/qwerty/"
"/folder/aAa/qwerty/",
"/Test1",
"/TEST1",
"/Test1.txt",
"/Test1.TXT_1",
"/Test1.txt_2",
"/Test1.txt_12",
"/Test1.TXT_22",
"/Test1.TXT 1",
"/Test1.txt 2",
"/Test1.txt 12",
"/Test1.TXT 22",
"/Test1.txt-1",
"/Test1.TXT-2",
"/Test1.txt-12",
"/Test1.TXT-22",
"/Test1_1.txt",
"/Test1_2.txt",
"/Test1_12.txt",
"/TEST1 1.txt",
"/TEST1 2.txt",
"/Test1 12.txt",
"/Test1.1.txt",
"/Test1-2.txt",
"/Test1-12.txt",
"/a/Test1.txt",
"/a/Test1.txt_1",
"/a/Test1.txt_12",
"/A/Test1.txt",
"/A/Test1.txt_1",
"/A/Test1.txt_12",
"/B/Test1.txt_2",
"/b/Test1.txt_22",
"/b/Test1.txt_2",
"/B/Test1.txt_22",
"/Test1 12.txt/a/",
"/Test1.1.txt/a/",
"/Test1-2.txt/a/",
"/Test1-12.txt/a/",
"/Test1 12.txt/A/",
"/Test1.1.txt/A/",
"/Test1-2.txt/A/",
"/Test1-12.txt/A/",
"/Test1 12.txt/b/",
"/Test1.1.txt/B/",
"/Test1-2.txt/b/",
"/Test1-12.txt/B/"
);
List<FilePath> filePaths = ContainerUtil.map(paths, it -> filePath(it));
assertComparisonContractNotViolated(filePaths, true);
assertComparisonContractNotViolated(filePaths, false);
assertComparisonContractNotViolated(filePaths, HierarchicalFilePathComparator.NATURAL);
assertComparisonContractNotViolated(filePaths, HierarchicalFilePathComparator.CASE_SENSITIVE);
assertComparisonContractNotViolated(filePaths, HierarchicalFilePathComparator.CASE_INSENSITIVE);
}
private static void assertComparisonContractNotViolated(@NotNull List<FilePath> paths, boolean ignoreCase) {
PlatformTestUtil.assertComparisonContractNotViolated(paths,
(path1, path2) -> compare(path1, path2, ignoreCase),
(path1, path2) -> equals(path1, path2, ignoreCase));
private static void assertStrictOrderedPaths(@NotNull Comparator<FilePath> comparator, @NotNull String... paths) {
assertOrderedPaths(Arrays.asList(paths), comparator, true);
}
private static boolean equals(@NotNull FilePath path1, @NotNull FilePath path2, boolean ignoreCase) {
private static void assertOrderedPaths(@NotNull Comparator<FilePath> comparator, @NotNull String... paths) {
assertOrderedPaths(Arrays.asList(paths), comparator, false);
}
private static void assertOrderedPaths(@NotNull List<String> paths, @NotNull Comparator<FilePath> comparator, boolean strict) {
List<FilePath> filePaths = ContainerUtil.map(paths, it -> filePath(it));
List<FilePath> sortedFilePaths = ContainerUtil.sorted(filePaths, comparator);
if (strict) {
TreeSet<FilePath> pathsSet = new TreeSet<>(comparator);
pathsSet.addAll(filePaths);
assertEquals(filePaths.size(), pathsSet.size());
}
assertEquals(filePaths, sortedFilePaths);
assertComparisonContractNotViolated(filePaths, comparator);
}
private static void assertComparisonContractNotViolated(@NotNull List<FilePath> filePaths, @NotNull Comparator<FilePath> comparator) {
PlatformTestUtil.assertComparisonContractNotViolated(filePaths,
(path1, path2) -> compare(path1, path2, comparator),
(path1, path2) -> equals(path1, path2, comparator));
}
private static boolean equals(@NotNull FilePath path1, @NotNull FilePath path2, @NotNull Comparator<FilePath> comparator) {
if (path1.isDirectory() != path2.isDirectory()) return false;
return ignoreCase || !SystemInfo.isFileSystemCaseSensitive
? StringUtil.equalsIgnoreCase(path1.getPath(), path2.getPath())
: StringUtil.equals(path1.getPath(), path2.getPath());
boolean ignoreCase = comparator == HierarchicalFilePathComparator.NATURAL ||
comparator == HierarchicalFilePathComparator.CASE_INSENSITIVE;
return ignoreCase ? StringUtil.equalsIgnoreCase(path1.getPath(), path2.getPath())
: StringUtil.equals(path1.getPath(), path2.getPath());
}
private static int compare(@NotNull String path1, @NotNull String path2) {
int compare1 = compare(filePath(path1), filePath(path2));
int compare2 = compare(filePath(path2), filePath(path1));
int compare1 = compare(filePath(path1), filePath(path2), HierarchicalFilePathComparator.NATURAL);
int compare2 = compare(filePath(path2), filePath(path1), HierarchicalFilePathComparator.NATURAL);
assert compare1 == -compare2;
return compare1;
}
private static int compare(FilePath path1, FilePath path2) {
return compare(path1, path2, true);
}
private static int compare(FilePath path1, FilePath path2, boolean ignoreCase) {
HierarchicalFilePathComparator comparator = ignoreCase
? HierarchicalFilePathComparator.IGNORE_CASE
: HierarchicalFilePathComparator.SYSTEM_CASE_SENSITIVE;
private static int compare(FilePath path1, FilePath path2, @NotNull Comparator<FilePath> comparator) {
return Integer.signum(comparator.compare(path1, path2));
}