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 →