Text Diff Viewer — Kotlin source
Compare two pieces of text and see exactly what changed. Highlights added and removed lines, words, or characters, shows a per-side summary, and exports a unified diff you can paste into a PR or commit. Runs 100% in your browser.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// text-diff — line-granularity diff via an LCS dynamic-programming table. Language: Kotlin (JVM 1.9+). Port of src/lib/text-diff.ts — core tokenizer/backwards-DP/greedy-walk/run-merge; word/char granularity, normalization options and unified hunk headers live in this dir's javascript.js (80-line budget).
enum class DiffType { EQUAL, REMOVED, ADDED }
/** A merged run of consecutive same-type tokens (line tokens rejoin with '\n'). */
data class DiffPart(val type: DiffType, val text: String)
object TextDiff {
/** Content-only lines — the TS tokenizer's 'line' case: joining the tokens
* back with '\n' reconstructs the input; '' tokenizes to nothing. */
fun tokenize(text: String): List<String> =
if (text.isEmpty()) emptyList() else text.split('\n')
/** dp[i][j] = LCS length of a[i..] and b[j..], built backwards. The greedy
* walk emits an equal part on token match, else drops the side whose
* remaining LCS is larger — the '>=' tie favors REMOVED, as in the TS. */
fun diff(oldText: String, newText: String): List<DiffPart> {
val a = tokenize(oldText); val b = tokenize(newText)
val n = a.size; val m = b.size
val dp = Array(n + 1) { IntArray(m + 1) }
for (i in n - 1 downTo 0)
for (j in m - 1 downTo 0)
dp[i][j] = if (a[i] == b[j]) dp[i + 1][j + 1] + 1 else maxOf(dp[i + 1][j], dp[i][j + 1])
val parts = mutableListOf<DiffPart>() // merge step: same-type runs rejoin '\n'
var i = 0; var j = 0
while (i < n || j < m) {
val (t, tok) = when {
i < n && j < m && a[i] == b[j] -> DiffType.EQUAL to a[i].also { i++; j++ }
j == m || (i < n && dp[i + 1][j] >= dp[i][j + 1]) -> DiffType.REMOVED to a[i++]
else -> DiffType.ADDED to b[j++]
}
val last = parts.lastOrNull()
if (last != null && last.type == t) parts[parts.size - 1] = last.copy(text = last.text + "\n" + tok)
else parts.add(DiffPart(t, tok))
}
return parts
}
}
fun main() {
val a = "const x = 1;\nfunction greet(name) {\n return 'hi ' + name;\n}\nconsole.log(greet('dev'));"
val b = "const x = 2;\nfunction greet(name) {\n return 'hello, ' + name + '!';\n}\nconsole.log(greet('dev'));"
var add = 0; var rem = 0; var same = 0
for (p in TextDiff.diff(a, b)) { // one prefix per line inside each part
val pre = if (p.type == DiffType.ADDED) '+' else if (p.type == DiffType.REMOVED) '-' else ' '
for (line in p.text.split('\n')) println("$pre $line")
when (p.type) {
DiffType.ADDED -> add += p.text.length
DiffType.REMOVED -> rem += p.text.length
else -> same += p.text.length
}
}
println("summary: +$add added, -$rem removed, =$same unchanged chars")
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →