Skip to content

Commit 606ae57

Browse files
committed
Fix #1405: Implement Xprint-diff without external libraries.
1 parent 3265323 commit 606ae57

File tree

2 files changed

+107
-27
lines changed

2 files changed

+107
-27
lines changed

project/Build.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ object DottyBuild extends Build {
9999
libraryDependencies ++= Seq("org.scala-lang.modules" %% "scala-xml" % "1.0.1",
100100
"org.scala-lang.modules" %% "scala-partest" % "1.0.11" % "test",
101101
"com.novocode" % "junit-interface" % "0.11" % "test",
102-
"com.googlecode.java-diff-utils" % "diffutils" % "1.3.0",
103102
"com.typesafe.sbt" % "sbt-interface" % sbtVersion.value),
104103
// enable improved incremental compilation algorithm
105104
incOptions := incOptions.value.withNameHashing(true),
@@ -177,7 +176,7 @@ object DottyBuild extends Build {
177176
path = file.getAbsolutePath
178177
} yield "-Xbootclasspath/p:" + path
179178
// dotty itself needs to be in the bootclasspath
180-
val fullpath = ("-Xbootclasspath/p:" + "dotty.jar") :: ("-Xbootclasspath/a:" + bin) :: path.toList
179+
val fullpath = ("-Xbootclasspath/a:" + bin) :: path.toList
181180
// System.err.println("BOOTPATH: " + fullpath)
182181

183182
val travis_build = // propagate if this is a travis build
Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package dotty.tools.dotc.util
22

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

67
object DiffUtil {
78

@@ -13,9 +14,8 @@ object DiffUtil {
1314
private final val ADDITION_COLOR = ANSI_GREEN
1415

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

18-
@tailrec def split(str: String, acc: List[String]): List[String] = {
18+
@tailrec def splitTokens(str: String, acc: List[String] = Nil): List[String] = {
1919
if (str == "") {
2020
acc.reverse
2121
} else {
@@ -30,38 +30,119 @@ object DiffUtil {
3030
!Character.isMirrored(c) && !Character.isWhitespace(c)
3131
}
3232
}
33-
split(rest, token :: acc)
33+
splitTokens(rest, token :: acc)
3434
}
3535
}
3636

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

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

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
42+
diff.collect {
43+
case Unmodified(str) => str
44+
case Inserted(str) => ADDITION_COLOR + str + ANSI_DEFAULT
45+
case Modified(old, str) if printDiffDel => DELETION_COLOR + old + ADDITION_COLOR + str + ANSI_DEFAULT
46+
case Modified(_, str) => ADDITION_COLOR + str + ANSI_DEFAULT
47+
case Deleted(str) if printDiffDel => DELETION_COLOR + str + ANSI_DEFAULT
48+
}.mkString
49+
}
4850

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
51+
private sealed trait Patch
52+
private final case class Unmodified(str: String) extends Patch
53+
private final case class Modified(original: String, str: String) extends Patch
54+
private final case class Deleted(str: String) extends Patch
55+
private final case class Inserted(str: String) extends Patch
5456

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-
}
57+
private def hirschberg(a: Array[String], b: Array[String]): Array[Patch] = {
58+
def build(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = {
59+
if (x.isEmpty) {
60+
builder += Inserted(y.mkString)
61+
} else if (y.isEmpty) {
62+
builder += Deleted(x.mkString)
63+
} else if (x.length == 1 || y.length == 1) {
64+
needlemanWunsch(x, y, builder)
65+
} else {
66+
val xlen = x.length
67+
val xmid = xlen / 2
68+
val ylen = y.length
69+
70+
val (x1, x2) = x.splitAt(xmid)
71+
val leftScore = nwScore(x1, y)
72+
val rightScore = nwScore(x2.reverse, y.reverse)
73+
val scoreSum = (leftScore zip rightScore.reverse).map {
74+
case (left, right) => left + right
75+
}
76+
val max = scoreSum.max
77+
val ymid = scoreSum.indexOf(max)
78+
79+
val (y1, y2) = y.splitAt(ymid)
80+
build(x1, y1, builder)
81+
build(x2, y2, builder)
82+
}
83+
}
84+
val builder = Array.newBuilder[Patch]
85+
build(a, b, builder)
86+
builder.result()
87+
}
6088

61-
case _ =>
89+
private def nwScore(x: Array[String], y: Array[String]): Array[Int] = {
90+
def ins(s: String) = -2
91+
def del(s: String) = -2
92+
def sub(s1: String, s2: String) = if (s1 == s2) 2 else -1
93+
94+
val score = Array.fill(x.length + 1, y.length + 1)(0)
95+
for (j <- 1 to y.length)
96+
score(0)(j) = score(0)(j - 1) + ins(y(j - 1))
97+
for (i <- 1 to x.length) {
98+
score(i)(0) = score(i - 1)(0) + del(x(i - 1))
99+
for (j <- 1 to y.length) {
100+
val scoreSub = score(i - 1)(j - 1) + sub(x(i - 1), y(j - 1))
101+
val scoreDel = score(i - 1)(j) + del(x(i - 1))
102+
val scoreIns = score(i)(j - 1) + ins(y(j - 1))
103+
score(i)(j) = scoreSub max scoreDel max scoreIns
104+
}
105+
}
106+
Array.tabulate(y.length + 1)(j => score(x.length)(j))
107+
}
108+
109+
private def needlemanWunsch(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = {
110+
def similarity(a: String, b: String) = if (a == b) 2 else -1
111+
val d = 1
112+
val score = Array.tabulate(x.length + 1, y.length + 1) { (i, j) =>
113+
if (i == 0) d * j
114+
else if (j == 0) d * i
115+
else 0
116+
}
117+
for (i <- 1 to x.length) {
118+
for (j <- 1 to y.length) {
119+
val mtch = score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))
120+
val delete = score(i - 1)(j) + d
121+
val insert = score(i)(j - 1) + d
122+
score(i)(j) = mtch max insert max delete
62123
}
63124
}
64125

65-
lines.mkString
126+
var alignment = List.empty[Patch]
127+
var i = x.length
128+
var j = y.length
129+
while (i > 0 || j > 0) {
130+
if (i > 0 && j > 0 && score(i)(j) == score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))) {
131+
val newHead =
132+
if (x(i - 1) == y(j - 1)) Unmodified(x(i - 1))
133+
else Modified(x(i - 1), y(j - 1))
134+
alignment = newHead :: alignment
135+
i = i - 1
136+
j = j - 1
137+
} else if (i > 0 && score(i)(j) == score(i - 1)(j) + d) {
138+
alignment = Deleted(x(i - 1)) :: alignment
139+
i = i - 1
140+
} else {
141+
alignment = Inserted(y(j - 1)) :: alignment
142+
j = j - 1
143+
}
144+
}
145+
builder ++= alignment
66146
}
147+
67148
}

0 commit comments

Comments
 (0)