1use std::ops::Range;
2
3use sum_tree::{Bias, SumTree};
4use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToPoint};
5
6pub use git2 as libgit;
7use libgit::{DiffOptions as GitOptions, Patch as GitPatch};
8
9#[derive(Debug, Clone, Copy)]
10pub enum DiffHunkStatus {
11 Added,
12 Modified,
13 Removed,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct DiffHunk<T> {
18 pub buffer_range: Range<T>,
19 pub head_range: Range<usize>,
20}
21
22impl DiffHunk<u32> {
23 pub fn status(&self) -> DiffHunkStatus {
24 if self.head_range.is_empty() {
25 DiffHunkStatus::Added
26 } else if self.buffer_range.is_empty() {
27 DiffHunkStatus::Removed
28 } else {
29 DiffHunkStatus::Modified
30 }
31 }
32}
33
34impl sum_tree::Item for DiffHunk<Anchor> {
35 type Summary = DiffHunkSummary;
36
37 fn summary(&self) -> Self::Summary {
38 DiffHunkSummary {
39 buffer_range: self.buffer_range.clone(),
40 head_range: self.head_range.clone(),
41 }
42 }
43}
44
45#[derive(Debug, Default, Clone)]
46pub struct DiffHunkSummary {
47 buffer_range: Range<Anchor>,
48 head_range: Range<usize>,
49}
50
51impl sum_tree::Summary for DiffHunkSummary {
52 type Context = text::BufferSnapshot;
53
54 fn add_summary(&mut self, other: &Self, _: &Self::Context) {
55 self.head_range.start = self.head_range.start.min(other.head_range.start);
56 self.head_range.end = self.head_range.end.max(other.head_range.end);
57 }
58}
59
60#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
61struct HunkHeadEnd(usize);
62
63impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkHeadEnd {
64 fn add_summary(&mut self, summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) {
65 self.0 = summary.head_range.end;
66 }
67
68 fn from_summary(summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) -> Self {
69 HunkHeadEnd(summary.head_range.end)
70 }
71}
72
73#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
74struct HunkBufferEnd(u32);
75
76impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd {
77 fn add_summary(&mut self, summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) {
78 self.0 = summary.buffer_range.end.to_point(buffer).row;
79 }
80
81 fn from_summary(summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) -> Self {
82 HunkBufferEnd(summary.buffer_range.end.to_point(buffer).row)
83 }
84}
85
86#[derive(Clone)]
87pub struct BufferDiffSnapshot {
88 tree: SumTree<DiffHunk<Anchor>>,
89}
90
91impl BufferDiffSnapshot {
92 pub fn hunks_in_range<'a>(
93 &'a self,
94 query_row_range: Range<u32>,
95 buffer: &'a BufferSnapshot,
96 ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
97 // println!("{} hunks overall", self.tree.iter().count());
98
99 self.tree.iter().filter_map(move |hunk| {
100 let range = hunk.buffer_range.to_point(&buffer);
101
102 if range.start.row < query_row_range.end && query_row_range.start < range.end.row {
103 let end_row = if range.end.column > 0 {
104 range.end.row + 1
105 } else {
106 range.end.row
107 };
108
109 Some(DiffHunk {
110 buffer_range: range.start.row..end_row,
111 head_range: hunk.head_range.clone(),
112 })
113 } else {
114 None
115 }
116 })
117 }
118
119 #[cfg(test)]
120 fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
121 self.hunks_in_range(0..u32::MAX, text)
122 }
123}
124
125pub struct BufferDiff {
126 last_update_version: clock::Global,
127 snapshot: BufferDiffSnapshot,
128}
129
130impl BufferDiff {
131 pub fn new(head_text: &Option<String>, buffer: &text::BufferSnapshot) -> BufferDiff {
132 let mut instance = BufferDiff {
133 last_update_version: buffer.version().clone(),
134 snapshot: BufferDiffSnapshot {
135 tree: SumTree::new(),
136 },
137 };
138
139 if let Some(head_text) = head_text {
140 instance.update(head_text, buffer);
141 }
142
143 instance
144 }
145
146 pub fn snapshot(&self) -> BufferDiffSnapshot {
147 self.snapshot.clone()
148 }
149
150 pub fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) {
151 let buffer_string = buffer.as_rope().to_string();
152 let buffer_bytes = buffer_string.as_bytes();
153
154 let mut options = GitOptions::default();
155 options.context_lines(0);
156 let patch = match GitPatch::from_buffers(
157 head_text.as_bytes(),
158 None,
159 buffer_bytes,
160 None,
161 Some(&mut options),
162 ) {
163 Ok(patch) => patch,
164 Err(_) => todo!("This needs to be handled"),
165 };
166
167 let mut hunks = SumTree::<DiffHunk<Anchor>>::new();
168 let mut delta = 0i64;
169 for hunk_index in 0..patch.num_hunks() {
170 for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() {
171 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
172
173 let hunk = match line.origin_value() {
174 libgit::DiffLineType::Addition => {
175 let buffer_start = line.content_offset();
176 let buffer_end = buffer_start as usize + line.content().len();
177 let head_offset = (buffer_start - delta) as usize;
178 delta += line.content().len() as i64;
179 DiffHunk {
180 buffer_range: buffer.anchor_before(buffer_start as usize)
181 ..buffer.anchor_after(buffer_end),
182 head_range: head_offset..head_offset,
183 }
184 }
185
186 libgit::DiffLineType::Deletion => {
187 let head_start = line.content_offset();
188 let head_end = head_start as usize + line.content().len();
189 let buffer_offset = (head_start + delta) as usize;
190 delta -= line.content().len() as i64;
191 DiffHunk {
192 buffer_range: buffer.anchor_before(buffer_offset)
193 ..buffer.anchor_after(buffer_offset),
194 head_range: (head_start as usize)..head_end,
195 }
196 }
197
198 libgit::DiffLineType::AddEOFNL => todo!(),
199 libgit::DiffLineType::ContextEOFNL => todo!(),
200 libgit::DiffLineType::DeleteEOFNL => todo!(),
201
202 libgit::DiffLineType::FileHeader => continue,
203 libgit::DiffLineType::HunkHeader => continue,
204 libgit::DiffLineType::Binary => continue,
205
206 //We specifically tell git to not give us context lines
207 libgit::DiffLineType::Context => unreachable!(),
208 };
209
210 let mut combined = false;
211 hunks.update_last(
212 |last_hunk| {
213 if last_hunk.head_range.end == hunk.head_range.start {
214 last_hunk.head_range.end = hunk.head_range.end;
215 last_hunk.buffer_range.end = hunk.buffer_range.end;
216 combined = true;
217 }
218 },
219 buffer,
220 );
221 if !combined {
222 hunks.push(hunk, buffer);
223 }
224 }
225 }
226
227 self.snapshot.tree = hunks;
228 }
229}
230
231#[derive(Debug, Clone, Copy)]
232pub enum GitDiffEdit {
233 Added(u32),
234 Modified(u32),
235 Removed(u32),
236}
237
238impl GitDiffEdit {
239 pub fn line(self) -> u32 {
240 use GitDiffEdit::*;
241
242 match self {
243 Added(line) | Modified(line) | Removed(line) => line,
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use text::Buffer;
252 use unindent::Unindent as _;
253
254 #[gpui::test]
255 fn test_buffer_diff_simple() {
256 let head_text = "
257 one
258 two
259 three
260 "
261 .unindent();
262
263 let buffer_text = "
264 one
265 hello
266 three
267 "
268 .unindent();
269
270 let mut buffer = Buffer::new(0, 0, buffer_text);
271 let diff = BufferDiff::new(&Some(head_text.clone()), &buffer);
272 assert_hunks(&diff, &buffer, &head_text, &[(1..2, "two\n")]);
273
274 buffer.edit([(0..0, "point five\n")]);
275 assert_hunks(&diff, &buffer, &head_text, &[(2..3, "two\n")]);
276 }
277
278 #[track_caller]
279 fn assert_hunks(
280 diff: &BufferDiff,
281 buffer: &BufferSnapshot,
282 head_text: &str,
283 expected_hunks: &[(Range<u32>, &str)],
284 ) {
285 let hunks = diff.snapshot.hunks(buffer).collect::<Vec<_>>();
286 assert_eq!(
287 hunks.len(),
288 expected_hunks.len(),
289 "actual hunks are {hunks:#?}"
290 );
291
292 let diff_iter = hunks.iter().enumerate();
293 for ((index, hunk), (expected_range, expected_str)) in diff_iter.zip(expected_hunks) {
294 assert_eq!(&hunk.buffer_range, expected_range, "for hunk {index}");
295 assert_eq!(
296 &head_text[hunk.head_range.clone()],
297 *expected_str,
298 "for hunk {index}"
299 );
300 }
301 }
302
303 // use rand::rngs::StdRng;
304 // #[gpui::test(iterations = 100)]
305 // fn test_buffer_diff_random(mut rng: StdRng) {}
306}