git.rs

  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}