build-a-diff-tool / lesson-22.md
Lesson 22 · Producing a unified diff

Merging nearby hunks

When two changes sit close together, their context bands overlap and would print the same lines twice. Today you merge such hunks into one, so nearby edits read as a single continuous block.

The goal

Merge hunks whose context bands overlap or touch, leaving distant changes as separate hunks.

Start here - the target
TO DO
Scenario: Close changes merge, distant changes do not
Giventhe diff of ["1".."9"] against the same list with "3" changed to "x" and "7" changed to "y", context 3
Whenthe hunks are grouped and merged
Thenthere is 1 hunk, because only 3 unchanged lines separate the changes (within 2 times the context), and its header is "@@ -1,9 +1,9 @@"
Andthe two-changes-7-apart case from the grouping lesson still yields 2 separate hunks
Background

The per-run grouping from before can produce hunks that overlap: if two changes are within 2·context lines of each other, the trailing context of the first hunk and the leading context of the second cover some of the same lines. Printing both would duplicate those lines and produce a malformed diff, so the fix is to merge any two hunks whose bands touch or overlap into a single hunk spanning both changes and the unchanged lines between them. Here the two changes are only three lines apart with context three, so they collapse into one hunk covering the whole file.

This is the rule real diff tools use, stated as a threshold: changes closer than twice the context share a hunk; changes farther apart get their own. It reads better - a reader sees one coherent block instead of two fragments with a sliver of context between - and it keeps the output valid for patch tools, which assume hunks are disjoint and in order. With merging in place, UnifiedDiff handles any arrangement of changes; wire the merge step into it so its output is always well-formed. Only the awkward file-boundary edges remain.

Make it work
// after building per-run hunks, fold overlapping neighbours together
merged := hunks[:1]
for _, h := range hunks[1:] {
last := &merged[len(merged)-1]
if h.startIndex <= last.endIndex { // context bands touch or overlap
// extend last to cover h, dropping the duplicated context lines
} else {
merged = append(merged, h)
}
}
CheckpointDONE
Nearby changes collapse into one hunk; distant ones stay separate. Commit and stop here.