src/gliff.erl

-module(gliff).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gliff.gleam").
-export([to_unified/3, to_unified_with/4, to_ansi/3, to_ansi_inline/1, from_unified/1, apply_patch/2, apply_patch_fuzzy/3, similarity/1, cleanup_semantic/1, cleanup_semantic_lossless/1, inline_highlight/1, merge3/3, default_config/0, diff/2, diff_chars/2, diff_myers/2, diff_patience/2, diff_words/2, diff_with/3, diff_chars_with/3]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " gliff — A text diffing library for Gleam.\n"
    "\n"
    " Provides Myers and Patience diff algorithms, unified diff formatting,\n"
    " patch application, semantic cleanup, and inline highlighting.\n"
).

-file("src/gliff.gleam", 54).
?DOC(
    " Format a list of edits as a unified diff string.\n"
    "\n"
    " The output matches the format produced by `diff -u`, with 3 lines of\n"
    " context around each change. `old_name` and `new_name` appear in the\n"
    " `---` and `+++` header lines.\n"
).
-spec to_unified(list(gliff@types:edit()), binary(), binary()) -> binary().
to_unified(Edits, Old_name, New_name) ->
    gliff@internal@unified:to_unified(Edits, Old_name, New_name).

-file("src/gliff.gleam", 66).
?DOC(
    " Format edits as a unified diff with a custom number of context lines.\n"
    "\n"
    " Same as `to_unified` but allows specifying how many unchanged lines\n"
    " surround each change. Equivalent to `diff -U<n>`.\n"
).
-spec to_unified_with(list(gliff@types:edit()), binary(), binary(), integer()) -> binary().
to_unified_with(Edits, Old_name, New_name, Context) ->
    gliff@internal@unified:to_unified_with(Edits, Old_name, New_name, Context).

-file("src/gliff.gleam", 79).
?DOC(
    " Render edits as a colored diff string using ANSI escape codes.\n"
    "\n"
    " Deletions appear in red, insertions in green, and hunk headers in cyan.\n"
    " Output follows the same structure as unified diff format.\n"
).
-spec to_ansi(list(gliff@types:edit()), binary(), binary()) -> binary().
to_ansi(Edits, Old_name, New_name) ->
    gliff@internal@ansi:to_ansi(Edits, Old_name, New_name).

-file("src/gliff.gleam", 91).
?DOC(
    " Render edits with ANSI colors and inline character highlighting.\n"
    "\n"
    " Changed characters within modified lines are rendered in bold,\n"
    " making it easy to see exactly what changed at a glance.\n"
).
-spec to_ansi_inline(list(gliff@types:edit())) -> binary().
to_ansi_inline(Edits) ->
    gliff@internal@ansi:to_ansi_inline(Edits).

-file("src/gliff.gleam", 99).
?DOC(
    " Parse a unified diff string into a list of hunks.\n"
    "\n"
    " Accepts the standard format produced by `diff -u` or `to_unified`.\n"
    " Returns an error if the input is malformed.\n"
).
-spec from_unified(binary()) -> {ok, list(gliff@types:hunk())} |
    {error, binary()}.
from_unified(Input) ->
    gliff@internal@unified:from_unified(Input).

-file("src/gliff.gleam", 110).
?DOC(
    " Apply edit operations to a text string to produce the target text.\n"
    "\n"
    " The fundamental property is:\n"
    " `apply_patch(old, diff(old, new)) == Ok(new)`\n"
    "\n"
    " Returns an error if the text doesn't match the expected content\n"
    " described by the Equal and Delete operations.\n"
).
-spec apply_patch(binary(), list(gliff@types:edit())) -> {ok, binary()} |
    {error, binary()}.
apply_patch(Text, Edits) ->
    gliff@internal@patch:apply_patch(Text, Edits).

-file("src/gliff.gleam", 120).
?DOC(
    " Apply edit operations with fuzzy matching for context/delete lines.\n"
    "\n"
    " When Equal or Delete lines don't match exactly, accepts lines that\n"
    " are similar enough based on character-level comparison.\n"
    " Tolerance ranges from 0.0 (exact match, same as `apply_patch`) to\n"
    " 1.0 (accept any line). A value of 0.6 works well for most cases.\n"
).
-spec apply_patch_fuzzy(binary(), list(gliff@types:edit()), float()) -> {ok,
        binary()} |
    {error, binary()}.
apply_patch_fuzzy(Text, Edits, Tolerance) ->
    gliff@internal@fuzzy_patch:apply_patch_fuzzy(Text, Edits, Tolerance).

-file("src/gliff.gleam", 132).
?DOC(
    " Compute the similarity ratio between two texts from their diff result.\n"
    "\n"
    " Returns a value between 0.0 (completely different) and 1.0 (identical).\n"
    " Calculated as `2 * matching_elements / total_elements`.\n"
).
-spec similarity(list(gliff@types:edit())) -> float().
similarity(Edits) ->
    gliff@internal@similarity:ratio(Edits).

-file("src/gliff.gleam", 170).
?DOC(
    " Eliminate trivial equalities and merge adjacent edits.\n"
    "\n"
    " Removes small Equal sections that are surrounded by larger changes,\n"
    " absorbing them into the Delete and Insert operations. This produces\n"
    " fewer, larger edit blocks that are easier to read.\n"
).
-spec cleanup_semantic(list(gliff@types:edit())) -> list(gliff@types:edit()).
cleanup_semantic(Edits) ->
    gliff@internal@cleanup:semantic(Edits).

-file("src/gliff.gleam", 179).
?DOC(
    " Shift edit boundaries to natural positions without changing semantics.\n"
    "\n"
    " Moves edit boundaries to align with word boundaries, sentence endings,\n"
    " or blank lines based on a scoring system. The resulting diff applies\n"
    " identically but reads more naturally.\n"
).
-spec cleanup_semantic_lossless(list(gliff@types:edit())) -> list(gliff@types:edit()).
cleanup_semantic_lossless(Edits) ->
    gliff@internal@cleanup:semantic_lossless(Edits).

-file("src/gliff.gleam", 188).
?DOC(
    " Enrich line-level edits with character-level change highlighting.\n"
    "\n"
    " For adjacent Delete/Insert pairs, computes a character-level sub-diff\n"
    " and returns spans marking which characters actually changed. Equal\n"
    " edits pass through as InlineEqual.\n"
).
-spec inline_highlight(list(gliff@types:edit())) -> list(gliff@types:inline_edit()).
inline_highlight(Edits) ->
    gliff@internal@inline:highlight(Edits).

-file("src/gliff.gleam", 201).
?DOC(
    " Perform a 3-way merge between two diverged versions of a base text.\n"
    "\n"
    " Computes diff(base, ours) and diff(base, theirs), then combines\n"
    " the changes. When both sides modify the same region differently,\n"
    " a conflict is reported with Git-style conflict markers.\n"
    "\n"
    " Returns `MergeOk` if the merge is clean, or `MergeConflict` with\n"
    " the merged text (including `<<<<<<<`/`=======`/`>>>>>>>` markers)\n"
    " and a list of conflicts.\n"
).
-spec merge3(binary(), binary(), binary()) -> gliff@types:merge_result().
merge3(Base, Ours, Theirs) ->
    gliff@internal@merge:merge3(Base, Ours, Theirs).

-file("src/gliff.gleam", 209).
?DOC(
    " Create a default diff configuration.\n"
    "\n"
    " Returns `DiffConfig(algorithm: Myers, cleanup: NoCleanup, max_iterations: 0)`\n"
    " where `max_iterations: 0` means unlimited computation.\n"
).
-spec default_config() -> gliff@types:diff_config().
default_config() ->
    {diff_config, myers, no_cleanup, 0}.

-file("src/gliff.gleam", 264).
-spec apply_cleanup(list(gliff@types:edit()), gliff@types:cleanup()) -> list(gliff@types:edit()).
apply_cleanup(Edits, Mode) ->
    case Mode of
        no_cleanup ->
            Edits;

        semantic_cleanup ->
            gliff@internal@cleanup:semantic(Edits);

        semantic_lossless_cleanup ->
            gliff@internal@cleanup:semantic_lossless(Edits)
    end.

-file("src/gliff.gleam", 272).
-spec split_lines(binary()) -> list(binary()).
split_lines(Text) ->
    case Text of
        <<""/utf8>> ->
            [];

        _ ->
            gleam@string:split(Text, <<"\n"/utf8>>)
    end.

-file("src/gliff.gleam", 302).
-spec collect_equals(list(gliff@types:raw_edit()), list(binary())) -> {list(binary()),
    list(gliff@types:raw_edit())}.
collect_equals(Raw, Acc) ->
    case Raw of
        [{raw_equal, V} | Rest] ->
            collect_equals(Rest, [V | Acc]);

        _ ->
            {Acc, Raw}
    end.

-file("src/gliff.gleam", 312).
-spec collect_inserts(list(gliff@types:raw_edit()), list(binary())) -> {list(binary()),
    list(gliff@types:raw_edit())}.
collect_inserts(Raw, Acc) ->
    case Raw of
        [{raw_insert, V} | Rest] ->
            collect_inserts(Rest, [V | Acc]);

        _ ->
            {Acc, Raw}
    end.

-file("src/gliff.gleam", 322).
-spec collect_deletes(list(gliff@types:raw_edit()), list(binary())) -> {list(binary()),
    list(gliff@types:raw_edit())}.
collect_deletes(Raw, Acc) ->
    case Raw of
        [{raw_delete, V} | Rest] ->
            collect_deletes(Rest, [V | Acc]);

        _ ->
            {Acc, Raw}
    end.

-file("src/gliff.gleam", 284).
-spec group_edits_loop(list(gliff@types:raw_edit()), list(gliff@types:edit())) -> list(gliff@types:edit()).
group_edits_loop(Raw, Acc) ->
    case Raw of
        [] ->
            Acc;

        [{raw_equal, V} | Rest] ->
            {Equals, Remaining} = collect_equals(Rest, [V]),
            group_edits_loop(Remaining, [{equal, lists:reverse(Equals)} | Acc]);

        [{raw_insert, V@1} | Rest@1] ->
            {Inserts, Remaining@1} = collect_inserts(Rest@1, [V@1]),
            group_edits_loop(
                Remaining@1,
                [{insert, lists:reverse(Inserts)} | Acc]
            );

        [{raw_delete, V@2} | Rest@2] ->
            {Deletes, Remaining@2} = collect_deletes(Rest@2, [V@2]),
            group_edits_loop(
                Remaining@2,
                [{delete, lists:reverse(Deletes)} | Acc]
            )
    end.

-file("src/gliff.gleam", 279).
-spec group_edits(list(gliff@types:raw_edit())) -> list(gliff@types:edit()).
group_edits(Raw) ->
    _pipe = group_edits_loop(Raw, []),
    lists:reverse(_pipe).

-file("src/gliff.gleam", 31).
?DOC(
    " Compute a line-level diff between two strings using the Myers algorithm.\n"
    "\n"
    " Returns a list of edit operations (Equal, Insert, Delete) where each\n"
    " operation contains one or more contiguous lines.\n"
).
-spec diff(binary(), binary()) -> list(gliff@types:edit()).
diff(Old, New) ->
    Old_lines = split_lines(Old),
    New_lines = split_lines(New),
    Raw_edits = gliff@internal@myers:diff(Old_lines, New_lines),
    group_edits(Raw_edits).

-file("src/gliff.gleam", 42).
?DOC(
    " Compute a character-level diff between two strings using the Myers algorithm.\n"
    "\n"
    " Each element in the returned edit list represents one or more contiguous\n"
    " grapheme clusters that share the same edit type.\n"
).
-spec diff_chars(binary(), binary()) -> list(gliff@types:edit()).
diff_chars(Old, New) ->
    Old_chars = gleam@string:to_graphemes(Old),
    New_chars = gleam@string:to_graphemes(New),
    Raw_edits = gliff@internal@myers:diff(Old_chars, New_chars),
    group_edits(Raw_edits).

-file("src/gliff.gleam", 137).
?DOC(" Compute a line-level diff using the Myers algorithm (explicit alias for `diff`).\n").
-spec diff_myers(binary(), binary()) -> list(gliff@types:edit()).
diff_myers(Old, New) ->
    diff(Old, New).

-file("src/gliff.gleam", 146).
?DOC(
    " Compute a line-level diff using the Patience algorithm.\n"
    "\n"
    " Patience diff anchors on lines that appear exactly once in both texts,\n"
    " then recursively diffs the gaps. This often produces more readable\n"
    " output for code changes where blocks are reordered.\n"
).
-spec diff_patience(binary(), binary()) -> list(gliff@types:edit()).
diff_patience(Old, New) ->
    Old_lines = split_lines(Old),
    New_lines = split_lines(New),
    Raw_edits = gliff@internal@patience:diff(Old_lines, New_lines),
    group_edits(Raw_edits).

-file("src/gliff.gleam", 158).
?DOC(
    " Compute a word-level diff between two strings.\n"
    "\n"
    " Tokenizes input by whitespace boundaries (preserving whitespace as\n"
    " separate tokens), then diffs the token sequences. Each edit contains\n"
    " one or more word/whitespace tokens.\n"
).
-spec diff_words(binary(), binary()) -> list(gliff@types:edit()).
diff_words(Old, New) ->
    Old_tokens = gliff@internal@tokenize:words(Old),
    New_tokens = gliff@internal@tokenize:words(New),
    Raw_edits = gliff@internal@myers:diff(Old_tokens, New_tokens),
    group_edits(Raw_edits).

-file("src/gliff.gleam", 219).
?DOC(
    " Compute a line-level diff with full configuration.\n"
    "\n"
    " Allows selecting the algorithm, cleanup mode, and iteration budget.\n"
    " Returns `Complete` if the diff finished normally, or `Truncated` if\n"
    " the iteration budget was exceeded (in which case a crude but correct\n"
    " fallback diff is returned).\n"
).
-spec diff_with(binary(), binary(), gliff@types:diff_config()) -> gliff@types:diff_result().
diff_with(Old, New, Config) ->
    Old_tokens = split_lines(Old),
    New_tokens = split_lines(New),
    B = gliff@internal@budget:from_max(erlang:element(4, Config)),
    {Raw_edits, Complete} = case erlang:element(2, Config) of
        myers ->
            gliff@internal@myers:diff_with_budget(Old_tokens, New_tokens, B);

        patience ->
            {gliff@internal@patience:diff(Old_tokens, New_tokens), true}
    end,
    Edits = group_edits(Raw_edits),
    Edits_cleaned = apply_cleanup(Edits, erlang:element(3, Config)),
    case Complete of
        true ->
            {complete, Edits_cleaned};

        false ->
            {truncated, Edits_cleaned}
    end.

-file("src/gliff.gleam", 241).
?DOC(
    " Compute a character-level diff with full configuration.\n"
    "\n"
    " Same as `diff_with` but operates on grapheme clusters instead of lines.\n"
).
-spec diff_chars_with(binary(), binary(), gliff@types:diff_config()) -> gliff@types:diff_result().
diff_chars_with(Old, New, Config) ->
    Old_chars = gleam@string:to_graphemes(Old),
    New_chars = gleam@string:to_graphemes(New),
    B = gliff@internal@budget:from_max(erlang:element(4, Config)),
    {Raw_edits, Complete} = case erlang:element(2, Config) of
        myers ->
            gliff@internal@myers:diff_with_budget(Old_chars, New_chars, B);

        patience ->
            {gliff@internal@patience:diff(Old_chars, New_chars), true}
    end,
    Edits = group_edits(Raw_edits),
    Edits_cleaned = apply_cleanup(Edits, erlang:element(3, Config)),
    case Complete of
        true ->
            {complete, Edits_cleaned};

        false ->
            {truncated, Edits_cleaned}
    end.