11package dotty .tools .dotc .util
22
33import scala .annotation .tailrec
4- import difflib . _
4+ import scala . collection . mutable
55
66object 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