1use imara_diff::{Algorithm, Diff, InternedInput};
12
13pub const CONTEXT: usize = 3;
15pub const MERGE_GAP: usize = CONTEXT * 2 + 1;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LineKind {
21 Context,
23 Add,
25 Del,
27}
28
29impl LineKind {
30 pub fn as_str(self) -> &'static str {
32 match self {
33 LineKind::Context => "context",
34 LineKind::Add => "add",
35 LineKind::Del => "del",
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct Line {
43 pub kind: LineKind,
45 pub old_no: Option<usize>,
47 pub new_no: Option<usize>,
49 pub text: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Hunk {
56 pub old_start: usize,
58 pub old_lines: usize,
60 pub new_start: usize,
62 pub new_lines: usize,
64 pub lines: Vec<Line>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
70pub struct FileDiff {
71 pub hunks: Vec<Hunk>,
73 pub insertions: usize,
75 pub deletions: usize,
77}
78
79pub fn hunks(before: &str, after: &str) -> FileDiff {
81 let before_lines: Vec<&str> = before.lines().collect();
82 let after_lines: Vec<&str> = after.lines().collect();
83
84 let input = InternedInput::new(before, after);
85 let mut diff = Diff::compute(Algorithm::Histogram, &input);
86 diff.postprocess_lines(&input);
87
88 let raw: Vec<(std::ops::Range<usize>, std::ops::Range<usize>)> = diff
89 .hunks()
90 .map(|h| {
91 (
92 h.before.start as usize..h.before.end as usize,
93 h.after.start as usize..h.after.end as usize,
94 )
95 })
96 .collect();
97
98 let insertions = raw.iter().map(|(_, a)| a.len()).sum();
99 let deletions = raw.iter().map(|(b, _)| b.len()).sum();
100
101 let mut groups: Vec<Vec<usize>> = Vec::new();
102 for (index, (before_range, _)) in raw.iter().enumerate() {
103 let joins = groups.last().is_some_and(|group| {
104 let last = &raw[*group.last().expect("a group is never empty")].0;
105 before_range.start.saturating_sub(last.end) <= MERGE_GAP
106 });
107 if joins {
108 groups.last_mut().expect("checked just above").push(index);
109 } else {
110 groups.push(vec![index]);
111 }
112 }
113
114 let mut out = Vec::new();
115 for group in groups {
116 let first = &raw[group[0]];
117 let last = &raw[*group.last().expect("a group is never empty")];
118 let old_from = first.0.start.saturating_sub(CONTEXT);
119 let old_to = (last.0.end + CONTEXT).min(before_lines.len());
120 let new_from = first.1.start.saturating_sub(CONTEXT);
121 let new_to = (last.1.end + CONTEXT).min(after_lines.len());
122
123 let mut lines = Vec::new();
124 let mut old_at = old_from;
125 let mut new_at = new_from;
126 for index in &group {
127 let (before_range, after_range) = &raw[*index];
128 while old_at < before_range.start {
129 lines.push(Line {
130 kind: LineKind::Context,
131 old_no: Some(old_at + 1),
132 new_no: Some(new_at + 1),
133 text: before_lines[old_at].to_owned(),
134 });
135 old_at += 1;
136 new_at += 1;
137 }
138 for line in before_range.clone() {
139 lines.push(Line {
140 kind: LineKind::Del,
141 old_no: Some(line + 1),
142 new_no: None,
143 text: before_lines[line].to_owned(),
144 });
145 }
146 for line in after_range.clone() {
147 lines.push(Line {
148 kind: LineKind::Add,
149 old_no: None,
150 new_no: Some(line + 1),
151 text: after_lines[line].to_owned(),
152 });
153 }
154 old_at = before_range.end;
155 new_at = after_range.end;
156 }
157 while old_at < old_to && new_at < new_to {
158 lines.push(Line {
159 kind: LineKind::Context,
160 old_no: Some(old_at + 1),
161 new_no: Some(new_at + 1),
162 text: before_lines[old_at].to_owned(),
163 });
164 old_at += 1;
165 new_at += 1;
166 }
167 let old_lines = old_at - old_from;
168 let new_lines = new_at - new_from;
169 out.push(Hunk {
170 old_start: if old_lines == 0 { 0 } else { old_from + 1 },
171 old_lines,
172 new_start: if new_lines == 0 { 0 } else { new_from + 1 },
173 new_lines,
174 lines,
175 });
176 }
177 FileDiff {
178 hunks: out,
179 insertions,
180 deletions,
181 }
182}
183
184pub fn unified(diff: &FileDiff) -> String {
186 let mut s = String::new();
187 for h in &diff.hunks {
188 s.push_str(&format!(
189 "@@ -{},{} +{},{} @@\n",
190 h.old_start, h.old_lines, h.new_start, h.new_lines
191 ));
192 for l in &h.lines {
193 s.push(match l.kind {
194 LineKind::Context => ' ',
195 LineKind::Add => '+',
196 LineKind::Del => '-',
197 });
198 s.push_str(&l.text);
199 s.push('\n');
200 }
201 }
202 s
203}