diff --git a/platform/lang-impl/src/com/intellij/codeInspection/ui/InspectionTree.java b/platform/lang-impl/src/com/intellij/codeInspection/ui/InspectionTree.java index 2b10c6742dfa..c57e9edad885 100644 --- a/platform/lang-impl/src/com/intellij/codeInspection/ui/InspectionTree.java +++ b/platform/lang-impl/src/com/intellij/codeInspection/ui/InspectionTree.java @@ -57,7 +57,6 @@ import java.util.*; import java.util.stream.Stream; import static com.intellij.codeInspection.CommonProblemDescriptor.DESCRIPTOR_COMPARATOR; -import static com.intellij.ui.tree.TreePathUtil.toTreePathArray; public class InspectionTree extends Tree { private static final Logger LOG = Logger.getInstance(InspectionTree.class); @@ -145,9 +144,8 @@ public class InspectionTree extends Tree { @Nullable public String[] getSelectedGroupPath() { - final TreePath[] paths = getSelectionPaths(); - if (paths == null) return null; - final TreePath commonPath = TreeUtil.findCommonPath(paths); + TreePath commonPath = TreePathUtil.findCommonAncestor(getSelectionPaths()); + if (commonPath == null) return null; for (Object n : commonPath.getPath()) { if (n instanceof InspectionGroupNode) { return getGroupPath((InspectionGroupNode)n); @@ -458,7 +456,8 @@ public class InspectionTree extends Tree { for (InspectionTreeNode parent : parents) { parent.dropProblemCountCaches(); } - TreeUtil.selectPath(this, TreeUtil.findCommonPath(toTreePathArray(pathsToSelect))); + TreePath commonPath = TreePathUtil.findCommonAncestor(pathsToSelect); + if (commonPath != null) TreeUtil.selectPath(this, commonPath); revalidate(); repaint(); diff --git a/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java b/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java index fe4a3d305861..4ffacf260ab5 100644 --- a/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java +++ b/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java @@ -282,7 +282,12 @@ public final class TreeUtil { } } + /** + * @deprecated use TreePathUtil#findCommonAncestor(TreePath...) instead + */ @NotNull + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "2020.2") public static TreePath findCommonPath(@NotNull final TreePath[] treePaths) { LOG.assertTrue(areComponentsEqual(treePaths, 0)); TreePath result = new TreePath(treePaths[0].getPathComponent(0)); diff --git a/platform/platform-impl/src/com/intellij/ui/tree/TreePathUtil.java b/platform/platform-impl/src/com/intellij/ui/tree/TreePathUtil.java index 9101d2439406..8758f98cbd92 100644 --- a/platform/platform-impl/src/com/intellij/ui/tree/TreePathUtil.java +++ b/platform/platform-impl/src/com/intellij/ui/tree/TreePathUtil.java @@ -192,6 +192,7 @@ public class TreePathUtil { return object == null ? null : converter.apply(object); } + @NotNull public static TreePath[] toTreePathArray(@NotNull Collection collection) { return collection.isEmpty() ? EMPTY_TREE_PATH : collection.toArray(EMPTY_TREE_PATH); } @@ -213,4 +214,38 @@ public class TreePathUtil { public static TreePath[] toTreePaths(TreeNode... nodes) { return nodes == null ? null : Stream.of(nodes).map(TreePathUtil::toTreePath).filter(Objects::nonNull).toArray(TreePath[]::new); } + + /** + * @param paths an array of tree paths to iterate through + * @return a common ancestor for the given paths, or {@code null} if these paths do not have one + */ + public static TreePath findCommonAncestor(TreePath... paths) { + if (ArrayUtil.isEmpty(paths)) return null; + if (paths.length == 1) return paths[0]; + return findCommonAncestor(Arrays.asList(paths)); + } + + /** + * @param paths a collection of tree paths to iterate through + * @return a common ancestor for the given paths, or {@code null} if these paths do not have one + */ + public static TreePath findCommonAncestor(@NotNull Iterable paths) { + TreePath ancestor = null; + for (int i = 0; i < Integer.MAX_VALUE; i++) { + TreePath first = null; + for (TreePath path : paths) { + int count = path.getPathCount(); + if (count <= i) return ancestor; // a path is too short + while (--count > i) path = path.getParentPath(); + if (path == null) throw new IllegalStateException("unexpected"); + if (first == null) first = path; // initialize with the first path in the given collection + if (first != path && !Objects.equals(first.getLastPathComponent(), path.getLastPathComponent())) { + return ancestor; // different components at the current index + } + } + if (first == null) return ancestor; // nothing to iterate + ancestor = createTreePath(ancestor, first.getLastPathComponent()); + } + return ancestor; + } } diff --git a/platform/platform-tests/testSrc/com/intellij/ui/tree/TreeCollectorTest.java b/platform/platform-tests/testSrc/com/intellij/ui/tree/TreeCollectorTest.java index 3cf24ffc23b3..8de1cca47b3d 100644 --- a/platform/platform-tests/testSrc/com/intellij/ui/tree/TreeCollectorTest.java +++ b/platform/platform-tests/testSrc/com/intellij/ui/tree/TreeCollectorTest.java @@ -2,6 +2,8 @@ package com.intellij.ui.tree; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.TimeoutUtil; +import com.intellij.util.ui.tree.TreeUtil; import org.junit.Assert; import org.junit.Test; @@ -9,13 +11,10 @@ import javax.swing.tree.TreePath; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import java.util.function.IntFunction; import java.util.function.Supplier; -import static com.intellij.util.TimeoutUtil.measureExecutionTime; -import static com.intellij.util.TimeoutUtil.sleep; -import static com.intellij.util.ui.tree.TreeUtil.selectMaximals; - public class TreeCollectorTest { private static final TreePath ROOT = new TreePath("root"); private static final TreePath PARENT1 = ROOT.pathByAddingChild("parent1"); @@ -170,18 +169,43 @@ public class TreeCollectorTest { public static final class Slow { private static final IntFunction SLOW = Slow::new; private static final IntFunction FAST = Integer::toString; + private static final Consumer SELECT_MAXIMALS = paths -> { + measureExecutionTime("selectMaximals", () -> TreeUtil.selectMaximals(paths)); + measureExecutionTime("collectRoots", () -> TreeCollector.TreePathRoots.collect(paths)); + measureExecutionTime("collectLeafs", () -> TreeCollector.TreePathLeafs.collect(paths)); + Assert.assertArrayEquals(TreeUtil.selectMaximals(paths), TreePathUtil.toTreePathArray(TreeCollector.TreePathRoots.collect(paths))); + }; + private static final Consumer FIND_COMMON_PATH = paths -> { + measureExecutionTime("findCommonPath", () -> TreeUtil.findCommonPath(paths)); + measureExecutionTime("findCommonAncestor", () -> TreePathUtil.findCommonAncestor(paths)); + Assert.assertEquals(TreePathUtil.findCommonAncestor(paths), TreePathUtil.findCommonAncestor(paths)); + }; // manual performance test public static void main(String[] args) { - testPaths(FAST, 7, 7); - testPaths(FAST, 1, 7); - testPaths(SLOW, 1, 1); - testPaths(SLOW, 1, 2); - testPaths(SLOW, 1, 3); - testPaths(SLOW, 1, 4); - testPaths(SLOW, 2, 4); - testPaths(SLOW, 3, 4); - testPaths(SLOW, 4, 4); + testPaths(FIND_COMMON_PATH, FAST, 10, 10); + testPaths(FIND_COMMON_PATH, FAST, 1, 10); + testPaths(FIND_COMMON_PATH, SLOW, 1, 1); + testPaths(FIND_COMMON_PATH, SLOW, 1, 2); + testPaths(FIND_COMMON_PATH, SLOW, 1, 3); + testPaths(FIND_COMMON_PATH, SLOW, 1, 4); + testPaths(FIND_COMMON_PATH, SLOW, 1, 5); + testPaths(FIND_COMMON_PATH, SLOW, 1, 6); + testPaths(FIND_COMMON_PATH, SLOW, 2, 6); + testPaths(FIND_COMMON_PATH, SLOW, 3, 6); + testPaths(FIND_COMMON_PATH, SLOW, 4, 6); + testPaths(FIND_COMMON_PATH, SLOW, 5, 6); + testPaths(FIND_COMMON_PATH, SLOW, 6, 6); + + testPaths(SELECT_MAXIMALS, FAST, 7, 7); + testPaths(SELECT_MAXIMALS, FAST, 1, 7); + testPaths(SELECT_MAXIMALS, SLOW, 1, 1); + testPaths(SELECT_MAXIMALS, SLOW, 1, 2); + testPaths(SELECT_MAXIMALS, SLOW, 1, 3); + testPaths(SELECT_MAXIMALS, SLOW, 1, 4); + testPaths(SELECT_MAXIMALS, SLOW, 2, 4); + testPaths(SELECT_MAXIMALS, SLOW, 3, 4); + testPaths(SELECT_MAXIMALS, SLOW, 4, 4); } private static void addPaths(IntFunction function, int min, int max, List list, TreePath path) { @@ -194,25 +218,23 @@ public class TreeCollectorTest { } } - private static void testPaths(IntFunction function, int min, int max) { + private static void testPaths(Consumer consumer, IntFunction function, int min, int max) { List list = new ArrayList<>(); addPaths(function, min, max, list, null); System.err.println(); System.err.println(list.size() + " paths with count from " + min + " to " + max); - testPaths(list, "1. ordered"); + System.err.println("1. ordered list:"); + consumer.accept(TreePathUtil.toTreePathArray(list)); Collections.reverse(list); - testPaths(list, "2. reversed"); + System.err.println("2. reversed list:"); + consumer.accept(TreePathUtil.toTreePathArray(list)); Collections.shuffle(list); - testPaths(list, "3. shuffled"); + System.err.println("3. shuffled list:"); + consumer.accept(TreePathUtil.toTreePathArray(list)); } - private static void testPaths(List list, String description) { - TreePath[] paths = TreePathUtil.toTreePathArray(list); - System.err.println(description + " list:"); - System.err.println(String.format("%,12d ms to selectMaximals", measureExecutionTime(() -> selectMaximals(paths)))); - System.err.println(String.format("%,12d ms to collectRoots", measureExecutionTime(() -> TreeCollector.TreePathRoots.collect(paths)))); - System.err.println(String.format("%,12d ms to collectLeafs", measureExecutionTime(() -> TreeCollector.TreePathLeafs.collect(paths)))); - Assert.assertArrayEquals(selectMaximals(paths), TreePathUtil.toTreePathArray(TreeCollector.TreePathRoots.collect(paths))); + private static void measureExecutionTime(String method, Runnable runnable) { + System.err.println(String.format("%,12d ms to " + method, TimeoutUtil.measureExecutionTime(runnable::run))); } @@ -234,7 +256,7 @@ public class TreeCollectorTest { @Override public boolean equals(Object object) { - sleep(1); // simulate slow comparison + TimeoutUtil.sleep(1); // simulate slow comparison return object instanceof Slow && ((Slow)object).id == id; } } diff --git a/platform/platform-tests/testSrc/com/intellij/ui/tree/TreePathUtilTest.java b/platform/platform-tests/testSrc/com/intellij/ui/tree/TreePathUtilTest.java index 8b73651b0bf5..f5e03e5cb9a3 100644 --- a/platform/platform-tests/testSrc/com/intellij/ui/tree/TreePathUtilTest.java +++ b/platform/platform-tests/testSrc/com/intellij/ui/tree/TreePathUtilTest.java @@ -210,4 +210,42 @@ public class TreePathUtilTest { private static void makeDeepTreeFromNode(TreeNode node, int count) { while (1 < count) node = node(--count, node); } + + @Test + public void findCommonAncestor() { + TreePath path = TreePathUtil.convertArrayToTreePath("root", "parent"); + Assert.assertEquals(path, TreePathUtil.findCommonAncestor( + TreePathUtil.convertArrayToTreePath("root", "parent", "one", "child", "one"), + TreePathUtil.convertArrayToTreePath("root", "parent", "one", "child", "two"), + TreePathUtil.convertArrayToTreePath("root", "parent", "two", "child", "one"), + TreePathUtil.convertArrayToTreePath("root", "parent", "two", "child", "two"))); + } + + @Test + public void findCommonAncestorWithParent() { + TreePath path = TreePathUtil.convertArrayToTreePath("root", "parent", "child"); + Assert.assertEquals(path, TreePathUtil.findCommonAncestor( + TreePathUtil.convertArrayToTreePath("root", "parent", "child", "one"), + TreePathUtil.convertArrayToTreePath("root", "parent", "child", "two"), + path)); + } + + @Test + public void findCommonAncestorSingle() { + TreePath path = TreePathUtil.convertArrayToTreePath("root", "parent", "child"); + Assert.assertEquals(path, TreePathUtil.findCommonAncestor(path)); + } + + @Test + public void findCommonAncestorEmpty() { + Assert.assertNull(TreePathUtil.findCommonAncestor()); + Assert.assertNull(TreePathUtil.findCommonAncestor((TreePath)null)); + } + + @Test + public void findCommonAncestorWrong() { + Assert.assertNull(TreePathUtil.findCommonAncestor( + TreePathUtil.convertArrayToTreePath("one", "root"), + TreePathUtil.convertArrayToTreePath("two", "root"))); + } }