aboutsummaryrefslogblamecommitdiff
path: root/src/dotty/tools/dotc/util/DiffUtil.scala
blob: 8bb39c88aa4c3cfcec6947ae4850077de00b56d0 (plain) (tree)
1
2
3
4
5
6
7
8
9
10


                               
                               





                                              


                                               
 
                                                                                          













                                                                                          
                                       


       























                                                                                          

                                                       
 
                                             
 







                                                                                                           
 




                                                                                
 




















                                                                                                 
 


                                      

       








                                                                         
 












                                                                    
   







































                                                                                                                 
 
package dotty.tools.dotc.util

import scala.annotation.tailrec
import scala.collection.mutable

object DiffUtil {

  private final val ANSI_DEFAULT = "\u001B[0m"
  private final val ANSI_RED = "\u001B[31m"
  private final val ANSI_GREEN = "\u001B[32m"

  private final val DELETION_COLOR = ANSI_RED
  private final val ADDITION_COLOR = ANSI_GREEN

  @tailrec private def splitTokens(str: String, acc: List[String] = Nil): List[String] = {
      if (str == "") {
        acc.reverse
      } else {
        val head = str.charAt(0)
        val (token, rest) = if (Character.isAlphabetic(head) || Character.isDigit(head)) {
          str.span(c => Character.isAlphabetic(c) || Character.isDigit(c))
        } else if (Character.isMirrored(head) || Character.isWhitespace(head)) {
          str.splitAt(1)
        } else {
          str.span { c =>
            !Character.isAlphabetic(c) && !Character.isDigit(c) &&
              !Character.isMirrored(c) && !Character.isWhitespace(c)
          }
        }
        splitTokens(rest, token :: acc)
      }
    }


  /** @return a tuple of the (found, expected) diffs as strings */
  def mkColoredTypeDiff(found: String, expected: String): (String, String) = {
    val foundTokens   = splitTokens(found, Nil).toArray
    val expectedTokens = splitTokens(expected, Nil).toArray

    val diffExp = hirschberg(foundTokens, expectedTokens)
    val diffAct = hirschberg(expectedTokens, foundTokens)

    val exp = diffExp.collect {
      case Unmodified(str) => str
      case Inserted(str) => ADDITION_COLOR + str + ANSI_DEFAULT
    }.mkString

    val fnd = diffAct.collect {
      case Unmodified(str) => str
      case Inserted(str) => DELETION_COLOR + str + ANSI_DEFAULT
    }.mkString

    (fnd, exp)
  }

  def mkColoredCodeDiff(code: String, lastCode: String, printDiffDel: Boolean): String = {

    val tokens = splitTokens(code, Nil).toArray
    val lastTokens = splitTokens(lastCode, Nil).toArray

    val diff = hirschberg(lastTokens, tokens)

    diff.collect {
      case Unmodified(str) => str
      case Inserted(str)                      => ADDITION_COLOR + str + ANSI_DEFAULT
      case Modified(old, str) if printDiffDel => DELETION_COLOR + old + ADDITION_COLOR + str + ANSI_DEFAULT
      case Modified(_, str)                   => ADDITION_COLOR + str + ANSI_DEFAULT
      case Deleted(str) if printDiffDel       => DELETION_COLOR + str + ANSI_DEFAULT
    }.mkString
  }

  private sealed trait Patch
  private final case class Unmodified(str: String) extends Patch
  private final case class Modified(original: String, str: String) extends Patch
  private final case class Deleted(str: String) extends Patch
  private final case class Inserted(str: String) extends Patch

  private def hirschberg(a: Array[String], b: Array[String]): Array[Patch] = {
    def build(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = {
      if (x.isEmpty) {
        builder += Inserted(y.mkString)
      } else if (y.isEmpty) {
        builder += Deleted(x.mkString)
      } else if (x.length == 1 || y.length == 1) {
        needlemanWunsch(x, y, builder)
      } else {
        val xlen = x.length
        val xmid = xlen / 2
        val ylen = y.length

        val (x1, x2) = x.splitAt(xmid)
        val leftScore = nwScore(x1, y)
        val rightScore = nwScore(x2.reverse, y.reverse)
        val scoreSum = (leftScore zip rightScore.reverse).map {
          case (left, right) => left + right
        }
        val max = scoreSum.max
        val ymid = scoreSum.indexOf(max)

        val (y1, y2) = y.splitAt(ymid)
        build(x1, y1, builder)
        build(x2, y2, builder)
      }
    }
    val builder = Array.newBuilder[Patch]
    build(a, b, builder)
    builder.result()
  }

  private def nwScore(x: Array[String], y: Array[String]): Array[Int] = {
    def ins(s: String) = -2
    def del(s: String) = -2
    def sub(s1: String, s2: String) = if (s1 == s2) 2 else -1

    val score = Array.fill(x.length + 1, y.length + 1)(0)
    for (j <- 1 to y.length)
      score(0)(j) = score(0)(j - 1) + ins(y(j - 1))
    for (i <- 1 to x.length) {
      score(i)(0) = score(i - 1)(0) + del(x(i - 1))
      for (j <- 1 to y.length) {
        val scoreSub = score(i - 1)(j - 1) + sub(x(i - 1), y(j - 1))
        val scoreDel = score(i - 1)(j) + del(x(i - 1))
        val scoreIns = score(i)(j - 1) + ins(y(j - 1))
        score(i)(j) = scoreSub max scoreDel max scoreIns
      }
    }
    Array.tabulate(y.length + 1)(j => score(x.length)(j))
  }

  private def needlemanWunsch(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = {
    def similarity(a: String, b: String) = if (a == b) 2 else -1
    val d = 1
    val score = Array.tabulate(x.length + 1, y.length + 1) { (i, j) =>
      if (i == 0) d * j
      else if (j == 0) d * i
      else 0
    }
    for (i <- 1 to x.length) {
      for (j <- 1 to y.length) {
        val mtch = score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))
        val delete = score(i - 1)(j) + d
        val insert = score(i)(j - 1) + d
        score(i)(j) = mtch max insert max delete
      }
    }

    var alignment = List.empty[Patch]
    var i = x.length
    var j = y.length
    while (i > 0 || j > 0) {
      if (i > 0 && j > 0 && score(i)(j) == score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))) {
        val newHead =
          if (x(i - 1) == y(j - 1)) Unmodified(x(i - 1))
          else Modified(x(i - 1), y(j - 1))
        alignment = newHead :: alignment
        i = i - 1
        j = j - 1
      } else if (i > 0 && score(i)(j) == score(i - 1)(j) + d) {
        alignment = Deleted(x(i - 1)) :: alignment
        i = i - 1
      } else {
        alignment = Inserted(y(j - 1)) :: alignment
        j = j - 1
      }
    }
    builder ++= alignment
  }

}