objects/worktree/
worktree_diff.rs1use crate::object::Blob;
5
6pub fn diff_blobs(old: &Blob, new: &Blob) -> Vec<DiffLine> {
8 let Some(old_text) = old.content_str() else {
9 return Vec::new();
10 };
11 let Some(new_text) = new.content_str() else {
12 return Vec::new();
13 };
14
15 let diff = similar::TextDiff::configure()
16 .algorithm(similar::Algorithm::Histogram)
17 .diff_lines(old_text, new_text)
18 .iter_all_changes()
19 .map(|change| {
20 let content = change.value().trim_end_matches('\n').to_string();
21 match change.tag() {
22 similar::ChangeTag::Delete => DiffLine::Removed(content),
23 similar::ChangeTag::Insert => DiffLine::Added(content),
24 similar::ChangeTag::Equal => DiffLine::Context(content),
25 }
26 })
27 .collect();
28 keep_annotations_with_inserted_items(diff)
29}
30
31fn keep_annotations_with_inserted_items(lines: Vec<DiffLine>) -> Vec<DiffLine> {
32 let mut output = Vec::with_capacity(lines.len());
33 let mut index = 0;
34
35 while index < lines.len() {
36 let Some(DiffLine::Context(annotation)) = lines.get(index) else {
37 output.push(lines[index].clone());
38 index += 1;
39 continue;
40 };
41
42 if !is_decoration_line(annotation) {
43 output.push(lines[index].clone());
44 index += 1;
45 continue;
46 }
47
48 let added_start = index + 1;
49 let mut added_end = added_start;
50 while matches!(lines.get(added_end), Some(DiffLine::Added(_))) {
51 added_end += 1;
52 }
53
54 let has_inserted_item = first_meaningful_added_line(&lines[added_start..added_end])
55 .is_some_and(is_item_declaration_line);
56 let decorates_next_context = matches!(
57 lines.get(added_end),
58 Some(DiffLine::Context(next)) if is_item_declaration_line(next)
59 );
60
61 if added_end > added_start && has_inserted_item && decorates_next_context {
62 output.push(DiffLine::Added(annotation.clone()));
63 output.extend(lines[added_start..added_end].iter().cloned());
64 output.push(lines[index].clone());
65 index = added_end;
66 continue;
67 }
68
69 output.push(lines[index].clone());
70 index += 1;
71 }
72
73 output
74}
75
76fn first_meaningful_added_line(lines: &[DiffLine]) -> Option<&str> {
77 lines.iter().find_map(|line| match line {
78 DiffLine::Added(content) if !content.trim().is_empty() => Some(content.as_str()),
79 _ => None,
80 })
81}
82
83fn is_attribute_line(line: &str) -> bool {
84 let trimmed = line.trim_start();
85 trimmed.starts_with("#[") || trimmed.starts_with("#![")
86}
87
88fn is_decoration_line(line: &str) -> bool {
89 let trimmed = line.trim_start();
90 is_attribute_line(line)
91 || trimmed.starts_with('@')
92 || trimmed.starts_with("///")
93 || trimmed.starts_with("//!")
94 || trimmed.starts_with("/**")
95 || trimmed.starts_with('*')
96 || trimmed.starts_with("\"\"\"")
97 || trimmed.starts_with("'''")
98}
99
100fn is_item_declaration_line(line: &str) -> bool {
101 let trimmed = line.trim_start();
102 matches!(
103 trimmed.split_whitespace().next(),
104 Some(
105 "fn" | "pub"
106 | "async"
107 | "const"
108 | "struct"
109 | "enum"
110 | "trait"
111 | "impl"
112 | "mod"
113 | "type"
114 | "def"
115 | "class"
116 | "function"
117 | "export"
118 | "let"
119 | "var"
120 )
121 )
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum DiffLine {
127 Context(String),
129 Added(String),
131 Removed(String),
133}
134
135impl DiffLine {
136 pub fn prefix(&self) -> &'static str {
138 match self {
139 DiffLine::Context(_) => " ",
140 DiffLine::Added(_) => "+",
141 DiffLine::Removed(_) => "-",
142 }
143 }
144
145 pub fn content(&self) -> &str {
147 match self {
148 DiffLine::Context(s) | DiffLine::Added(s) | DiffLine::Removed(s) => s,
149 }
150 }
151}