Skip to content

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 →