Skip to content

Text Diff Viewer — C# 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 C# 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: C# (.NET 7+). 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).
using System;
using System.Collections.Generic;

namespace TextDiff;

public enum DiffType { Equal, Removed, Added }

/// <summary>A merged run of consecutive same-type tokens (lines rejoin with '\n').</summary>
public readonly record struct Part(DiffType Type, string Text);

public static class TextDiffTool
{
    // Content-only lines — the TS tokenizer's 'line' case: joining with '\n'
    // reconstructs the input exactly; '' tokenizes to nothing.
    static List<string> Tokenize(string text) =>
        text == "" ? new List<string>() : new List<string>(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.
    public static List<Part> Diff(string oldText, string newText)
    {
        var a = Tokenize(oldText); var b = Tokenize(newText);
        int n = a.Count, m = b.Count;
        var dp = new int[n + 1, m + 1];
        for (int i = n - 1; i >= 0; i--)
            for (int j = m - 1; j >= 0; j--)
                dp[i, j] = a[i] == b[j] ? dp[i + 1, j + 1] + 1 : Math.Max(dp[i + 1, j], dp[i, j + 1]);
        var parts = new List<Part>(); // merge step: same-type runs rejoin '\n'
        void Push(DiffType t, string tok)
        {
            if (parts.Count > 0 && parts[^1].Type == t)
                parts[^1] = new Part(t, parts[^1].Text + "\n" + tok);
            else parts.Add(new Part(t, tok));
        }
        int i = 0, j = 0;
        while (i < n || j < m)
        {
            if (i < n && j < m && a[i] == b[j]) { Push(DiffType.Equal, a[i]); i++; j++; }
            else if (j == m || (i < n && dp[i + 1, j] >= dp[i, j + 1])) Push(DiffType.Removed, a[i++]);
            else Push(DiffType.Added, b[j++]);
        }
        return parts;
    }
}

public static class Demo
{
    public static void Main()
    {
        var a = "const x = 1;\nfunction greet(name) {\n  return 'hi ' + name;\n}\nconsole.log(greet('dev'));";
        var b = "const x = 2;\nfunction greet(name) {\n  return 'hello, ' + name + '!';\n}\nconsole.log(greet('dev'));";
        int add = 0, rem = 0, same = 0;
        foreach (var p in TextDiffTool.Diff(a, b)) // one prefix per line per part
        {
            char pre = p.Type == DiffType.Added ? '+' : p.Type == DiffType.Removed ? '-' : ' ';
            foreach (var line in p.Text.Split('\n')) Console.WriteLine($"{pre} {line}");
            if (p.Type == DiffType.Added) add += p.Text.Length;
            else if (p.Type == DiffType.Removed) rem += p.Text.Length;
            else same += p.Text.Length;
        }
        Console.WriteLine($"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 →