1use std::ops::Range;
2
3use similar::{Algorithm, DiffTag, capture_diff_slices, group_diff_ops};
4
5use crate::highlight::LineClasses;
6use crate::project::{LayerMode, Projection};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum RowKind {
10 Context,
11 Removed,
12 Added,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Row {
17 pub kind: RowKind,
18 pub old_line: Option<usize>,
20 pub new_line: Option<usize>,
22 pub text: String,
23 pub emphasis: Vec<Range<usize>>,
25 pub syntax: LineClasses,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Hunk {
30 pub rows: Vec<Row>,
31}
32
33impl Hunk {
34 pub fn changed(&self) -> impl Iterator<Item = &Row> {
35 self.rows.iter().filter(|row| row.kind != RowKind::Context)
36 }
37
38 pub fn header(&self) -> String {
39 let old = self.rows.iter().find_map(|row| row.old_line);
40 let new = self.rows.iter().find_map(|row| row.new_line);
41 let fmt = |line: Option<usize>| line.map_or("0".to_string(), |n| n.to_string());
42 format!("@@ -{} +{} @@", fmt(old), fmt(new))
43 }
44}
45
46pub fn diff(old: &Projection, new: &Projection, context: usize) -> Vec<Hunk> {
49 let ops = capture_diff_slices(Algorithm::Histogram, &old.lines, &new.lines);
50 let old_row = |i: usize, kind| Row {
51 kind,
52 old_line: Some(old.orig[i] + 1),
53 new_line: None,
54 text: old.lines[i].clone(),
55 emphasis: Vec::new(),
56 syntax: old.syntax.get(i).cloned().unwrap_or_default(),
57 };
58 let new_row = |j: usize| Row {
59 kind: RowKind::Added,
60 old_line: None,
61 new_line: Some(new.orig[j] + 1),
62 text: new.lines[j].clone(),
63 emphasis: Vec::new(),
64 syntax: new.syntax.get(j).cloned().unwrap_or_default(),
65 };
66
67 group_diff_ops(ops, context)
68 .into_iter()
69 .map(|group| {
70 let mut rows = Vec::new();
71 for op in group {
72 let (tag, old_range, new_range) = op.as_tag_tuple();
73 match tag {
74 DiffTag::Equal => {
75 for (i, j) in old_range.zip(new_range) {
76 rows.push(Row {
77 new_line: Some(new.orig[j] + 1),
78 ..old_row(i, RowKind::Context)
79 });
80 }
81 }
82 DiffTag::Delete => rows.extend(old_range.map(|i| old_row(i, RowKind::Removed))),
83 DiffTag::Insert => rows.extend(new_range.map(new_row)),
84 DiffTag::Replace => {
85 let removed_at = rows.len();
86 let pairs = old_range.len().min(new_range.len());
87 rows.extend(old_range.map(|i| old_row(i, RowKind::Removed)));
88 let added_at = rows.len();
89 rows.extend(new_range.map(new_row));
90 for k in 0..pairs {
92 let (old_text, new_text) =
93 (&rows[removed_at + k].text, &rows[added_at + k].text);
94 if let Some((old_marks, new_marks)) = inline_changes(old_text, new_text)
95 {
96 rows[removed_at + k].emphasis = old_marks;
97 rows[added_at + k].emphasis = new_marks;
98 }
99 }
100 }
101 }
102 }
103 Hunk { rows }
104 })
105 .collect()
106}
107
108pub type Marks = Vec<Range<usize>>;
110
111pub fn inline_changes(old: &str, new: &str) -> Option<(Marks, Marks)> {
114 let (old_tokens, new_tokens) = (tokens(old), tokens(new));
115 let old_text: Vec<&str> = old_tokens.iter().map(|range| &old[range.clone()]).collect();
116 let new_text: Vec<&str> = new_tokens.iter().map(|range| &new[range.clone()]).collect();
117
118 let mut old_marks: Vec<Range<usize>> = Vec::new();
119 let mut new_marks: Vec<Range<usize>> = Vec::new();
120 for op in capture_diff_slices(Algorithm::Myers, &old_text, &new_text) {
121 let (tag, old_range, new_range) = op.as_tag_tuple();
122 if tag == DiffTag::Equal {
123 continue;
124 }
125 if let Some(span) = span_of(&old_tokens, old_range) {
126 push_merged(&mut old_marks, span);
127 }
128 if let Some(span) = span_of(&new_tokens, new_range) {
129 push_merged(&mut new_marks, span);
130 }
131 }
132
133 let changed = |marks: &[Range<usize>], line: &str| {
134 let line = line.trim();
135 let bytes: usize = marks.iter().map(|m| m.len()).sum();
136 line.is_empty() || bytes * 10 > line.len() * 6
137 };
138 if changed(&old_marks, old) && changed(&new_marks, new) {
139 return None;
140 }
141 Some((old_marks, new_marks))
142}
143
144pub(crate) fn tokens(line: &str) -> Vec<Range<usize>> {
146 let class = |c: char| {
147 if c.is_alphanumeric() || c == '_' {
148 0
149 } else if c.is_whitespace() {
150 1
151 } else {
152 2
153 }
154 };
155 let mut out: Vec<Range<usize>> = Vec::new();
156 let mut previous = None;
157 for (i, c) in line.char_indices() {
158 let kind = class(c);
159 match out.last_mut() {
160 Some(last) if previous == Some(kind) && kind != 2 => last.end = i + c.len_utf8(),
161 _ => out.push(i..i + c.len_utf8()),
162 }
163 previous = Some(kind);
164 }
165 out
166}
167
168fn span_of(tokens: &[Range<usize>], range: Range<usize>) -> Option<Range<usize>> {
169 if range.is_empty() {
170 return None;
171 }
172 Some(tokens[range.start].start..tokens[range.end - 1].end)
173}
174
175fn push_merged(marks: &mut Vec<Range<usize>>, span: Range<usize>) {
176 match marks.last_mut() {
177 Some(last) if last.end >= span.start => last.end = last.end.max(span.end),
178 _ => marks.push(span),
179 }
180}
181
182pub fn filter_formatting(hunks: &mut Vec<Hunk>, mode: LayerMode, indentation_matters: bool) {
186 if mode == LayerMode::Shown {
187 return;
188 }
189 for hunk in hunks.iter_mut() {
190 let formatting = formatting_rows(&hunk.rows, indentation_matters);
191 let rows = std::mem::take(&mut hunk.rows);
192 hunk.rows = rows
193 .into_iter()
194 .zip(formatting)
195 .filter_map(
196 |(row, is_formatting)| match (mode, row.kind, is_formatting) {
197 (_, RowKind::Context, _) => Some(row),
198 (LayerMode::Hidden, RowKind::Added, true) => Some(Row {
200 kind: RowKind::Context,
201 old_line: None,
202 emphasis: Vec::new(),
203 ..row
204 }),
205 (LayerMode::Hidden, _, true) => None,
206 (LayerMode::Only, _, false) => None,
207 _ => Some(row),
208 },
209 )
210 .collect();
211 }
212 hunks.retain(|hunk| hunk.changed().next().is_some());
213}
214
215fn formatting_rows(rows: &[Row], indentation_matters: bool) -> Vec<bool> {
219 let mut marks = vec![false; rows.len()];
220 let mut start = 0;
221 while start < rows.len() {
222 if rows[start].kind == RowKind::Context {
223 start += 1;
224 continue;
225 }
226 let end = (start..rows.len())
227 .find(|&i| rows[i].kind == RowKind::Context)
228 .unwrap_or(rows.len());
229 let removed: Vec<usize> = (start..end)
230 .filter(|&i| rows[i].kind == RowKind::Removed)
231 .collect();
232 let added: Vec<usize> = (start..end)
233 .filter(|&i| rows[i].kind == RowKind::Added)
234 .collect();
235 let stream = |indices: &[usize]| -> Vec<String> {
236 indices
237 .iter()
238 .flat_map(|&i| significant_tokens(&rows[i].text, indentation_matters))
239 .collect()
240 };
241 if !(removed.is_empty() && added.is_empty()) {
242 if stream(&removed) == stream(&added) {
244 marks[start..end].iter_mut().for_each(|mark| *mark = true);
245 } else if removed.len() == added.len() {
246 for (&old, &new) in removed.iter().zip(&added) {
247 if stream(&[old]) == stream(&[new]) {
248 marks[old] = true;
249 marks[new] = true;
250 }
251 }
252 }
253 }
254 start = end;
255 }
256 marks
257}
258
259fn significant_tokens(line: &str, indentation_matters: bool) -> Vec<String> {
262 let mut out = Vec::new();
263 if indentation_matters && !line.trim().is_empty() {
264 out.push(format!(
265 "indent:{}",
266 &line[..line.len() - line.trim_start().len()]
267 ));
268 }
269 out.extend(
270 tokens(line)
271 .into_iter()
272 .map(|range| &line[range])
273 .filter(|token| !token.trim().is_empty())
274 .map(str::to_string),
275 );
276 out
277}