Skip to content

Commit c3c796f

Browse files
committed
Fix #1405: Implement Xprint-diff without external libraries.
1 parent 0e8f05d commit c3c796f

File tree

2 files changed

+105
-26
lines changed

2 files changed

+105
-26
lines changed

project/Build.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ object DottyBuild extends Build {
104104
"org.scala-lang.modules" %% "scala-partest" % "1.0.11" % "test",
105105
"ch.epfl.lamp" % "dottydoc-client" % "0.1-SNAPSHOT",
106106
"com.novocode" % "junit-interface" % "0.11" % "test",
107-
"com.googlecode.java-diff-utils" % "diffutils" % "1.3.0",
108107
"com.github.spullara.mustache.java" % "compiler" % "0.9.3",
109108
"com.typesafe.sbt" % "sbt-interface" % sbtVersion.value),
110109
// enable improved incremental compilation algorithm
Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package dotty.tools.dotc.util
22

33
import scala.annotation.tailrec
4-
import difflib._
4+
import scala.collection.mutable
55

66
object DiffUtil {
77

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

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

18-
@tailrec def split(str: String, acc: List[String]): List[String] = {
17+
@tailrec def splitTokens(str: String, acc: List[String] = Nil): List[String] = {
1918
if (str == "") {
2019
acc.reverse
2120
} else {
@@ -30,38 +29,119 @@ object DiffUtil {
3029
!Character.isMirrored(c) && !Character.isWhitespace(c)
3130
}
3231
}
33-
split(rest, token :: acc)
32+
splitTokens(rest, token :: acc)
3433
}
3534
}
3635

37-
val lines = split(code, Nil).toArray
38-
val diff = DiffUtils.diff(split(lastCode, Nil), lines.toList)
36+
val tokens = splitTokens(code, Nil).toArray
37+
val lastTokens = splitTokens(lastCode, Nil).toArray
3938

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

44-
delta.getType.toString match { // Issue #1355 forces us to use the toString
45-
case "INSERT" =>
46-
lines(pos) = ADDITION_COLOR + lines(pos)
47-
lines(endPos) = lines(endPos) + ANSI_DEFAULT
41+
diff.collect {
42+
case Unmodified(str) => str
43+
case Inserted(str) => ADDITION_COLOR + str + ANSI_DEFAULT
44+
case Modified(old, str) if printDiffDel => DELETION_COLOR + old + ADDITION_COLOR + str + ANSI_DEFAULT
45+
case Modified(_, str) => ADDITION_COLOR + str + ANSI_DEFAULT
46+
case Deleted(str) if printDiffDel => DELETION_COLOR + str + ANSI_DEFAULT
47+
}.mkString
48+
}
4849

49-
case "CHANGE" =>
50-
val old = if (!printDiffDel) "" else
51-
DELETION_COLOR + delta.getOriginal.getLines.mkString + ANSI_DEFAULT
52-
lines(pos) = old + ADDITION_COLOR + lines(pos)
53-
lines(endPos) = lines(endPos) + ANSI_DEFAULT
50+
private sealed trait Patch
51+
private final case class Unmodified(str: String) extends Patch
52+
private final case class Modified(original: String, str: String) extends Patch
53+
private final case class Deleted(str: String) extends Patch
54+
private final case class Inserted(str: String) extends Patch
5455

55-
case "DELETE" if printDiffDel =>
56-
val deleted = delta.getOriginal.getLines.mkString
57-
if (!deleted.forall(Character.isWhitespace)) {
58-
lines(pos) = DELETION_COLOR + deleted + ANSI_DEFAULT + lines(pos)
59-
}
56+
private def hirschberg(a: Array[String], b: Array[String]): Array[Patch] = {
57+
def build(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = {
58+
if (x.isEmpty) {
59+
builder += Inserted(y.mkString)
60+
} else if (y.isEmpty) {
61+
builder += Deleted(x.mkString)
62+
} else if (x.length == 1 || y.length == 1) {
63+
needlemanWunsch(x, y, builder)
64+
} else {
65+
val xlen = x.length
66+
val xmid = xlen / 2
67+
val ylen = y.length
68+
69+
val (x1, x2) = x.splitAt(xmid)
70+
val leftScore = nwScore(x1, y)
71+
val rightScore = nwScore(x2.reverse, y.reverse)
72+
val scoreSum = (leftScore zip rightScore.reverse).map {
73+
case (left, right) => left + right
74+
}
75+
val max = scoreSum.max
76+
val ymid = scoreSum.indexOf(max)
6077

61-
case _ =>
78+
val (y1, y2) = y.splitAt(ymid)
79+
build(x1, y1, builder)
80+
build(x2, y2, builder)
6281
}
6382
}
83+
val builder = Array.newBuilder[Patch]
84+
build(a, b, builder)
85+
builder.result()
86+
}
87+
88+
private def nwScore(x: Array[String], y: Array[String]): Array[Int] = {
89+
def ins(s: String) = -2
90+
def del(s: String) = -2
91+
def sub(s1: String, s2: String) = if (s1 == s2) 2 else -1
6492

65-
lines.mkString
93+
val score = Array.fill(x.length + 1, y.length + 1)(0)
94+
for (j <- 1 to y.length)
95+
score(0)(j) = score(0)(j - 1) + ins(y(j - 1))
96+
for (i <- 1 to x.length) {
97+
score(i)(0) = score(i - 1)(0) + del(x(i - 1))
98+
for (j <- 1 to y.length) {
99+
val scoreSub = score(i - 1)(j - 1) + sub(x(i - 1), y(j - 1))
100+
val scoreDel = score(i - 1)(j) + del(x(i - 1))
101+
val scoreIns = score(i)(j - 1) + ins(y(j - 1))
102+
score(i)(j) = scoreSub max scoreDel max scoreIns
103+
}
104+
}
105+
Array.tabulate(y.length + 1)(j => score(x.length)(j))
66106
}
107+
108+
private def needlemanWunsch(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = {
109+
def similarity(a: String, b: String) = if (a == b) 2 else -1
110+
val d = 1
111+
val score = Array.tabulate(x.length + 1, y.length + 1) { (i, j) =>
112+
if (i == 0) d * j
113+
else if (j == 0) d * i
114+
else 0
115+
}
116+
for (i <- 1 to x.length) {
117+
for (j <- 1 to y.length) {
118+
val mtch = score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))
119+
val delete = score(i - 1)(j) + d
120+
val insert = score(i)(j - 1) + d
121+
score(i)(j) = mtch max insert max delete
122+
}
123+
}
124+
125+
var alignment = List.empty[Patch]
126+
var i = x.length
127+
var j = y.length
128+
while (i > 0 || j > 0) {
129+
if (i > 0 && j > 0 && score(i)(j) == score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))) {
130+
val newHead =
131+
if (x(i - 1) == y(j - 1)) Unmodified(x(i - 1))
132+
else Modified(x(i - 1), y(j - 1))
133+
alignment = newHead :: alignment
134+
i = i - 1
135+
j = j - 1
136+
} else if (i > 0 && score(i)(j) == score(i - 1)(j) + d) {
137+
alignment = Deleted(x(i - 1)) :: alignment
138+
i = i - 1
139+
} else {
140+
alignment = Inserted(y(j - 1)) :: alignment
141+
j = j - 1
142+
}
143+
}
144+
builder ++= alignment
145+
}
146+
67147
}

0 commit comments

Comments
 (0)