All Downloads are FREE. Search and download functionalities are using the official Maven repository.

quasar.RenderTree.scala Maven / Gradle / Ivy

There is a newer version: 28.1.6
Show newest version
/*
 * Copyright 2014–2016 SlamData Inc.
 *
 * 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 quasar

import quasar.Predef._
import quasar.fp._

import java.lang.{Object, Runnable}
import scala.Any

import argonaut._, Argonaut._
import matryoshka._, Recursive.ops._
import scalaz._, Scalaz._
import simulacrum.typeclass

final case class RenderedTree(nodeType: List[String], label: Option[String], children: List[RenderedTree]) {
  def simpleType: Option[String] = nodeType.headOption

  def relabel(f: String => String) = this.copy(label = label.map(f))
  def retype(f: List[String] => List[String]) = this.copy(nodeType = f(nodeType))

  /**
   A tree that describes differences between two trees:
   - If the two trees are identical, the result is the same as (either) input.
   - If the trees differ only in the labels on nodes, then the result has those
      nodes decorated with "[Changed] old -> new".
   - If a single node is unmatched on either side, it is decorated with "[Added]"
      or "[Deleted]".
   As soon as a difference is found and decorated, the subtree(s) beneath the
   decorated nodes are not inspected.

   Node types are not compared or necessarily preserved.
   */
  def diff(that: RenderedTree): RenderedTree = {
    def prefixedType(t: RenderedTree, p: String): List[String] = t.nodeType match {
      case first :: rest => (p + " " + first) :: rest
      case Nil           => p :: Nil
    }

    def prefixType(t: RenderedTree, p: String): RenderedTree = t.copy(nodeType = prefixedType(t, p))
    val deleted = ">>>"
    val added = "<<<"

    (this, that) match {
      case (RenderedTree(nodeType1, l1, children1), RenderedTree(nodeType2, l2, children2)) => {
        if (nodeType1 != nodeType2 || l1 != l2)
          RenderedTree(List("[Root differs]"), None,
            prefixType(this, deleted) ::
            prefixType(that, added) ::
            Nil)
        else {
          def matchChildren(children1: List[RenderedTree], children2: List[RenderedTree]): List[RenderedTree] = (children1, children2) match {
            case (Nil, Nil)     => Nil
            case (x :: xs, Nil) => prefixType(x, deleted) :: matchChildren(xs, Nil)
            case (Nil, x :: xs) => prefixType(x, added) :: matchChildren(Nil, xs)

            case (a :: as, b :: bs)        if a.typeAndLabel == b.typeAndLabel  => a.diff(b) :: matchChildren(as, bs)
            case (a1 :: a2 :: as, b :: bs) if a2.typeAndLabel == b.typeAndLabel => prefixType(a1, deleted) :: a2.diff(b) :: matchChildren(as, bs)
            case (a :: as, b1 :: b2 :: bs) if a.typeAndLabel == b2.typeAndLabel => prefixType(b1, added) :: a.diff(b2) :: matchChildren(as, bs)

            case (a :: as, b :: bs) => prefixType(a, deleted) :: prefixType(b, added) :: matchChildren(as, bs)
          }
          RenderedTree(nodeType1, l1, matchChildren(children1, children2))
        }
      }
    }
  }

  /**
  A 2D String representation of this Tree, separated into lines. Based on
  scalaz Tree's show, but improved to use a single line per node, use
  unicode box-drawing glyphs, and to handle newlines in the rendered
  nodes.
  */
  def draw: Stream[String] = {
    def drawSubTrees(s: List[RenderedTree]): Stream[String] = s match {
      case Nil      => Stream.Empty
      case t :: Nil => shift("╰─ ", "   ", t.draw)
      case t :: ts  => shift("├─ ", "│  ", t.draw) append drawSubTrees(ts)
    }
    def shift(first: String, other: String, s: Stream[String]): Stream[String] =
      (first #:: Stream.continually(other)).zip(s).map {
        case (a, b) => a + b
      }
    def mapParts[A, B](as: Stream[A])(f: (A, Boolean, Boolean) => B): Stream[B] = {
      def loop(as: Stream[A], first: Boolean): Stream[B] =
        if (as.isEmpty)           Stream.empty
        else if (as.tail.isEmpty) f(as.head, first, true) #:: Stream.empty
        else                      f(as.head, first, false) #:: loop(as.tail, false)
      loop(as, true)
    }

    val (prefix, body, suffix) = (simpleType, label) match {
      case (None,             None)        => ("", "", "")
      case (None,             Some(label)) => ("", label, "")
      case (Some(simpleType), None)        => ("", simpleType, "")
      case (Some(simpleType), Some(label)) => (simpleType + "(",  label, ")")
    }
    val indent = " " * (prefix.length-2)
    val lines = body.split("\n").toStream
    mapParts(lines) { (a, first, last) =>
      val pre = if (first) prefix else indent
      val suf = if (last) suffix else ""
      pre + a + suf
    } ++ drawSubTrees(children)
  }

  private def typeAndLabel: String = (simpleType, label) match {
    case (None,             None)        => ""
    case (None,             Some(label)) => label
    case (Some(simpleType), None)        => simpleType
    case (Some(simpleType), Some(label)) => simpleType + "(" + label + ")"
  }
}
object RenderedTree {
  implicit val RenderedTreeShow: Show[RenderedTree] = new Show[RenderedTree] {
    override def show(t: RenderedTree) = t.draw.mkString("\n")
  }

  implicit val RenderedTreeEncodeJson: EncodeJson[RenderedTree] = EncodeJson {
    case RenderedTree(nodeType, label, children) =>
      Json.obj((
        (nodeType match {
          case Nil => None
          case _   => Some("type" := nodeType.reverse.mkString("/"))
        }) ::
          Some("label" := label) ::
          {
            if (children.empty) None
            else Some("children" := children.map(RenderedTreeEncodeJson.encode(_)))
          } ::
          Nil).foldMap(_.toList): _*)
  }
}
object Terminal {
  def apply(nodeType: List[String], label: Option[String]): RenderedTree = RenderedTree(nodeType, label, Nil)
}
object NonTerminal {
  def apply(nodeType: List[String], label: Option[String], children: List[RenderedTree]): RenderedTree = RenderedTree(nodeType, label, children)
}

@typeclass trait RenderTree[A] {
  def render(a: A): RenderedTree
}
object RenderTree extends RenderTreeInstances {
  import RenderTree.ops._

  def fromToString[A](simpleType: String) = new RenderTree[A] {
    val nodeType = simpleType :: Nil
    def render(v: A) = Terminal(nodeType, Some(v.toString))
  }

  /** For use with `<|`, mostly. */
  def print[A: RenderTree](label: String, a: A): Unit = println(label + ":\n" + a.show)

  def showGraphviz[A](a: A)(implicit RA: RenderTree[A]): Cord = {
    def nodeName: State[Int, String] =
      for {
        i <- get
        _ <- put(i+1)
      } yield "n" + i

    final case class Node(name: String, dot: Cord)

    def render(t: RenderedTree): State[Int, Node] = {
      def escape(str: String) = str.replace("\\\\", "\\\\").replace("\"", "\\\"")
      def escapeHtml(str: String) = str.replace("&", "&").replace("\"", """).replace("<", "<").replace(">", ">")

      def decl(name: String) = {
        val formatted = t.nodeType match {
          case Nil => "\"" + escape(t.label.toString) + "\""
          case _ => "<" + "" + escapeHtml(t.nodeType.mkString("/")) + "
" + escapeHtml(t.label.toString) + ">" } Cord(" ") ++ name ++ "[label=" ++ formatted ++ "];\n" } for { n <- nodeName cc <- t match { case RenderedTree(_, _, Nil) => state[Int, Cord](Cord("")) case RenderedTree(_, _, children) => { for { nodes <- children.traverse(render(_)) } yield nodes.map(cn => Cord(" ") ++ n ++ " -> " ++ cn.name ++ ";\n" ++ cn.dot).concatenate } } } yield Node(n, decl(n) ++ cc) } val tree = RA.render(a) val program = for { foo <- render(tree) } yield Cord("digraph G {\n") ++ foo.dot ++ Cord("}") program.eval(0) } /** (Effectfully) adds the given object(s) to a crude UI which shows them as trees that can be interactively explored. Can be used with `unsafeTap` aka `<|` to capture a value from the middle of some expression. */ def showSwing[A](as: A*)(implicit RA: RenderTree[A]): Unit = { import javax.swing._ import java.awt.event._ import javax.swing.tree._ val roots = as.toList.map { a => RA.render(a) } trait Node { def children: List[TreeNode] } final case object RootNode extends Node { val children = roots.map(new TreeNode(_)) } // Not a case class, because JTree gets confused if there are multiple // equal nodes, but yes, that's a serious drag. class TreeNode(val t: RenderedTree) extends Node { val children = t.children.map(new TreeNode(_)) override def toString = t.label.getOrElse("") } class RenderedTreeModel(roots: List[RenderedTree]) extends TreeModel { def addTreeModelListener(l: javax.swing.event.TreeModelListener): Unit = () def getChild(parent: Any, index: Int): Object = children(parent)(index) def getChildCount(parent: Any): Int = children(parent).length def getIndexOfChild(parent: Any, child: Any): Int = children(parent).indexOf(child) def getRoot(): Object = RootNode def isLeaf(node: Any): Boolean = children(node).isEmpty def removeTreeModelListener(l: javax.swing.event.TreeModelListener): Unit = () def valueForPathChanged(path: javax.swing.tree.TreePath, newValue: Any): Unit = () private def children(node: Any): List[TreeNode] = node match { case n: Node => n.children case _ => Nil } } class RenderedTreeCellRenderer extends DefaultTreeCellRenderer { override def getTreeCellRendererComponent(tree: JTree, value: Any, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean): java.awt.Component = { val comp = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus) (value, comp) match { case (n: TreeNode, label: JLabel) => val typ = n.t.nodeType.lastOption.getOrElse("") label.setText(s"${typ} (${n.t.label})") } comp } } implicit def toActionListener(l: ActionEvent => Unit): ActionListener = new ActionListener { def actionPerformed(evt: ActionEvent): Unit = l(evt) } val m = new RenderedTreeModel(roots) val jt = new JTree() jt.setModel(m) jt.setCellRenderer(new RenderedTreeCellRenderer) jt.setRootVisible(false) jt.setShowsRootHandles(true) val sc = new JScrollPane(jt) // Dumb search field at the top of the frame. Hit enter to search. val s = new JTextField(10) s.addActionListener((evt: ActionEvent) => { val pattern = s.getText.trim implicit def toTreePath(l: List[TreeNode]): TreePath = new TreePath((m.getRoot :: l).toArray) def paths: List[List[TreeNode]] = { def loop(node: Any): List[List[TreeNode]] = { for { i <- (0 until m.getChildCount(node)).toList child = m.getChild(node, i).asInstanceOf[TreeNode] p <- (List() :: loop(child)).map(child :: _) } yield p } loop(m.getRoot) } val matches = paths.filter(path => { def strMatch(str: String): Boolean = str.toLowerCase.contains(pattern.toLowerCase) path.lastOption.fold(false) { node => strMatch(node.t.label.getOrElse("")) || node.t.simpleType.map(strMatch).getOrElse(false) } }) for (p <- paths.reverse) { jt.collapsePath(p) jt.removeSelectionPath(p) } for (p <- matches) { jt.makeVisible(p) jt.addSelectionPath(p) } }) val sl = new JLabel("Search:") val sp = new JPanel() ignore(sp.add(sl)) ignore(sp.add(s)) val f = new JFrame("RenderedTree - " + new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new java.util.Date())) f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) f.setSize(400, 600) val count = windowCount.incrementAndGet f.setLocation(count*20, count*20) f.getContentPane().add(sp, "North") f.getContentPane().add(sc, "Center") java.awt.EventQueue.invokeLater(new Runnable { def run = { f.setVisible(true) }}) } val windowCount = new java.util.concurrent.atomic.AtomicInteger() implicit def ntRenderTree[F[_], A: RenderTree]( implicit F: RenderTree ~> (RenderTree ∘ F)#λ): RenderTree[F[A]] = F(RenderTree[A]) implicit def fixRenderTree[F[_]](implicit RF: RenderTree ~> λ[α => RenderTree[F[α]]]): RenderTree[Fix[F]] = new RenderTree[Fix[F]] { def render(v: Fix[F]) = RF(fixRenderTree[F]).render(v.unFix).retype { case h :: t => ("Fix:" + h) :: t case Nil => "Fix" :: Nil } } implicit def cofreeRenderTree[F[_], A: RenderTree](implicit RF: RenderTree ~> λ[α => RenderTree[F[α]]]): RenderTree[Cofree[F, A]] = new RenderTree[Cofree[F, A]] { def render(t: Cofree[F, A]) = { NonTerminal(List("Cofree"), None, List(t.head.render, RF(cofreeRenderTree[F, A]).render(t.tail))) } } } sealed abstract class RenderTreeInstances { implicit val renderTreeUnit = new RenderTree[Unit] { def render(v: Unit) = Terminal(List("()", "Unit"), None) } implicit def recursiveRenderTree[T[_[_]]: Recursive, F[_]: Functor]( implicit F: RenderTree ~> (RenderTree ∘ F)#λ): RenderTree[T[F]] = new RenderTree[T[F]] { def render(v: T[F]) = F(recursiveRenderTree[T, F]).render(v.project) } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy