1use crate::types::{DocDiffHunk, DocDiffLine, DocDiffLineKind};
9
10#[derive(Debug, Clone)]
13struct DiffOp {
14 kind: DocDiffLineKind,
15 old_line: Option<u32>,
16 new_line: Option<u32>,
17 text: String,
18 old_before: usize,
19 new_before: usize,
20}
21
22pub fn build_line_hunks(old_text: &str, new_text: &str, context_lines: usize) -> Vec<DocDiffHunk> {
25 if old_text == new_text {
26 return vec![];
27 }
28
29 let old_lines = split_lines(old_text);
30 let new_lines = split_lines(new_text);
31 let ops = build_diff_ops(&old_lines, &new_lines);
32 let ranges = build_hunk_ranges(&ops, context_lines);
33
34 ranges
35 .into_iter()
36 .map(|(start, end)| build_hunk(&ops[start..=end]))
37 .collect()
38}
39
40pub fn build_line_hunks_default(old_text: &str, new_text: &str) -> Vec<DocDiffHunk> {
42 build_line_hunks(old_text, new_text, 3)
43}
44
45fn split_lines(text: &str) -> Vec<String> {
46 if text.is_empty() {
47 return vec![];
48 }
49
50 let mut lines: Vec<String> = text
51 .split('\n')
52 .map(|line| line.strip_suffix('\r').unwrap_or(line).to_string())
53 .collect();
54 if lines.last().map(|l| l.is_empty()).unwrap_or(false) {
55 lines.pop();
56 }
57
58 lines
59}
60
61fn build_diff_ops(old_lines: &[String], new_lines: &[String]) -> Vec<DiffOp> {
62 let lcs = build_lcs_table(old_lines, new_lines);
63 let mut ops = Vec::new();
64 let mut old_index = 0usize;
65 let mut new_index = 0usize;
66
67 while old_index < old_lines.len() || new_index < new_lines.len() {
68 let old_before = old_index;
69 let new_before = new_index;
70
71 if old_index < old_lines.len()
72 && new_index < new_lines.len()
73 && old_lines[old_index] == new_lines[new_index]
74 {
75 ops.push(DiffOp {
76 kind: DocDiffLineKind::Context,
77 old_line: Some((old_index + 1) as u32),
78 new_line: Some((new_index + 1) as u32),
79 text: old_lines[old_index].clone(),
80 old_before,
81 new_before,
82 });
83 old_index += 1;
84 new_index += 1;
85 } else if old_index < old_lines.len()
86 && (new_index == new_lines.len()
87 || lcs[old_index + 1][new_index] >= lcs[old_index][new_index + 1])
88 {
89 ops.push(DiffOp {
90 kind: DocDiffLineKind::Removed,
91 old_line: Some((old_index + 1) as u32),
92 new_line: None,
93 text: old_lines[old_index].clone(),
94 old_before,
95 new_before,
96 });
97 old_index += 1;
98 } else {
99 ops.push(DiffOp {
100 kind: DocDiffLineKind::Added,
101 old_line: None,
102 new_line: Some((new_index + 1) as u32),
103 text: new_lines[new_index].clone(),
104 old_before,
105 new_before,
106 });
107 new_index += 1;
108 }
109 }
110
111 ops
112}
113
114fn build_lcs_table(old_lines: &[String], new_lines: &[String]) -> Vec<Vec<usize>> {
115 let mut table = vec![vec![0usize; new_lines.len() + 1]; old_lines.len() + 1];
116
117 for old_index in (0..old_lines.len()).rev() {
118 for new_index in (0..new_lines.len()).rev() {
119 table[old_index][new_index] = if old_lines[old_index] == new_lines[new_index] {
120 table[old_index + 1][new_index + 1] + 1
121 } else {
122 table[old_index + 1][new_index].max(table[old_index][new_index + 1])
123 };
124 }
125 }
126
127 table
128}
129
130fn build_hunk_ranges(ops: &[DiffOp], context_lines: usize) -> Vec<(usize, usize)> {
131 let mut ranges: Vec<(usize, usize)> = Vec::new();
132
133 for (index, op) in ops.iter().enumerate() {
134 if op.kind == DocDiffLineKind::Context {
135 continue;
136 }
137
138 let start = index.saturating_sub(context_lines);
139 let end = (index + context_lines).min(ops.len() - 1);
140
141 if let Some(previous) = ranges.last_mut() {
142 if start <= previous.1 + 1 {
143 previous.1 = previous.1.max(end);
144 continue;
145 }
146 }
147 ranges.push((start, end));
148 }
149
150 ranges
151}
152
153fn build_hunk(lines: &[DiffOp]) -> DocDiffHunk {
154 let old_lines = lines
155 .iter()
156 .filter(|line| line.kind != DocDiffLineKind::Added)
157 .count() as u32;
158 let new_lines = lines
159 .iter()
160 .filter(|line| line.kind != DocDiffLineKind::Removed)
161 .count() as u32;
162
163 DocDiffHunk {
164 old_start: hunk_start(lines, Side::Old),
165 old_lines,
166 new_start: hunk_start(lines, Side::New),
167 new_lines,
168 lines: lines
169 .iter()
170 .map(|line| DocDiffLine {
171 kind: line.kind,
172 old_line: line.old_line,
173 new_line: line.new_line,
174 text: line.text.clone(),
175 })
176 .collect(),
177 }
178}
179
180enum Side {
181 Old,
182 New,
183}
184
185fn hunk_start(lines: &[DiffOp], side: Side) -> u32 {
186 let first_line = lines.iter().find_map(|line| match side {
187 Side::Old => line.old_line,
188 Side::New => line.new_line,
189 });
190
191 if let Some(value) = first_line {
192 return value;
193 }
194
195 match side {
196 Side::Old => lines[0].old_before as u32,
197 Side::New => lines[0].new_before as u32,
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use DocDiffLineKind::*;
205
206 fn line(
207 kind: DocDiffLineKind,
208 old_line: Option<u32>,
209 new_line: Option<u32>,
210 text: &str,
211 ) -> DocDiffLine {
212 DocDiffLine {
213 kind,
214 old_line,
215 new_line,
216 text: text.into(),
217 }
218 }
219
220 #[test]
221 fn identical_returns_no_hunks() {
222 assert!(build_line_hunks_default("alpha\nbeta\ngamma", "alpha\nbeta\ngamma").is_empty());
223 }
224
225 #[test]
226 fn replacement_marks_context_removed_added_context() {
227 let h = build_line_hunks_default("alpha\nbeta\ngamma", "alpha\ndelta\ngamma");
228 assert_eq!(
229 h,
230 vec![DocDiffHunk {
231 old_start: 1,
232 old_lines: 3,
233 new_start: 1,
234 new_lines: 3,
235 lines: vec![
236 line(Context, Some(1), Some(1), "alpha"),
237 line(Removed, Some(2), None, "beta"),
238 line(Added, None, Some(2), "delta"),
239 line(Context, Some(3), Some(3), "gamma"),
240 ],
241 }]
242 );
243 }
244
245 #[test]
246 fn distant_edits_split_into_two_hunks_with_context_one() {
247 let h = build_line_hunks("a\nb\nc\nd\ne\nf\ng", "a\nB\nc\nd\ne\nF\ng", 1);
248 assert_eq!(
249 h,
250 vec![
251 DocDiffHunk {
252 old_start: 1,
253 old_lines: 3,
254 new_start: 1,
255 new_lines: 3,
256 lines: vec![
257 line(Context, Some(1), Some(1), "a"),
258 line(Removed, Some(2), None, "b"),
259 line(Added, None, Some(2), "B"),
260 line(Context, Some(3), Some(3), "c"),
261 ],
262 },
263 DocDiffHunk {
264 old_start: 5,
265 old_lines: 3,
266 new_start: 5,
267 new_lines: 3,
268 lines: vec![
269 line(Context, Some(5), Some(5), "e"),
270 line(Removed, Some(6), None, "f"),
271 line(Added, None, Some(6), "F"),
272 line(Context, Some(7), Some(7), "g"),
273 ],
274 },
275 ]
276 );
277 }
278}