Skip to content

Text Diff Viewer — Swift 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 Swift 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: Swift (5.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 DiffType { case equal, removed, added }

/// A merged run of consecutive same-type tokens (line tokens rejoin with '\n').
struct DiffPart { var type: DiffType; var text: String }

/// Content-only lines — the TS tokenizer's 'line' case: joining the tokens
/// back with '\n' reconstructs the input exactly; '' tokenizes to nothing.
func tokenize(_ text: String) -> [String] {
    text.isEmpty ? [] : text.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
}

/// 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 reference.
func diff(_ oldText: String, _ newText: String) -> [DiffPart] {
    let a = tokenize(oldText), b = tokenize(newText)
    let n = a.count, m = b.count
    var dp = [[Int]](repeating: [Int](repeating: 0, count: m + 1), count: n + 1)
    for i in stride(from: n - 1, through: 0, by: -1) {
        for j in stride(from: m - 1, through: 0, by: -1) {
            dp[i][j] = a[i] == b[j] ? dp[i + 1][j + 1] + 1 : max(dp[i + 1][j], dp[i][j + 1])
        }
    }
    var parts: [DiffPart] = [] // merge step: same-type runs rejoin with '\n'
    func push(_ t: DiffType, _ tok: String) {
        if let last = parts.last, last.type == t {
            parts[parts.count - 1].text += "\n" + tok
        } else { parts.append(DiffPart(type: t, text: tok)) }
    }
    var i = 0, j = 0
    while i < n || j < m {
        if i < n && j < m && a[i] == b[j] { push(.equal, a[i]); i += 1; j += 1 }
        else if j == m || (i < n && dp[i + 1][j] >= dp[i][j + 1]) { push(.removed, a[i]); i += 1 }
        else { push(.added, b[j]); j += 1 }
    }
    return parts
}

let a = "const x = 1;\nfunction greet(name) {\n  return 'hi ' + name;\n}\nconsole.log(greet('dev'));"
let b = "const x = 2;\nfunction greet(name) {\n  return 'hello, ' + name + '!';\n}\nconsole.log(greet('dev'));"
var add = 0, rem = 0, same = 0
for p in diff(a, b) { // one prefix per line inside each part
    let pre = p.type == .added ? "+" : p.type == .removed ? "-" : " "
    for line in p.text.split(separator: "\n", omittingEmptySubsequences: false) { print("\(pre) \(line)") }
    switch p.type {
    case .added: add += p.text.count
    case .removed: rem += p.text.count
    case .equal: same += p.text.count
    }
}
print("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 →