Skip to content

Text Diff Viewer — Zig 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 Zig 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: Zig (0.12+). 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).
const std = @import("std");

const DiffType = enum { equal, removed, added };
/// A merged run of consecutive same-type tokens (line tokens rejoin with '\n').
const DiffPart = struct { type: DiffType, text: []const u8 };
/// Content-only lines — the TS tokenizer's 'line' case; '\n'-join reconstructs the input.
fn tokenize(alloc: std.mem.Allocator, text: []const u8) !std.ArrayList([]const u8) {
    var lines = std.ArrayList([]const u8).init(alloc);
    var it = std.mem.splitScalar(u8, text, '\n');
    while (it.next()) |line| try lines.append(line);
    return lines;
}

/// dp[i][j] = LCS length of a[i..] and b[j..], built backwards. The greedy walk
/// emits equal on match, else drops the larger-remaining-LCS side ('>=' favors 'removed').
fn diff(alloc: std.mem.Allocator, old: []const u8, new: []const u8) !std.ArrayList(DiffPart) {
    var a = try tokenize(alloc, old);
    defer a.deinit();
    var b = try tokenize(alloc, new);
    defer b.deinit();
    const n = a.items.len;
    const m = b.items.len;
    const w = m + 1;
    const dp = try alloc.alloc(usize, (n + 1) * w);
    defer alloc.free(dp);
    @memset(dp, 0);
    var i: usize = n;
    while (i > 0) {
        i -= 1;
        var j: usize = m;
        while (j > 0) {
            j -= 1;
            dp[i * w + j] = if (std.mem.eql(u8, a.items[i], b.items[j]))
                dp[(i + 1) * w + j + 1] + 1
            else
                @max(dp[(i + 1) * w + j], dp[i * w + j + 1]);
        }
    }
    var parts = std.ArrayList(DiffPart).init(alloc); // merge: same-type runs join '\n'
    i = 0;
    var j: usize = 0;
    while (i < n or j < m) {
        var t: DiffType = .added;
        var tok: []const u8 = undefined;
        if (i < n and j < m and std.mem.eql(u8, a.items[i], b.items[j])) {
            t = .equal; tok = a.items[i]; i += 1; j += 1;
        } else if (j == m or (i < n and dp[(i + 1) * w + j] >= dp[i * w + j + 1])) {
            t = .removed; tok = a.items[i]; i += 1;
        } else {
            t = .added; tok = b.items[j]; j += 1;
        }
        if (parts.items.len > 0 and parts.items[parts.items.len - 1].type == t) {
            const last = &parts.items[parts.items.len - 1];
            last.text = try std.mem.concat(alloc, u8, &.{ last.text, "\n", tok });
        } else try parts.append(.{ .type = t, .text = tok });
    }
    return parts;
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    const a = "const x = 1;\nfunction greet(name) {\n  return 'hi ' + name;\n}\nconsole.log(greet('dev'));";
    const b = "const x = 2;\nfunction greet(name) {\n  return 'hello, ' + name + '!';\n}\nconsole.log(greet('dev'));";
    var parts = try diff(alloc, a, b);
    var add: usize = 0; var rem: usize = 0; var same: usize = 0;
    for (parts.items) |p| { // one prefix per line inside each part
        const pre: u8 = if (p.type == .added) '+' else if (p.type == .removed) '-' else ' ';
        var lines = std.mem.splitScalar(u8, p.text, '\n');
        while (lines.next()) |line| std.debug.print("{c} {s}\n", .{ pre, line });
        switch (p.type) {
            .added => add += p.text.len,
            .removed => rem += p.text.len,
            .equal => same += p.text.len,
        }
    }
    std.debug.print("summary: +{d} added, -{d} removed, ={d} unchanged chars\n", .{ add, rem, same });
}

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 →