Skip to content

Implement Xprint-diff without external libraries. #1417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ object DottyBuild extends Build {
"org.scala-lang.modules" %% "scala-partest" % "1.0.11" % "test",
"ch.epfl.lamp" % "dottydoc-client" % "0.1-SNAPSHOT",
"com.novocode" % "junit-interface" % "0.11" % "test",
"com.googlecode.java-diff-utils" % "diffutils" % "1.3.0",
"com.github.spullara.mustache.java" % "compiler" % "0.9.3",
"com.typesafe.sbt" % "sbt-interface" % sbtVersion.value),
// enable improved incremental compilation algorithm
Expand Down
130 changes: 105 additions & 25 deletions src/dotty/tools/dotc/util/DiffUtil.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dotty.tools.dotc.util

import scala.annotation.tailrec
import difflib._
import scala.collection.mutable

object DiffUtil {

Expand All @@ -13,9 +13,8 @@ object DiffUtil {
private final val ADDITION_COLOR = ANSI_GREEN

def mkColoredCodeDiff(code: String, lastCode: String, printDiffDel: Boolean): String = {
import scala.collection.JavaConversions._

@tailrec def split(str: String, acc: List[String]): List[String] = {
@tailrec def splitTokens(str: String, acc: List[String] = Nil): List[String] = {
if (str == "") {
acc.reverse
} else {
Expand All @@ -30,38 +29,119 @@ object DiffUtil {
!Character.isMirrored(c) && !Character.isWhitespace(c)
}
}
split(rest, token :: acc)
splitTokens(rest, token :: acc)
}
}

val lines = split(code, Nil).toArray
val diff = DiffUtils.diff(split(lastCode, Nil), lines.toList)
val tokens = splitTokens(code, Nil).toArray
val lastTokens = splitTokens(lastCode, Nil).toArray

for (delta <- diff.getDeltas) {
val pos = delta.getRevised.getPosition
val endPos = pos + delta.getRevised.getLines.size - 1
val diff = hirschberg(lastTokens, tokens)

delta.getType.toString match { // Issue #1355 forces us to use the toString
case "INSERT" =>
lines(pos) = ADDITION_COLOR + lines(pos)
lines(endPos) = lines(endPos) + ANSI_DEFAULT
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
}

case "CHANGE" =>
val old = if (!printDiffDel) "" else
DELETION_COLOR + delta.getOriginal.getLines.mkString + ANSI_DEFAULT
lines(pos) = old + ADDITION_COLOR + lines(pos)
lines(endPos) = lines(endPos) + ANSI_DEFAULT
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

case "DELETE" if printDiffDel =>
val deleted = delta.getOriginal.getLines.mkString
if (!deleted.forall(Character.isWhitespace)) {
lines(pos) = DELETION_COLOR + deleted + ANSI_DEFAULT + lines(pos)
}
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)

case _ =>
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

lines.mkString
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
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use Array.tabulate rather than Array.fill?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to remain as close as possible to the pseudocode of the algorithm. I will change it now to save up two small traversals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented


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
}

}