diff --git a/core/shared/src/main/scala/coursier/util/Tree.scala b/core/shared/src/main/scala/coursier/util/Tree.scala index a2a985641..1947b6c34 100644 --- a/core/shared/src/main/scala/coursier/util/Tree.scala +++ b/core/shared/src/main/scala/coursier/util/Tree.scala @@ -1,29 +1,56 @@ package coursier.util +import scala.collection.mutable import scala.collection.mutable.ArrayBuffer object Tree { def apply[T](roots: IndexedSeq[T])(children: T => Seq[T], print: T => String): String = { + /** + * Add elements to the stack + * @param stack a mutable stack which will have elements + * @param elems elements to add + * @param isLast a list that contains whether an element is the last in its siblings or not. + * The first element is the deepest, due to the fact prepending a List is faster than appending + */ + def push(stack: mutable.Stack[(T, List[Boolean])], elems: Seq[T], isLast: List[Boolean]) = { + // Reverse the list because the stack is LIFO but elems must be shown in the order + for ((x, idx) <- elems.zipWithIndex.reverse) { + stack.push((x, (idx == elems.length - 1) :: isLast)) + } + } - def helper(elems: Seq[T], prefix: String, acc: String => Unit): Unit = - for ((elem, idx) <- elems.zipWithIndex) { - val isLast = idx == elems.length - 1 + def prefix(isLast: List[Boolean]): String = { + // Reverse the list because its first element is the deepest element + isLast.reverse.zipWithIndex.map { + case (last, idx) => + if (idx == isLast.length - 1) + if (last) "└─ " else "├─ " + else + if (last) " " else "| " + }.mkString("") + } - val tee = if (isLast) "└─ " else "├─ " + // Depth-first traverse + def helper(elems: Seq[T], acc: String => Unit): Unit = { + val stack = new mutable.Stack[(T, List[Boolean])]() + val seen = new mutable.HashSet[T]() - acc(prefix + tee + print(elem)) + push(stack, elems, List[Boolean]()) - val children0 = children(elem) + while (stack.nonEmpty) { + val (elem, isLast) = stack.pop() + acc(prefix(isLast) + print(elem)) - if (children0.nonEmpty) { - val extraPrefix = if (isLast) " " else "| " - helper(children0, prefix + extraPrefix, acc) + if (! seen.contains(elem)) { + push(stack, children(elem), isLast) + seen.add(elem) } } + } val b = new ArrayBuffer[String] - helper(roots, "", b += _) + helper(roots, b += _) b.mkString("\n") } diff --git a/tests/shared/src/test/scala/coursier/test/TreeTests.scala b/tests/shared/src/test/scala/coursier/test/TreeTests.scala new file mode 100644 index 000000000..9b518a79e --- /dev/null +++ b/tests/shared/src/test/scala/coursier/test/TreeTests.scala @@ -0,0 +1,35 @@ +package coursier +package test + +import coursier.util.{Tree, Xml} +import utest._ + +object TreeTests extends TestSuite { + private def tree(str: String, xs: Array[Xml.Node] = Array()): Xml.Node = { + new Xml.Node { + override def label: String = str + override def children = xs + + override def attributes: Seq[(String, String, String)] = Array[(String, String, String)]() + override def isText: Boolean = false + override def textContent: String = "" + override def isElement: Boolean = true + } + } + + val roots = Array( + tree("p1", Array(tree("c1"), tree("c2"))), + tree("p2", Array(tree("c3"), tree("c4")))) + + val tests = TestSuite { + 'apply { + val str = Tree(roots)(_.children, _.label) + assert(str == """├─ p1 + || ├─ c1 + || └─ c2 + |└─ p2 + | ├─ c3 + | └─ c4""".stripMargin) + } + } +}