Packages

A simple snapshot testing library inspired by Birdie (Gleam) and Insta (Rust).

Current section

Files

Jump to
calque lib diff.ex
Raw

lib/diff.ex

defmodule Calque.Diff do
@moduledoc """
Robust line-by-line diff that returns plain maps with kind metadata (no ANSI).
Coloring is handled by the UI layer (Calque.pretty_diff_line/2).
"""
@type diff_line_kind :: :old | :new | :shared
@type diff_line :: %{number: pos_integer(), line: String.t(), kind: diff_line_kind()}
@doc """
Compare two multiline strings line by line and return a flat list of lines.
For each index:
* if both sides present and equal -> one `:shared` line
* if only old present -> one `:old` line
* if only new present -> one `:new` line
* if both present but different -> two lines: first `:old`, then `:new`
"""
@spec line_by_line(String.t(), String.t()) :: [diff_line()]
def line_by_line(old, new) when is_binary(old) and is_binary(new) do
# Degenerate case: both empty -> one shared empty line
if old == "" and new == "" do
[%{number: 1, line: "", kind: :shared}]
else
# Treat a totally empty side as "no lines" (not [""])
old_lines =
if old == "" do
[]
else
String.split(old, "\n", trim: false)
end
new_lines =
if new == "" do
[]
else
String.split(new, "\n", trim: false)
end
max_len = max(length(old_lines), length(new_lines))
0..(max_len - 1)
|> Enum.flat_map(fn i ->
n = i + 1
old_line = Enum.at(old_lines, i, nil)
new_line = Enum.at(new_lines, i, nil)
cond do
not is_nil(old_line) and not is_nil(new_line) and old_line == new_line ->
[%{number: n, line: old_line, kind: :shared}]
not is_nil(old_line) and is_nil(new_line) ->
[%{number: n, line: old_line, kind: :old}]
is_nil(old_line) and not is_nil(new_line) ->
[%{number: n, line: new_line, kind: :new}]
true ->
[
%{number: n, line: old_line, kind: :old},
%{number: n, line: new_line, kind: :new}
]
end
end)
end
end
# permissive fallback
def line_by_line(_, _), do: []
end