diff --git a/Cargo.lock b/Cargo.lock index bf0135a582..f14b26d7da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,6 +2753,7 @@ dependencies = [ "ctor", "dashmap 6.1.0", "derive_more", + "diff 0.1.0", "editor", "env_logger 0.11.6", "envy", @@ -3841,14 +3842,18 @@ dependencies = [ name = "diff" version = "0.1.0" dependencies = [ + "futures 0.3.31", "git2", "gpui", "language", + "log", "pretty_assertions", + "rope", "serde_json", "sum_tree", "text", "unindent", + "util", ] [[package]] @@ -4021,6 +4026,7 @@ dependencies = [ "convert_case 0.7.1", "ctor", "db", + "diff 0.1.0", "emojis", "env_logger 0.11.6", "file_icons", @@ -5320,6 +5326,7 @@ dependencies = [ "anyhow", "collections", "db", + "diff 0.1.0", "editor", "feature_flags", "futures 0.3.31", @@ -7924,6 +7931,7 @@ dependencies = [ "clock", "collections", "ctor", + "diff 0.1.0", "env_logger 0.11.6", "futures 0.3.31", "git", @@ -10029,6 +10037,7 @@ dependencies = [ "client", "clock", "collections", + "diff 0.1.0", "env_logger 0.11.6", "fancy-regex 0.14.0", "fs", diff --git a/Cargo.toml b/Cargo.toml index 457d8f6caf..e108955b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -232,6 +232,7 @@ copilot = { path = "crates/copilot" } db = { path = "crates/db" } deepseek = { path = "crates/deepseek" } diagnostics = { path = "crates/diagnostics" } +diff = { path = "crates/diff" } editor = { path = "crates/editor" } extension = { path = "crates/extension" } extension_host = { path = "crates/extension_host" } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index db293c5173..7d61621c95 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -33,6 +33,7 @@ clock.workspace = true collections.workspace = true dashmap.workspace = true derive_more.workspace = true +diff.workspace = true envy = "0.4.2" futures.workspace = true google_ai.workspace = true diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a512a9f10c..c7ab1b6811 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2613,7 +2613,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2641,7 +2641,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2663,7 +2663,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(committed_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2689,7 +2689,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2703,7 +2703,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2717,7 +2717,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(new_committed_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2763,7 +2763,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &change_set.base_text_string().unwrap(), @@ -2790,7 +2790,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &staged_text, @@ -2812,7 +2812,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &new_staged_text, @@ -2826,7 +2826,7 @@ async fn test_git_diff_base_change( change_set.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - git::diff::assert_hunks( + diff::assert_hunks( change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &new_staged_text, diff --git a/crates/diff/Cargo.toml b/crates/diff/Cargo.toml new file mode 100644 index 0000000000..6641fdf1cb --- /dev/null +++ b/crates/diff/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "diff" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/diff.rs" + +[dependencies] +futures.workspace = true +git2.workspace = true +gpui.workspace = true +language.workspace = true +log.workspace = true +rope.workspace = true +sum_tree.workspace = true +text.workspace = true +util.workspace = true + +[dev-dependencies] +unindent.workspace = true +serde_json.workspace = true +pretty_assertions.workspace = true +text = {workspace = true, features = ["test-support"]} + +[features] +test-support = [] diff --git a/crates/diff/LICENSE-GPL b/crates/diff/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/diff/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/diff/src/diff.rs b/crates/diff/src/diff.rs new file mode 100644 index 0000000000..bce19a8892 --- /dev/null +++ b/crates/diff/src/diff.rs @@ -0,0 +1,801 @@ +use futures::channel::oneshot; +use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; +use gpui::{App, Context, Entity, EventEmitter}; +use rope::Rope; +use std::{cmp, iter, ops::Range}; +use sum_tree::SumTree; +use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point}; +use util::ResultExt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DiffHunkStatus { + Added, + Modified, + Removed, +} + +/// A diff hunk resolved to rows in the buffer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DiffHunk { + /// The buffer range, expressed in terms of rows. + pub row_range: Range, + /// The range in the buffer to which this hunk corresponds. + pub buffer_range: Range, + /// The range in the buffer's diff base text to which this hunk corresponds. + pub diff_base_byte_range: Range, +} + +/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. +#[derive(Debug, Clone, PartialEq, Eq)] +struct InternalDiffHunk { + buffer_range: Range, + diff_base_byte_range: Range, +} + +impl sum_tree::Item for InternalDiffHunk { + type Summary = DiffHunkSummary; + + fn summary(&self, _cx: &text::BufferSnapshot) -> 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 zero(_cx: &Self::Context) -> Self { + Default::default() + } + + 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(Debug, Clone)] +pub struct BufferDiffSnapshot { + tree: SumTree, +} + +impl BufferDiffSnapshot { + pub fn new(buffer: &BufferSnapshot) -> BufferDiffSnapshot { + BufferDiffSnapshot { + tree: SumTree::new(buffer), + } + } + + pub fn new_with_single_insertion(buffer: &BufferSnapshot) -> Self { + Self { + tree: SumTree::from_item( + InternalDiffHunk { + buffer_range: Anchor::MIN..Anchor::MAX, + diff_base_byte_range: 0..0, + }, + buffer, + ), + } + } + + pub fn build(diff_base: Option<&str>, buffer: &text::BufferSnapshot) -> Self { + let mut tree = SumTree::new(buffer); + + if let Some(diff_base) = diff_base { + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(diff_base, &buffer_text); + + // A common case in Zed is that the empty buffer is represented as just a newline, + // but if we just compute a naive diff you get a "preserved" line in the middle, + // which is a bit odd. + if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 { + tree.push( + InternalDiffHunk { + buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0), + diff_base_byte_range: 0..diff_base.len() - 1, + }, + buffer, + ); + return Self { tree }; + } + + 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 } + } + + 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 range = range.to_offset(buffer); + + let mut cursor = self + .tree + .filter::<_, DiffHunkSummary>(buffer, move |summary| { + let summary_range = summary.buffer_range.to_offset(buffer); + let before_start = summary_range.end < range.start; + let after_end = summary_range.start > range.end; + !before_start && !after_end + }); + + let anchor_iter = iter::from_fn(move || { + cursor.next(buffer); + cursor.item() + }) + .flat_map(move |hunk| { + [ + ( + &hunk.buffer_range.start, + (hunk.buffer_range.start, hunk.diff_base_byte_range.start), + ), + ( + &hunk.buffer_range.end, + (hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ), + ] + }); + + let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); + iter::from_fn(move || loop { + let (start_point, (start_anchor, start_base)) = summaries.next()?; + let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?; + + if !start_anchor.is_valid(buffer) { + continue; + } + + if end_point.column > 0 { + end_point.row += 1; + end_point.column = 0; + end_anchor = buffer.anchor_before(end_point); + } + + return Some(DiffHunk { + row_range: start_point.row..end_point.row, + diff_base_byte_range: start_base..end_base, + buffer_range: start_anchor..end_anchor, + }); + }) + } + + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator { + let mut cursor = self + .tree + .filter::<_, DiffHunkSummary>(buffer, 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 + }); + + 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 { + row_range: range.start.row..end_row, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + buffer_range: hunk.buffer_range.clone(), + }) + }) + } + + pub fn compare(&self, old: &Self, new_snapshot: &BufferSnapshot) -> Option> { + let mut new_cursor = self.tree.cursor::<()>(new_snapshot); + let mut old_cursor = old.tree.cursor::<()>(new_snapshot); + old_cursor.next(new_snapshot); + new_cursor.next(new_snapshot); + let mut start = None; + let mut end = None; + + loop { + match (new_cursor.item(), old_cursor.item()) { + (Some(new_hunk), Some(old_hunk)) => { + match new_hunk + .buffer_range + .start + .cmp(&old_hunk.buffer_range.start, new_snapshot) + { + cmp::Ordering::Less => { + start.get_or_insert(new_hunk.buffer_range.start); + end.replace(new_hunk.buffer_range.end); + new_cursor.next(new_snapshot); + } + cmp::Ordering::Equal => { + if new_hunk != old_hunk { + start.get_or_insert(new_hunk.buffer_range.start); + if old_hunk + .buffer_range + .end + .cmp(&new_hunk.buffer_range.end, new_snapshot) + .is_ge() + { + end.replace(old_hunk.buffer_range.end); + } else { + end.replace(new_hunk.buffer_range.end); + } + } + + new_cursor.next(new_snapshot); + old_cursor.next(new_snapshot); + } + cmp::Ordering::Greater => { + start.get_or_insert(old_hunk.buffer_range.start); + end.replace(old_hunk.buffer_range.end); + old_cursor.next(new_snapshot); + } + } + } + (Some(new_hunk), None) => { + start.get_or_insert(new_hunk.buffer_range.start); + end.replace(new_hunk.buffer_range.end); + new_cursor.next(new_snapshot); + } + (None, Some(old_hunk)) => { + start.get_or_insert(old_hunk.buffer_range.start); + end.replace(old_hunk.buffer_range.end); + old_cursor.next(new_snapshot); + } + (None, None) => break, + } + } + + start.zip(end).map(|(start, end)| start..end) + } + + #[cfg(test)] + fn clear(&mut self, buffer: &text::BufferSnapshot) { + self.tree = SumTree::new(buffer); + } + + pub fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) { + *self = Self::build(Some(&diff_base.to_string()), buffer); + } + + #[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( + patch: &GitPatch<'_>, + hunk_index: usize, + buffer: &text::BufferSnapshot, + buffer_row_divergence: &mut i64, + ) -> InternalDiffHunk { + 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); + InternalDiffHunk { + buffer_range, + diff_base_byte_range, + } + } +} + +pub struct BufferDiff { + pub buffer_id: BufferId, + pub base_text: Option, + pub diff_to_buffer: BufferDiffSnapshot, + pub unstaged_change_set: Option>, +} + +impl std::fmt::Debug for BufferDiff { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BufferChangeSet") + .field("buffer_id", &self.buffer_id) + .field("base_text", &self.base_text.as_ref().map(|s| s.text())) + .field("diff_to_buffer", &self.diff_to_buffer) + .finish() + } +} + +pub enum BufferDiffEvent { + DiffChanged { changed_range: Range }, + LanguageChanged, +} + +impl EventEmitter for BufferDiff {} + +impl BufferDiff { + pub fn set_state( + &mut self, + base_text: Option, + diff: BufferDiffSnapshot, + buffer: &text::BufferSnapshot, + cx: &mut Context, + ) { + if let Some(base_text) = base_text.as_ref() { + let changed_range = if Some(base_text.remote_id()) + != self.base_text.as_ref().map(|buffer| buffer.remote_id()) + { + Some(text::Anchor::MIN..text::Anchor::MAX) + } else { + diff.compare(&self.diff_to_buffer, buffer) + }; + if let Some(changed_range) = changed_range { + cx.emit(BufferDiffEvent::DiffChanged { changed_range }); + } + } + self.base_text = base_text; + self.diff_to_buffer = diff; + } + + pub fn diff_hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer_snapshot: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.diff_to_buffer + .hunks_intersecting_range(range, buffer_snapshot) + } + + pub fn diff_hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer_snapshot: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.diff_to_buffer + .hunks_intersecting_range_rev(range, buffer_snapshot) + } + + /// Used in cases where the change set isn't derived from git. + pub fn set_base_text( + &mut self, + base_buffer: Entity, + buffer: text::BufferSnapshot, + cx: &mut Context, + ) -> oneshot::Receiver<()> { + let (tx, rx) = oneshot::channel(); + let this = cx.weak_entity(); + let base_buffer = base_buffer.read(cx).snapshot(); + cx.spawn(|_, mut cx| async move { + let diff = cx + .background_executor() + .spawn({ + let base_buffer = base_buffer.clone(); + let buffer = buffer.clone(); + async move { BufferDiffSnapshot::build(Some(&base_buffer.text()), &buffer) } + }) + .await; + let Some(this) = this.upgrade() else { + tx.send(()).ok(); + return; + }; + this.update(&mut cx, |this, cx| { + this.set_state(Some(base_buffer), diff, &buffer, cx); + }) + .log_err(); + tx.send(()).ok(); + }) + .detach(); + rx + } + + #[cfg(any(test, feature = "test-support"))] + pub fn base_text_string(&self) -> Option { + self.base_text.as_ref().map(|buffer| buffer.text()) + } + + pub fn new(buffer: &Entity, cx: &mut App) -> Self { + BufferDiff { + buffer_id: buffer.read(cx).remote_id(), + base_text: None, + diff_to_buffer: BufferDiffSnapshot::new(&buffer.read(cx).text_snapshot()), + unstaged_change_set: None, + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn new_with_base_text( + base_text: &str, + buffer: &Entity, + cx: &mut App, + ) -> Self { + let mut base_text = base_text.to_owned(); + text::LineEnding::normalize(&mut base_text); + let diff_to_buffer = + BufferDiffSnapshot::build(Some(&base_text), &buffer.read(cx).text_snapshot()); + let base_text = language::Buffer::build_snapshot_sync(base_text.into(), None, None, cx); + BufferDiff { + buffer_id: buffer.read(cx).remote_id(), + base_text: Some(base_text), + diff_to_buffer, + unstaged_change_set: None, + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn recalculate_diff_sync( + &mut self, + snapshot: text::BufferSnapshot, + cx: &mut Context, + ) { + let mut base_text = self.base_text.as_ref().map(|buffer| buffer.text()); + if let Some(base_text) = base_text.as_mut() { + text::LineEnding::normalize(base_text); + } + let diff_to_buffer = BufferDiffSnapshot::build(base_text.as_deref(), &snapshot); + self.set_state(self.base_text.clone(), diff_to_buffer, &snapshot, cx); + } +} + +/// 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.row_range.clone(), + &diff_base[hunk.diff_base_byte_range], + buffer + .text_for_range( + Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_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, BufferId}; + use unindent::Unindent as _; + + #[test] + fn test_buffer_diff_simple() { + let diff_base = " + one + two + three + " + .unindent(); + let diff_base_rope = Rope::from(diff_base.clone()); + + let buffer_text = " + one + HELLO + three + " + .unindent(); + + let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let mut diff = BufferDiffSnapshot::new(&buffer); + diff.update(&diff_base_rope, &buffer); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(1..2, "two\n", "HELLO\n")], + ); + + buffer.edit([(0..0, "point five\n")]); + diff.update(&diff_base_rope, &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 diff_base_rope = Rope::from(diff_base.clone()); + + let buffer_text = " + A + one + B + two + C + three + HELLO + four + five + SIXTEEN + seven + eight + WORLD + nine + + ten + + " + .unindent(); + + let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let mut diff = BufferDiffSnapshot::new(&buffer); + diff.update(&diff_base_rope, &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"), + ], + ); + } + + #[test] + fn test_buffer_diff_compare() { + let base_text = " + zero + one + two + three + four + five + six + seven + eight + nine + " + .unindent(); + + let buffer_text_1 = " + one + three + four + five + SIX + seven + eight + NINE + " + .unindent(); + + let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1); + + let empty_diff = BufferDiffSnapshot::new(&buffer); + let diff_1 = BufferDiffSnapshot::build(Some(&base_text), &buffer); + let range = diff_1.compare(&empty_diff, &buffer).unwrap(); + assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0)); + + // Edit does not affect the diff. + buffer.edit_via_marked_text( + &" + one + three + four + five + «SIX.5» + seven + eight + NINE + " + .unindent(), + ); + let diff_2 = BufferDiffSnapshot::build(Some(&base_text), &buffer); + assert_eq!(None, diff_2.compare(&diff_1, &buffer)); + + // Edit turns a deletion hunk into a modification. + buffer.edit_via_marked_text( + &" + one + «THREE» + four + five + SIX.5 + seven + eight + NINE + " + .unindent(), + ); + let diff_3 = BufferDiffSnapshot::build(Some(&base_text), &buffer); + let range = diff_3.compare(&diff_2, &buffer).unwrap(); + assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0)); + + // Edit turns a modification hunk into a deletion. + buffer.edit_via_marked_text( + &" + one + THREE + four + five«» + seven + eight + NINE + " + .unindent(), + ); + let diff_4 = BufferDiffSnapshot::build(Some(&base_text), &buffer); + let range = diff_4.compare(&diff_3, &buffer).unwrap(); + assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0)); + + // Edit introduces a new insertion hunk. + buffer.edit_via_marked_text( + &" + one + THREE + four« + FOUR.5 + »five + seven + eight + NINE + " + .unindent(), + ); + let diff_5 = BufferDiffSnapshot::build(Some(&base_text), &buffer); + let range = diff_5.compare(&diff_4, &buffer).unwrap(); + assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0)); + + // Edit removes a hunk. + buffer.edit_via_marked_text( + &" + one + THREE + four + FOUR.5 + five + seven + eight + «nine» + " + .unindent(), + ); + let diff_6 = BufferDiffSnapshot::build(Some(&base_text), &buffer); + let range = diff_6.compare(&diff_5, &buffer).unwrap(); + assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0)); + } +} diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 756fb9da7f..d78dff8d2a 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -38,6 +38,7 @@ clock.workspace = true collections.workspace = true convert_case.workspace = true db.workspace = true +diff.workspace = true emojis.workspace = true file_icons.workspace = true futures.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b533345e66..5d545c6629 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -47,7 +47,6 @@ mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; -use ::git::diff::DiffHunkStatus; pub(crate) use actions::*; pub use actions::{OpenExcerpts, OpenExcerptsSplit}; use aho_corasick::AhoCorasick; @@ -74,6 +73,7 @@ use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, CompletionsMenu, ContextMenuOrigin, }; +use diff::DiffHunkStatus; use git::blame::GitBlame; use gpui::{ div, impl_actions, linear_color_stop, linear_gradient, point, prelude::*, pulsating_between, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 491510ed32..f14371a167 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7,6 +7,7 @@ use crate::{ }, JoinLines, }; +use diff::{BufferDiff, DiffHunkStatus}; use futures::StreamExt; use gpui::{ div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, @@ -26,7 +27,7 @@ use language_settings::{Formatter, FormatterList, IndentGuideSettings}; use multi_buffer::IndentGuide; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; -use project::{buffer_store::BufferChangeSet, FakeFs}; +use project::FakeFs; use project::{ lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT, project_settings::{LspSettings, ProjectSettings}, @@ -12440,8 +12441,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { (buffer_2.clone(), base_text_2), (buffer_3.clone(), base_text_3), ] { - let change_set = - cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx)); @@ -13135,7 +13135,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) (buffer_3.clone(), file_3_old), ] { let change_set = - cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx)); + cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx)); @@ -13251,7 +13251,7 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext }); editor .update(cx, |editor, _window, cx| { - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx)) @@ -14420,8 +14420,7 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContex editor.buffer().update(cx, |multibuffer, cx| { let buffer = multibuffer.as_singleton().unwrap(); - let change_set = - cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer.add_change_set(change_set, cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4412426c82..296794f04b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -26,8 +26,9 @@ use crate::{ }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap, HashSet}; +use diff::DiffHunkStatus; use file_icons::FileIcons; -use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; +use git::{blame::BlameEntry, Oid}; use gpui::{ anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 9a61656a58..7f9c4c7387 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -1,10 +1,11 @@ use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider}; use collections::HashSet; +use diff::BufferDiff; use futures::{channel::mpsc, future::join_all}; use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task}; use language::{Buffer, BufferEvent, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; -use project::{buffer_store::BufferChangeSet, Project}; +use project::Project; use smol::stream::StreamExt; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; use text::ToOffset; @@ -186,7 +187,7 @@ impl ProposedChangesEditor { } else { branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx)); new_change_sets.push(cx.new(|cx| { - let mut change_set = BufferChangeSet::new(&branch_buffer, cx); + let mut change_set = BufferDiff::new(&branch_buffer, cx); let _ = change_set.set_base_text( location.buffer.clone(), branch_buffer.read(cx).text_snapshot(), diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index c51dc0d6a6..7cfaf56224 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -3,8 +3,9 @@ use crate::{ RowExt, }; use collections::BTreeMap; +use diff::DiffHunkStatus; use futures::Future; -use git::diff::DiffHunkStatus; + use gpui::{ prelude::*, AnyWindowHandle, App, Context, Entity, Focusable as _, Keystroke, Pixels, Point, VisualTestContext, Window, WindowHandle, diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 8a2519b8c0..ad4dbdf990 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -16,6 +16,7 @@ path = "src/git_ui.rs" anyhow.workspace = true collections.workspace = true db.workspace = true +diff.workspace = true editor.workspace = true feature_flags.workspace = true futures.workspace = true diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 5d2689ed4c..7ce02f89a0 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -2,6 +2,7 @@ use std::any::{Any, TypeId}; use anyhow::Result; use collections::HashSet; +use diff::BufferDiff; use editor::{scroll::Autoscroll, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use futures::StreamExt; @@ -11,7 +12,7 @@ use gpui::{ }; use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point}; use multi_buffer::{MultiBuffer, PathKey}; -use project::{buffer_store::BufferChangeSet, git::GitState, Project, ProjectPath}; +use project::{git::GitState, Project, ProjectPath}; use theme::ActiveTheme; use ui::prelude::*; use util::ResultExt as _; @@ -43,7 +44,7 @@ pub(crate) struct ProjectDiff { struct DiffBuffer { path_key: PathKey, buffer: Entity, - change_set: Entity, + change_set: Entity, } const CONFLICT_NAMESPACE: &'static str = "0"; diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index b8b625378d..0f4ccbaf3c 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -14,9 +14,10 @@ doctest = false [features] test-support = [ - "text/test-support", - "language/test-support", + "diff/test-support", "gpui/test-support", + "language/test-support", + "text/test-support", "util/test-support", ] @@ -25,6 +26,7 @@ anyhow.workspace = true clock.workspace = true collections.workspace = true ctor.workspace = true +diff.workspace = true env_logger.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 1986944a1d..c73ffbd914 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -9,8 +9,8 @@ pub use position::{TypedOffset, TypedPoint, TypedRow}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; +use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkStatus}; use futures::{channel::mpsc, SinkExt}; -use git::diff::DiffHunkStatus; use gpui::{App, Context, Entity, EntityId, EventEmitter, Task}; use itertools::Itertools; use language::{ @@ -21,7 +21,7 @@ use language::{ TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, }; -use project::buffer_store::{BufferChangeSet, BufferChangeSetEvent}; + use rope::DimensionPair; use smallvec::SmallVec; use smol::future::yield_now; @@ -216,18 +216,18 @@ struct BufferState { } struct ChangeSetState { - change_set: Entity, + change_set: Entity, _subscription: gpui::Subscription, } impl ChangeSetState { - fn new(change_set: Entity, cx: &mut Context) -> Self { + fn new(change_set: Entity, cx: &mut Context) -> Self { ChangeSetState { _subscription: cx.subscribe(&change_set, |this, change_set, event, cx| match event { - BufferChangeSetEvent::DiffChanged { changed_range } => { + BufferDiffEvent::DiffChanged { changed_range } => { this.buffer_diff_changed(change_set, changed_range.clone(), cx) } - BufferChangeSetEvent::LanguageChanged => { + BufferDiffEvent::LanguageChanged => { this.buffer_diff_language_changed(change_set, cx) } }), @@ -270,7 +270,7 @@ pub enum DiffTransform { #[derive(Clone)] struct DiffSnapshot { - diff: git::diff::BufferDiff, + diff: diff::BufferDiffSnapshot, base_text: language::BufferSnapshot, } @@ -318,7 +318,7 @@ pub struct RowInfo { pub buffer_id: Option, pub buffer_row: Option, pub multibuffer_row: Option, - pub diff_status: Option, + pub diff_status: Option, } /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. @@ -2154,7 +2154,7 @@ impl MultiBuffer { fn buffer_diff_language_changed( &mut self, - change_set: Entity, + change_set: Entity, cx: &mut Context, ) { self.sync(cx); @@ -2178,7 +2178,7 @@ impl MultiBuffer { fn buffer_diff_changed( &mut self, - change_set: Entity, + change_set: Entity, range: Range, cx: &mut Context, ) { @@ -2210,7 +2210,7 @@ impl MultiBuffer { snapshot.diffs.insert( buffer_id, DiffSnapshot { - diff: git::diff::BufferDiff::new_with_single_insertion(&base_text), + diff: BufferDiffSnapshot::new_with_single_insertion(&base_text), base_text, }, ); @@ -2352,14 +2352,14 @@ impl MultiBuffer { self.as_singleton().unwrap().read(cx).is_parsing() } - pub fn add_change_set(&mut self, change_set: Entity, cx: &mut Context) { + pub fn add_change_set(&mut self, change_set: Entity, cx: &mut Context) { let buffer_id = change_set.read(cx).buffer_id; self.buffer_diff_changed(change_set.clone(), text::Anchor::MIN..text::Anchor::MAX, cx); self.diff_bases .insert(buffer_id, ChangeSetState::new(change_set, cx)); } - pub fn change_set_for(&self, buffer_id: BufferId) -> Option> { + pub fn change_set_for(&self, buffer_id: BufferId) -> Option> { self.diff_bases .get(&buffer_id) .map(|state| state.change_set.clone()) diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 25c3a4cf91..cb9509bf34 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -1,5 +1,5 @@ use super::*; -use git::diff::DiffHunkStatus; +use diff::DiffHunkStatus; use gpui::{App, TestAppContext}; use indoc::indoc; use language::{Buffer, Rope}; @@ -361,7 +361,7 @@ fn test_diff_boundary_anchors(cx: &mut TestAppContext) { let base_text = "one\ntwo\nthree\n"; let text = "one\nthree\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); multibuffer.update(cx, |multibuffer, cx| { multibuffer.add_change_set(change_set, cx) @@ -405,7 +405,7 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) { let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n"; let text = "one\nfour\nseven\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { (multibuffer.snapshot(cx), multibuffer.subscribe()) @@ -498,7 +498,7 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) { let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n"; let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(&base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { @@ -979,7 +979,7 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("", cx)); let base_text = "a\nb\nc"; - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_all_diff_hunks_expanded(cx); multibuffer.add_change_set(change_set.clone(), cx); @@ -1273,7 +1273,7 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) { ); let buffer = cx.new(|cx| Buffer::local(text, cx)); - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -1516,7 +1516,7 @@ fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) { ); let buffer = cx.new(|cx| Buffer::local(text, cx)); - let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx)); + let change_set = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -1918,8 +1918,8 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let change_set_1 = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_1, &buffer_1, cx)); - let change_set_2 = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_2, &buffer_2, cx)); + let change_set_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); + let change_set_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -2101,7 +2101,7 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { #[derive(Default)] struct ReferenceMultibuffer { excerpts: Vec, - change_sets: HashMap>, + change_sets: HashMap>, } #[derive(Debug)] @@ -2396,7 +2396,7 @@ impl ReferenceMultibuffer { } } - fn add_change_set(&mut self, change_set: Entity, cx: &mut App) { + fn add_change_set(&mut self, change_set: Entity, cx: &mut App) { let buffer_id = change_set.read(cx).buffer_id; self.change_sets.insert(buffer_id, change_set); } @@ -2551,7 +2551,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx)); let change_set = - cx.new(|cx| BufferChangeSet::new_with_base_text(&base_text, &buffer, cx)); + cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx)); multibuffer.update(cx, |multibuffer, cx| { reference.add_change_set(change_set.clone(), cx); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 5149a818cf..1d4eef6a56 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -30,6 +30,7 @@ async-trait.workspace = true client.workspace = true clock.workspace = true collections.workspace = true +diff.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index c91d905f4c..5c251f441f 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -8,13 +8,14 @@ use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegist use anyhow::{anyhow, bail, Context as _, Result}; use client::Client; use collections::{hash_map, HashMap, HashSet}; +use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot}; use fs::Fs; use futures::{ channel::oneshot, future::{OptionFuture, Shared}, Future, FutureExt as _, StreamExt, }; -use git::{blame::Blame, diff::BufferDiff, repository::RepoPath}; +use git::{blame::Blame, repository::RepoPath}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -56,7 +57,7 @@ pub struct BufferStore { #[allow(clippy::type_complexity)] loading_change_sets: HashMap< (BufferId, ChangeSetKind), - Shared, Arc>>>, + Shared, Arc>>>, >, worktree_store: Entity, opened_buffers: HashMap, @@ -67,14 +68,14 @@ pub struct BufferStore { #[derive(Hash, Eq, PartialEq, Clone)] struct SharedBuffer { buffer: Entity, - change_set: Option>, + change_set: Option>, lsp_handle: Option, } #[derive(Default)] struct BufferChangeSetState { - unstaged_changes: Option>, - uncommitted_changes: Option>, + unstaged_changes: Option>, + uncommitted_changes: Option>, recalculate_diff_task: Option>>, language: Option>, language_registry: Option>, @@ -106,11 +107,11 @@ impl BufferChangeSetState { let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx); } - fn unstaged_changes(&self) -> Option> { + fn unstaged_changes(&self) -> Option> { self.unstaged_changes.as_ref().and_then(|set| set.upgrade()) } - fn uncommitted_changes(&self) -> Option> { + fn uncommitted_changes(&self) -> Option> { self.uncommitted_changes .as_ref() .and_then(|set| set.upgrade()) @@ -233,20 +234,22 @@ impl BufferChangeSetState { ) }; - let diff = - cx.background_executor().spawn({ - let buffer = buffer.clone(); - async move { - BufferDiff::build(index.as_ref().map(|index| index.as_str()), &buffer) - } - }); + let diff = cx.background_executor().spawn({ + let buffer = buffer.clone(); + async move { + BufferDiffSnapshot::build( + index.as_ref().map(|index| index.as_str()), + &buffer, + ) + } + }); let (staged_snapshot, diff) = futures::join!(staged_snapshot, diff); unstaged_changes.update(&mut cx, |unstaged_changes, cx| { unstaged_changes.set_state(staged_snapshot.clone(), diff, &buffer, cx); if language_changed { - cx.emit(BufferChangeSetEvent::LanguageChanged); + cx.emit(BufferDiffEvent::LanguageChanged); } })?; } @@ -286,7 +289,10 @@ impl BufferChangeSetState { let buffer = buffer.clone(); let head = head.clone(); async move { - BufferDiff::build(head.as_ref().map(|head| head.as_str()), &buffer) + BufferDiffSnapshot::build( + head.as_ref().map(|head| head.as_str()), + &buffer, + ) } }); futures::join!(committed_snapshot, diff) @@ -295,7 +301,7 @@ impl BufferChangeSetState { uncommitted_changes.update(&mut cx, |change_set, cx| { change_set.set_state(snapshot, diff, &buffer, cx); if language_changed { - cx.emit(BufferChangeSetEvent::LanguageChanged); + cx.emit(BufferDiffEvent::LanguageChanged); } })?; } @@ -1383,7 +1389,7 @@ impl BufferStore { &mut self, buffer: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let buffer_id = buffer.read(cx).remote_id(); if let Some(change_set) = self.get_unstaged_changes(buffer_id, cx) { return Task::ready(Ok(change_set)); @@ -1427,7 +1433,7 @@ impl BufferStore { &mut self, buffer: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let buffer_id = buffer.read(cx).remote_id(); if let Some(change_set) = self.get_uncommitted_changes(buffer_id, cx) { return Task::ready(Ok(change_set)); @@ -1484,11 +1490,7 @@ impl BufferStore { } #[cfg(any(test, feature = "test-support"))] - pub fn set_unstaged_change_set( - &mut self, - buffer_id: BufferId, - change_set: Entity, - ) { + pub fn set_unstaged_change_set(&mut self, buffer_id: BufferId, change_set: Entity) { self.loading_change_sets.insert( (buffer_id, ChangeSetKind::Unstaged), Task::ready(Ok(change_set)).shared(), @@ -1501,7 +1503,7 @@ impl BufferStore { texts: Result, buffer: Entity, mut cx: AsyncApp, - ) -> Result> { + ) -> Result> { let diff_bases_change = match texts { Err(e) => { this.update(&mut cx, |this, cx| { @@ -1532,10 +1534,10 @@ impl BufferStore { }) }); - let change_set = cx.new(|cx| BufferChangeSet { + let change_set = cx.new(|cx| BufferDiff { buffer_id, base_text: None, - diff_to_buffer: BufferDiff::new(&buffer.read(cx).text_snapshot()), + diff_to_buffer: BufferDiffSnapshot::new(&buffer.read(cx).text_snapshot()), unstaged_change_set: None, }); match kind { @@ -1547,10 +1549,10 @@ impl BufferStore { if let Some(change_set) = change_set_state.unstaged_changes() { change_set } else { - let unstaged_change_set = cx.new(|cx| BufferChangeSet { + let unstaged_change_set = cx.new(|cx| BufferDiff { buffer_id, base_text: None, - diff_to_buffer: BufferDiff::new( + diff_to_buffer: BufferDiffSnapshot::new( &buffer.read(cx).text_snapshot(), ), unstaged_change_set: None, @@ -1870,7 +1872,7 @@ impl BufferStore { &self, buffer_id: BufferId, cx: &App, - ) -> Option> { + ) -> Option> { if let OpenBuffer::Complete { change_set_state, .. } = self.opened_buffers.get(&buffer_id)? @@ -1889,7 +1891,7 @@ impl BufferStore { &self, buffer_id: BufferId, cx: &App, - ) -> Option> { + ) -> Option> { if let OpenBuffer::Complete { change_set_state, .. } = self.opened_buffers.get(&buffer_id)? diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2a7759daa4..91cd7495b5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -21,6 +21,7 @@ mod project_tests; mod direnv; mod environment; +use diff::BufferDiff; pub use environment::EnvironmentErrorMessage; use git::Repository; pub mod search_history; @@ -28,7 +29,7 @@ mod yarn; use crate::git::GitState; use anyhow::{anyhow, Context as _, Result}; -use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent}; +use buffer_store::{BufferStore, BufferStoreEvent}; use client::{ proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, }; @@ -1959,7 +1960,7 @@ impl Project { &mut self, buffer: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { if self.is_disconnected(cx) { return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); } @@ -1973,7 +1974,7 @@ impl Project { &mut self, buffer: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { if self.is_disconnected(cx) { return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index dfd5a5dc56..affd10d6e9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,5 +1,5 @@ use crate::{Event, *}; -use ::git::diff::assert_hunks; +use diff::assert_hunks; use fs::FakeFs; use futures::{future, StreamExt}; use gpui::{App, SemanticVersion, UpdateGlobal};