diff --git a/Cargo.lock b/Cargo.lock index db8e88cb1cc02a7dd66bdd87a900096e5a9e2c2d..8b7ea1c6414ab053ad1730715683b07655a24b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3079,7 +3079,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "text", + "text2", "time", "util", ] @@ -3371,6 +3371,26 @@ dependencies = [ "url", ] +[[package]] +name = "git3" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clock", + "collections", + "futures 0.3.28", + "git2", + "lazy_static", + "log", + "parking_lot 0.11.2", + "smol", + "sum_tree", + "text2", + "unindent", + "util", +] + [[package]] name = "glob" version = "0.3.1" @@ -4345,7 +4365,7 @@ dependencies = [ "env_logger 0.9.3", "futures 0.3.28", "fuzzy2", - "git", + "git3", "globset", "gpui2", "indoc", @@ -4366,7 +4386,7 @@ dependencies = [ "smallvec", "smol", "sum_tree", - "text", + "text2", "theme2", "tree-sitter", "tree-sitter-elixir", @@ -5081,7 +5101,7 @@ dependencies = [ "ctor", "env_logger 0.9.3", "futures 0.3.28", - "git", + "git3", "gpui2", "indoc", "itertools 0.10.5", @@ -5104,7 +5124,7 @@ dependencies = [ "smol", "snippet", "sum_tree", - "text", + "text2", "theme2", "tree-sitter", "tree-sitter-html", @@ -6284,8 +6304,8 @@ dependencies = [ "fsevent", "futures 0.3.28", "fuzzy2", - "git", "git2", + "git3", "globset", "gpui2", "ignore", @@ -6313,7 +6333,7 @@ dependencies = [ "sum_tree", "tempdir", "terminal2", - "text", + "text2", "thiserror", "toml 0.5.11", "unindent", diff --git a/crates/client2/Cargo.toml b/crates/client2/Cargo.toml index 45e1f618d2f5d29d33741f8f6a0d90b66e111057..ace229bc210a41243d3c59f6c8add3d4a63ce729 100644 --- a/crates/client2/Cargo.toml +++ b/crates/client2/Cargo.toml @@ -17,7 +17,7 @@ db = { package = "db2", path = "../db2" } gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } rpc = { package = "rpc2", path = "../rpc2" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" } sum_tree = { path = "../sum_tree" } diff --git a/crates/fs2/Cargo.toml b/crates/fs2/Cargo.toml index 636def05ec750b4e7fddd26baa987b15b2b70f80..ca525afe5fbe8cdc122e5072fc7022c14ae74b79 100644 --- a/crates/fs2/Cargo.toml +++ b/crates/fs2/Cargo.toml @@ -10,7 +10,7 @@ path = "src/fs2.rs" [dependencies] collections = { path = "../collections" } rope = { path = "../rope" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } diff --git a/crates/git3/Cargo.toml b/crates/git3/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e88fa6574dac961ab634f437ff4032a40da4f815 --- /dev/null +++ b/crates/git3/Cargo.toml @@ -0,0 +1,30 @@ +[package] +# git2 was already taken. +name = "git3" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/git.rs" + +[dependencies] +anyhow.workspace = true +clock = { path = "../clock" } +lazy_static.workspace = true +sum_tree = { path = "../sum_tree" } +text = { package = "text2", path = "../text2" } +collections = { path = "../collections" } +util = { path = "../util" } +log.workspace = true +smol.workspace = true +parking_lot.workspace = true +async-trait.workspace = true +futures.workspace = true +git2.workspace = true + +[dev-dependencies] +unindent.workspace = true + +[features] +test-support = [] diff --git a/crates/git3/src/diff.rs b/crates/git3/src/diff.rs new file mode 100644 index 0000000000000000000000000000000000000000..39383cfc78b297e355c1d4c096219069cd92b1da --- /dev/null +++ b/crates/git3/src/diff.rs @@ -0,0 +1,412 @@ +use std::{iter, ops::Range}; +use sum_tree::SumTree; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; + +pub use git2 as libgit; +use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiffHunkStatus { + Added, + Modified, + Removed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DiffHunk { + pub buffer_range: Range, + pub diff_base_byte_range: Range, +} + +impl DiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.diff_base_byte_range.is_empty() { + DiffHunkStatus::Added + } else if self.buffer_range.is_empty() { + DiffHunkStatus::Removed + } else { + DiffHunkStatus::Modified + } + } +} + +impl sum_tree::Item for DiffHunk { + type Summary = DiffHunkSummary; + + fn summary(&self) -> Self::Summary { + DiffHunkSummary { + buffer_range: self.buffer_range.clone(), + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct DiffHunkSummary { + buffer_range: Range, +} + +impl sum_tree::Summary for DiffHunkSummary { + type Context = text::BufferSnapshot; + + fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { + self.buffer_range.start = self + .buffer_range + .start + .min(&other.buffer_range.start, buffer); + self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer); + } +} + +#[derive(Clone)] +pub struct BufferDiff { + last_buffer_version: Option, + tree: SumTree>, +} + +impl BufferDiff { + pub fn new() -> BufferDiff { + BufferDiff { + last_buffer_version: None, + tree: SumTree::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.tree.is_empty() + } + + pub fn hunks_in_row_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let start = buffer.anchor_before(Point::new(range.start, 0)); + let end = buffer.anchor_after(Point::new(range.end, 0)); + + self.hunks_intersecting_range(start..end, buffer) + } + + pub fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + let anchor_iter = std::iter::from_fn(move || { + cursor.next(buffer); + cursor.item() + }) + .flat_map(move |hunk| { + [ + (&hunk.buffer_range.start, hunk.diff_base_byte_range.start), + (&hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ] + .into_iter() + }); + + let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); + iter::from_fn(move || { + let (start_point, start_base) = summaries.next()?; + let (end_point, end_base) = summaries.next()?; + + let end_row = if end_point.column > 0 { + end_point.row + 1 + } else { + end_point.row + }; + + Some(DiffHunk { + buffer_range: start_point.row..end_row, + diff_base_byte_range: start_base..end_base, + }) + }) + } + + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + std::iter::from_fn(move || { + cursor.prev(buffer); + + let hunk = cursor.item()?; + let range = hunk.buffer_range.to_point(buffer); + let end_row = if range.end.column > 0 { + range.end.row + 1 + } else { + range.end.row + }; + + Some(DiffHunk { + buffer_range: range.start.row..end_row, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }) + } + + pub fn clear(&mut self, buffer: &text::BufferSnapshot) { + self.last_buffer_version = Some(buffer.version().clone()); + self.tree = SumTree::new(); + } + + pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) { + let mut tree = SumTree::new(); + + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(&diff_base, &buffer_text); + + if let Some(patch) = patch { + let mut divergence = 0; + for hunk_index in 0..patch.num_hunks() { + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence); + tree.push(hunk, buffer); + } + } + + self.tree = tree; + self.last_buffer_version = Some(buffer.version().clone()); + } + + #[cfg(test)] + fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { + let start = text.anchor_before(Point::new(0, 0)); + let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); + self.hunks_intersecting_range(start..end, text) + } + + fn diff<'a>(head: &'a str, current: &'a str) -> Option> { + let mut options = GitOptions::default(); + options.context_lines(0); + + let patch = GitPatch::from_buffers( + head.as_bytes(), + None, + current.as_bytes(), + None, + Some(&mut options), + ); + + match patch { + Ok(patch) => Some(patch), + + Err(err) => { + log::error!("`GitPatch::from_buffers` failed: {}", err); + None + } + } + } + + fn process_patch_hunk<'a>( + patch: &GitPatch<'a>, + hunk_index: usize, + buffer: &text::BufferSnapshot, + buffer_row_divergence: &mut i64, + ) -> DiffHunk { + let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap(); + assert!(line_item_count > 0); + + let mut first_deletion_buffer_row: Option = None; + let mut buffer_row_range: Option> = None; + let mut diff_base_byte_range: Option> = None; + + for line_index in 0..line_item_count { + let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); + let kind = line.origin_value(); + let content_offset = line.content_offset() as isize; + let content_len = line.content().len() as isize; + + if kind == GitDiffLineType::Addition { + *buffer_row_divergence += 1; + let row = line.new_lineno().unwrap().saturating_sub(1); + + match &mut buffer_row_range { + Some(buffer_row_range) => buffer_row_range.end = row + 1, + None => buffer_row_range = Some(row..row + 1), + } + } + + if kind == GitDiffLineType::Deletion { + let end = content_offset + content_len; + + match &mut diff_base_byte_range { + Some(head_byte_range) => head_byte_range.end = end as usize, + None => diff_base_byte_range = Some(content_offset as usize..end as usize), + } + + if first_deletion_buffer_row.is_none() { + let old_row = line.old_lineno().unwrap().saturating_sub(1); + let row = old_row as i64 + *buffer_row_divergence; + first_deletion_buffer_row = Some(row as u32); + } + + *buffer_row_divergence -= 1; + } + } + + //unwrap_or deletion without addition + let buffer_row_range = buffer_row_range.unwrap_or_else(|| { + //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk + let row = first_deletion_buffer_row.unwrap(); + row..row + }); + + //unwrap_or addition without deletion + let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0); + + let start = Point::new(buffer_row_range.start, 0); + let end = Point::new(buffer_row_range.end, 0); + let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end); + DiffHunk { + buffer_range, + diff_base_byte_range, + } + } +} + +/// Range (crossing new lines), old, new +#[cfg(any(test, feature = "test-support"))] +#[track_caller] +pub fn assert_hunks( + diff_hunks: Iter, + buffer: &BufferSnapshot, + diff_base: &str, + expected_hunks: &[(Range, &str, &str)], +) where + Iter: Iterator>, +{ + let actual_hunks = diff_hunks + .map(|hunk| { + ( + hunk.buffer_range.clone(), + &diff_base[hunk.diff_base_byte_range], + buffer + .text_for_range( + Point::new(hunk.buffer_range.start, 0) + ..Point::new(hunk.buffer_range.end, 0), + ) + .collect::(), + ) + }) + .collect::>(); + + let expected_hunks: Vec<_> = expected_hunks + .iter() + .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .collect(); + + assert_eq!(actual_hunks, expected_hunks); +} + +#[cfg(test)] +mod tests { + use std::assert_eq; + + use super::*; + use text::Buffer; + use unindent::Unindent as _; + + #[test] + fn test_buffer_diff_simple() { + let diff_base = " + one + two + three + " + .unindent(); + + let buffer_text = " + one + HELLO + three + " + .unindent(); + + let mut buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(1..2, "two\n", "HELLO\n")], + ); + + buffer.edit([(0..0, "point five\n")]); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], + ); + + diff.clear(&buffer); + assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]); + } + + #[test] + fn test_buffer_diff_range() { + let diff_base = " + one + two + three + four + five + six + seven + eight + nine + ten + " + .unindent(); + + let buffer_text = " + A + one + B + two + C + three + HELLO + four + five + SIXTEEN + seven + eight + WORLD + nine + + ten + + " + .unindent(); + + let buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_eq!(diff.hunks(&buffer).count(), 8); + + assert_hunks( + diff.hunks_in_row_range(7..12, &buffer), + &buffer, + &diff_base, + &[ + (6..7, "", "HELLO\n"), + (9..10, "six\n", "SIXTEEN\n"), + (12..13, "", "WORLD\n"), + ], + ); + } +} diff --git a/crates/git3/src/git.rs b/crates/git3/src/git.rs new file mode 100644 index 0000000000000000000000000000000000000000..b1b885eca2c93e43f1e3f868e13f25d68fdb4097 --- /dev/null +++ b/crates/git3/src/git.rs @@ -0,0 +1,11 @@ +use std::ffi::OsStr; + +pub use git2 as libgit; +pub use lazy_static::lazy_static; + +pub mod diff; + +lazy_static! { + pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); + pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); +} diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index 4fca16bcb595c3e90e6e19b04b6cc2916d35aa0d..0e4d9addfa084b54afc6658802a07c99a008c02e 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -25,13 +25,13 @@ test-support = [ clock = { path = "../clock" } collections = { path = "../collections" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -git = { path = "../git" } +git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } lsp = { package = "lsp2", path = "../lsp2" } rpc = { package = "rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } @@ -64,7 +64,7 @@ client = { package = "client2", path = "../client2", features = ["test-support"] collections = { path = "../collections", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } -text = { path = "../text", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor.workspace = true diff --git a/crates/multi_buffer2/Cargo.toml b/crates/multi_buffer2/Cargo.toml index a57ef29531ca5aeabc791bed20e490c98e4607a7..4b69edd5a813c4979f6d63bccc9f09c655645171 100644 --- a/crates/multi_buffer2/Cargo.toml +++ b/crates/multi_buffer2/Cargo.toml @@ -23,7 +23,7 @@ test-support = [ client = { package = "client2", path = "../client2" } clock = { path = "../clock" } collections = { path = "../collections" } -git = { path = "../git" } +git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } lsp = { package = "lsp2", path = "../lsp2" } @@ -31,7 +31,7 @@ rich_text = { path = "../rich_text" } settings = { package = "settings2", path = "../settings2" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } @@ -60,7 +60,7 @@ tree-sitter-typescript = { workspace = true, optional = true } [dev-dependencies] copilot = { package = "copilot2", path = "../copilot2", features = ["test-support"] } -text = { path = "../text", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/project2/Cargo.toml b/crates/project2/Cargo.toml index 7aae9fb0076d529fcb6605d78196cca6fb9a091a..892ddb91c7d75d472bac5c5f2f7560690496cffc 100644 --- a/crates/project2/Cargo.toml +++ b/crates/project2/Cargo.toml @@ -20,7 +20,7 @@ test-support = [ ] [dependencies] -text = { path = "../text" } +text = { package = "text2", path = "../text2" } copilot = { package = "copilot2", path = "../copilot2" } client = { package = "client2", path = "../client2" } clock = { path = "../clock" } @@ -29,7 +29,7 @@ db = { package = "db2", path = "../db2" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -git = { path = "../git" } +git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } lsp = { package = "lsp2", path = "../lsp2" }