Group numbered changed lines into diff hunks with true file offsets. `changed` must be in file order. Changes separated by more than `2 * context` unchanged lines become separate hunks; closer changes merge with the gap emitted as context. Context line content is taken from `before_lines` (the file's recorded before-content); when it is `None`, hunks are emitted with zero context but still carry
(
changed: &[NumberedLine],
before_lines: Option<&[Vec<u8>]>,
context: usize,
)
| 126 | /// at boundary `p` is `@@ -p,0 +n,k @@`, a pure deletion is |
| 127 | /// `@@ -l,k +p,0 @@`, and context lines count toward both sides. |
| 128 | pub(super) fn hunks_from_changed_lines( |
| 129 | changed: &[NumberedLine], |
| 130 | before_lines: Option<&[Vec<u8>]>, |
| 131 | context: usize, |
| 132 | ) -> Vec<DiffHunk> { |
| 133 | let mut hunks = Vec::new(); |
| 134 | if changed.is_empty() { |
| 135 | return hunks; |
| 136 | } |
| 137 | |
| 138 | let old_len = before_lines.map(|l| l.len()).unwrap_or(0); |
| 139 | let ctx = if before_lines.is_some() { context } else { 0 }; |
| 140 | let merge_gap = 2 * ctx as isize; |
| 141 | |
| 142 | // 1. Group into runs of nearby changes, measured in old-file |
| 143 | // coordinates. |
| 144 | let mut runs: Vec<(usize, usize)> = Vec::new(); |
| 145 | let mut run_start = 0usize; |
| 146 | let mut run_hi = changed[0].old_pos(); |
| 147 | for (i, nl) in changed.iter().enumerate().skip(1) { |
| 148 | let lo = nl.old_pos(); |
| 149 | // Unchanged lines between the run and this change. An addition |
| 150 | // sits at a boundary, so the gap is measured from the boundary |
| 151 | // itself; a removal occupies a line, hence the extra -1. |
| 152 | let gap = match nl.line.status { |
| 153 | LineStatus::Removed => lo as isize - run_hi as isize - 1, |
| 154 | _ => lo as isize - run_hi as isize, |
| 155 | }; |
| 156 | if gap > merge_gap { |
| 157 | runs.push((run_start, i)); |
| 158 | run_start = i; |
| 159 | } |
| 160 | run_hi = run_hi.max(nl.old_pos()); |
| 161 | } |
| 162 | runs.push((run_start, changed.len())); |
| 163 | |
| 164 | // 2. Emit one hunk per run. |
| 165 | for (start, end) in runs { |
| 166 | let lines = &changed[start..end]; |
| 167 | |
| 168 | let removed_count = lines.iter().filter(|l| l.line.is_removed()).count(); |
| 169 | let added_count = lines.len() - removed_count; |
| 170 | |
| 171 | let old_lo = lines.iter().map(|l| l.old_pos()).min().unwrap_or(0); |
| 172 | let old_hi = lines.iter().map(|l| l.old_pos()).max().unwrap_or(0); |
| 173 | let new_lo = lines.iter().map(|l| l.new_pos()).min().unwrap_or(0); |
| 174 | |
| 175 | // Old-file range covered by the hunk, including context. |
| 176 | let (old_start, old_end, old_count) = if removed_count > 0 { |
| 177 | let s = old_lo.saturating_sub(ctx).max(1); |
| 178 | let e = if ctx > 0 { |
| 179 | old_hi.saturating_add(ctx).min(old_len) |
| 180 | } else { |
| 181 | old_hi |
| 182 | }; |
| 183 | let e = e.max(s); |
| 184 | (s, e, e - s + 1) |
| 185 | } else { |
nothing calls this directly
no test coverage detected