Skip to content

Commit 5ec8a18

Browse files
committed
Add text-diff functionality
1 parent 2aaa120 commit 5ec8a18

File tree

3 files changed

+147
-33
lines changed

3 files changed

+147
-33
lines changed

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal data class Node(
3434

3535
companion object {
3636
fun getNodeFields(): List<String> {
37-
return listOf("props", "state", "rendering")
37+
return listOf("Props", "State", "Rendering")
3838
}
3939

4040
fun getNodeData(node: Node, field: String): String {

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,12 @@ private fun DetailCard(
163163
text = label,
164164
style = MaterialTheme.typography.h6,
165165
color = Color.Black,
166-
fontWeight = FontWeight.Bold
166+
fontWeight = FontWeight.Bold,
167167
)
168168
if (!open) {
169169
return@Card
170170
}
171171

172-
Spacer(modifier = Modifier.height(4.dp))
173172
if (pastValue != null) {
174173
Column {
175174
Text(
@@ -180,7 +179,10 @@ private fun DetailCard(
180179
)
181180
Text(
182181
text = computeAnnotatedDiff(pastValue, currValue),
183-
style = MaterialTheme.typography.body2
182+
style = MaterialTheme.typography.body2,
183+
modifier = Modifier
184+
.padding(top = 8.dp)
185+
.align(Alignment.CenterHorizontally)
184186
)
185187

186188
Spacer(modifier = Modifier.height(16.dp))

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/DiffUtils.kt

Lines changed: 141 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,171 @@ import androidx.compose.ui.graphics.Color
44
import androidx.compose.ui.text.AnnotatedString
55
import androidx.compose.ui.text.SpanStyle
66
import androidx.compose.ui.text.buildAnnotatedString
7-
import com.github.difflib.text.DiffRow
7+
import com.github.difflib.text.DiffRow.Tag
88
import com.github.difflib.text.DiffRowGenerator
99

1010
/**
11-
* Utility class to generate colored diff between two texts
11+
* Generates a field-level word-diff for each node's states.
12+
*
1213
*/
13-
1414
fun computeAnnotatedDiff(
1515
past: String,
1616
current: String
1717
): AnnotatedString {
18-
19-
2018
val diffGenerator = DiffRowGenerator.create()
2119
.showInlineDiffs(true)
2220
.inlineDiffByWord(true)
23-
// .replaceOriginalLinefeedInChangesWithSpaces(true)
21+
.mergeOriginalRevised(true)
2422
.oldTag { f -> "--" }
2523
.newTag { f -> "++" }
2624
.build()
2725

2826
val pastName = extractTypeName(past)
29-
val pastFields = getFieldsAsList(past)
3027
val currentName = extractTypeName(current)
28+
val pastFields = getFieldsAsList(past)
3129
val currentFields = getFieldsAsList(current)
32-
print(past + "\n\n")
33-
println(pastFields)
34-
// println(diffGenerator.generateDiffRows(pastFields, currentFields))
30+
val diffRows = diffGenerator.generateDiffRows(pastFields, currentFields)
31+
3532
var existsDiff = false
3633
return buildAnnotatedString {
34+
// A full change in the type means all internal data will be changed, so it's easier to just
35+
//generalize and show the diff in the type's name
3736
if (pastName != currentName) {
38-
append("\n")
39-
pushStyle(SpanStyle(background = Color.Red.copy(alpha = 0.3f)))
40-
append("$pastName(...)")
41-
pop()
37+
buildString(
38+
style = DiffStyles.DELETE,
39+
text = "$pastName(...)",
40+
builder = this
41+
)
42+
// pushStyle(DiffStyles.DELETE)
43+
// append("$pastName(...)")
44+
// pop()
4245
append("")
43-
pushStyle(SpanStyle(background = Color.Green.copy(alpha = 0.3f)))
44-
append("$currentName(...)")
45-
pop()
46+
buildString(
47+
style = DiffStyles.INSERT,
48+
text = "$currentName(...)",
49+
builder = this
50+
)
51+
return@buildAnnotatedString
52+
}
53+
54+
diffRows.forEach { row ->
55+
val tag = row.tag!!
56+
// The 'mergeOriginalRevised' flag changes the semantics of the data, but the API still returns
57+
// the same components
58+
val fullDiff = row.oldLine
59+
60+
/*
61+
Tag.INSERT and Tag.DELETE only happens when there is a difference in number of rows, i.e.:
62+
Tag(["a"],["a","b"]) == INSERT
63+
and
64+
Tag(["a","b"],["a"]) == DELETE
65+
but
66+
Tag([""],["a"]) == CHANGE
67+
*/
68+
when (tag) {
69+
Tag.CHANGE -> {
70+
existsDiff = true
71+
parseChangedDiff(fullDiff).forEach { (style, text) ->
72+
buildString(
73+
style = style,
74+
text = text,
75+
builder = this
76+
)
77+
}
78+
append("\n\n")
79+
}
80+
81+
Tag.INSERT -> {
82+
existsDiff = true
83+
buildString(
84+
text = fullDiff.replace("++", ""),
85+
style = DiffStyles.INSERT,
86+
builder = this
87+
)
88+
append("\n\n")
89+
}
90+
91+
Tag.DELETE -> {
92+
existsDiff = true
93+
buildString(
94+
text = fullDiff.replace("--", ""),
95+
style = DiffStyles.DELETE,
96+
builder = this
97+
)
98+
append("\n\n")
99+
}
100+
101+
Tag.EQUAL -> {
102+
// NoOp
103+
}
104+
}
105+
}
106+
107+
if (!existsDiff) {
108+
buildString(
109+
style = DiffStyles.NO_CHANGE,
110+
text = "No Diff",
111+
builder = this
112+
)
46113
}
114+
}
115+
}
116+
117+
/**
118+
* Parses the full diff within Tag.CHANGED to give back a list of operations to perform
119+
*/
120+
private fun parseChangedDiff(fullDiff: String): List<Pair<SpanStyle, String>> {
121+
val operations: MutableList<Pair<SpanStyle, String>> = mutableListOf()
122+
var i = 0
123+
while (i < fullDiff.length) {
124+
when {
125+
fullDiff.startsWith("--", i) -> {
126+
val end = fullDiff.indexOf("--", i + 2)
127+
if (end != -1) {
128+
val removed = fullDiff.substring(i + 2, end)
129+
operations.add(DiffStyles.DELETE to removed)
130+
i = end + 2
131+
}
132+
}
47133

48-
/*
49-
zip shortens both to the shortest one, so
50-
a) if past > current, then we pushStyle(delete) for the rest of past
51-
b) if current > past, then we pushStyle(add) for the rest of current
52-
*/
53-
// pastFields.zip(currentFields).forEach { (pastField, currentField) ->
54-
// val diff = diffGenerator.generateDiffRows(pastField, currentField)
55-
// val tag = diff[0]
56-
// if (tag == DiffRow.Tag.CHANGE) {
57-
//
58-
// }
59-
// }
134+
fullDiff.startsWith("++", i) -> {
135+
val end = fullDiff.indexOf("++", i + 2)
136+
if (end != -1) {
137+
val added = fullDiff.substring(i + 2, end)
138+
operations.add(DiffStyles.INSERT to added)
139+
i = end + 2
140+
}
141+
}
142+
143+
else -> {
144+
val nextTagStart = listOf(
145+
fullDiff.indexOf("--", i),
146+
fullDiff.indexOf("++", i)
147+
).filter { it >= 0 }.minOrNull() ?: fullDiff.length
148+
operations.add(DiffStyles.UNCHANGED to fullDiff.substring(i, nextTagStart))
149+
i = nextTagStart
150+
}
151+
}
60152
}
153+
154+
return operations
155+
}
156+
157+
object DiffStyles {
158+
val DELETE = SpanStyle(background = Color.Red.copy(alpha = 0.3f))
159+
val INSERT = SpanStyle(background = Color.Green.copy(alpha = 0.3f))
160+
val NO_CHANGE = SpanStyle(background = Color.LightGray)
161+
val UNCHANGED = SpanStyle()
162+
}
163+
164+
internal fun buildString(
165+
style: SpanStyle,
166+
text: String,
167+
builder: AnnotatedString.Builder
168+
) {
169+
builder.pushStyle(style)
170+
builder.append(text)
171+
builder.pop()
61172
}
62173

63174
/**
@@ -95,6 +206,7 @@ private fun getFieldsAsList(field: String): List<String> {
95206
currentField.append(char)
96207
}
97208
}
209+
98210
else -> currentField.append(char)
99211
}
100212
i++

0 commit comments

Comments
 (0)