From 8f75fe25e5150ef1156436a19b1993acc27a4994 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 10 Feb 2025 21:43:25 -0500 Subject: [PATCH] Add staged status information to diff hunks (#24475) Release Notes: - Render unstaged hunks in the project diff editor with a slashed background --------- Co-authored-by: maxbrunsfeld Co-authored-by: Max Brunsfeld --- Cargo.lock | 48 +- Cargo.toml | 4 +- crates/{diff => buffer_diff}/Cargo.toml | 7 +- crates/{diff => buffer_diff}/LICENSE-GPL | 0 .../src/buffer_diff.rs} | 964 ++++++++++++------ crates/collab/Cargo.toml | 2 +- crates/collab/src/tests/integration_tests.rs | 71 +- crates/editor/Cargo.toml | 2 +- crates/editor/src/editor.rs | 30 +- crates/editor/src/editor_tests.rs | 44 +- crates/editor/src/element.rs | 128 ++- crates/editor/src/proposed_changes_editor.rs | 4 +- crates/editor/src/test/editor_test_context.rs | 8 +- crates/git_ui/Cargo.toml | 2 +- crates/git_ui/src/project_diff.rs | 7 +- crates/gpui/examples/pattern.rs | 36 +- crates/gpui/src/color.rs | 9 +- crates/gpui/src/platform/blade/shaders.wgsl | 26 +- crates/gpui/src/platform/mac/shaders.metal | 20 +- crates/multi_buffer/Cargo.toml | 6 +- crates/multi_buffer/src/anchor.rs | 6 +- crates/multi_buffer/src/multi_buffer.rs | 217 ++-- crates/multi_buffer/src/multi_buffer_tests.rs | 49 +- crates/project/Cargo.toml | 4 +- crates/project/src/buffer_store.rs | 131 ++- crates/project/src/project.rs | 2 +- crates/project/src/project_tests.rs | 49 +- .../remote_server/src/remote_editing_tests.rs | 9 +- 28 files changed, 1132 insertions(+), 753 deletions(-) rename crates/{diff => buffer_diff}/Cargo.toml (80%) rename crates/{diff => buffer_diff}/LICENSE-GPL (100%) rename crates/{diff/src/diff.rs => buffer_diff/src/buffer_diff.rs} (50%) diff --git a/Cargo.lock b/Cargo.lock index 63c66cf045..c61131c347 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2024,6 +2024,24 @@ dependencies = [ "serde", ] +[[package]] +name = "buffer_diff" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures 0.3.31", + "git2", + "gpui", + "language", + "pretty_assertions", + "rope", + "serde_json", + "sum_tree", + "text", + "unindent", + "util", +] + [[package]] name = "built" version = "0.7.5" @@ -2742,6 +2760,7 @@ dependencies = [ "axum", "axum-extra", "base64 0.22.1", + "buffer_diff", "call", "channel", "chrono", @@ -2753,7 +2772,6 @@ dependencies = [ "ctor", "dashmap 6.1.0", "derive_more", - "diff 0.1.0", "editor", "env_logger 0.11.6", "envy", @@ -3860,24 +3878,6 @@ dependencies = [ "zeroize", ] -[[package]] -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]] name = "diff" version = "0.1.13" @@ -4041,6 +4041,7 @@ dependencies = [ "aho-corasick", "anyhow", "assets", + "buffer_diff", "chrono", "client", "clock", @@ -4048,7 +4049,6 @@ dependencies = [ "convert_case 0.7.1", "ctor", "db", - "diff 0.1.0", "emojis", "env_logger 0.11.6", "file_icons", @@ -5347,9 +5347,9 @@ name = "git_ui" version = "0.1.0" dependencies = [ "anyhow", + "buffer_diff", "collections", "db", - "diff 0.1.0", "editor", "feature_flags", "futures 0.3.31", @@ -7980,10 +7980,10 @@ name = "multi_buffer" version = "0.1.0" dependencies = [ "anyhow", + "buffer_diff", "clock", "collections", "ctor", - "diff 0.1.0", "env_logger 0.11.6", "futures 0.3.31", "gpui", @@ -9995,7 +9995,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ - "diff 0.1.13", + "diff", "yansi", ] @@ -10088,10 +10088,10 @@ dependencies = [ "aho-corasick", "anyhow", "async-trait", + "buffer_diff", "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 935b053378..ec893364e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ members = [ "crates/db", "crates/deepseek", "crates/diagnostics", - "crates/diff", + "crates/buffer_diff", "crates/docs_preprocessor", "crates/editor", "crates/evals", @@ -235,7 +235,7 @@ copilot = { path = "crates/copilot" } db = { path = "crates/db" } deepseek = { path = "crates/deepseek" } diagnostics = { path = "crates/diagnostics" } -diff = { path = "crates/diff" } +buffer_diff = { path = "crates/buffer_diff" } editor = { path = "crates/editor" } extension = { path = "crates/extension" } extension_host = { path = "crates/extension_host" } diff --git a/crates/diff/Cargo.toml b/crates/buffer_diff/Cargo.toml similarity index 80% rename from crates/diff/Cargo.toml rename to crates/buffer_diff/Cargo.toml index 7a4186f6e5..d4cac616d0 100644 --- a/crates/diff/Cargo.toml +++ b/crates/buffer_diff/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "diff" +name = "buffer_diff" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,17 +9,17 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/diff.rs" +path = "src/buffer_diff.rs" [features] test-support = [] [dependencies] +anyhow.workspace = true 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 @@ -29,4 +29,5 @@ util.workspace = true pretty_assertions.workspace = true serde_json.workspace = true text = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } unindent.workspace = true diff --git a/crates/diff/LICENSE-GPL b/crates/buffer_diff/LICENSE-GPL similarity index 100% rename from crates/diff/LICENSE-GPL rename to crates/buffer_diff/LICENSE-GPL diff --git a/crates/diff/src/diff.rs b/crates/buffer_diff/src/buffer_diff.rs similarity index 50% rename from crates/diff/src/diff.rs rename to crates/buffer_diff/src/buffer_diff.rs index adb25417a7..772835f9a9 100644 --- a/crates/diff/src/diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1,20 +1,60 @@ use futures::{channel::oneshot, future::OptionFuture}; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; -use gpui::{App, Context, Entity, EventEmitter}; +use gpui::{App, AsyncApp, Context, Entity, EventEmitter}; use language::{Language, LanguageRegistry}; use rope::Rope; use std::{cmp, future::Future, iter, ops::Range, sync::Arc}; use sum_tree::SumTree; -use text::{Anchor, BufferId, OffsetRangeExt, Point}; +use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; use util::ResultExt; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum DiffHunkStatus { - Added, - Modified, - Removed, +pub struct BufferDiff { + pub buffer_id: BufferId, + inner: BufferDiffInner, + secondary_diff: Option>, } +#[derive(Clone)] +pub struct BufferDiffSnapshot { + inner: BufferDiffInner, + secondary_diff: Option>, +} + +#[derive(Clone)] +struct BufferDiffInner { + hunks: SumTree, + base_text: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiffHunkStatus { + Added(DiffHunkSecondaryStatus), + Modified(DiffHunkSecondaryStatus), + Removed(DiffHunkSecondaryStatus), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiffHunkSecondaryStatus { + HasSecondaryHunk, + OverlapsWithSecondaryHunk, + None, +} + +// to stage a hunk: +// - assume hunk starts out as not staged +// - hunk exists with the same buffer range in the unstaged diff and the uncommitted diff +// - we want to construct a "version" of the file that +// - starts from the index base text +// - has the single hunk applied to it +// - the hunk is the one from the UNSTAGED diff, so that the diff base offset range is correct to apply to that diff base +// - write that new version of the file into the index + +// to unstage a hunk +// - no hunk in the unstaged diff intersects this hunk from the uncommitted diff +// - we want to compute the hunk that +// - we can apply to the index text +// - at the end of applying it, + /// A diff hunk resolved to rows in the buffer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiffHunk { @@ -24,6 +64,7 @@ pub struct DiffHunk { pub buffer_range: Range, /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range, + pub secondary_status: DiffHunkSecondaryStatus, } /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. @@ -64,13 +105,17 @@ impl sum_tree::Summary for DiffHunkSummary { } } -#[derive(Clone)] -pub struct BufferDiffSnapshot { - hunks: SumTree, - pub base_text: Option, +impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor { + fn cmp( + &self, + cursor_location: &DiffHunkSummary, + buffer: &text::BufferSnapshot, + ) -> cmp::Ordering { + self.cmp(&cursor_location.buffer_range.end, buffer) + } } -impl std::fmt::Debug for BufferDiffSnapshot { +impl std::fmt::Debug for BufferDiffInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("hunks", &self.hunks) @@ -79,142 +124,56 @@ impl std::fmt::Debug for BufferDiffSnapshot { } impl BufferDiffSnapshot { - pub fn new(buffer: &text::BufferSnapshot) -> BufferDiffSnapshot { - BufferDiffSnapshot { - hunks: SumTree::new(buffer), - base_text: None, - } - } - - pub fn new_with_single_insertion(cx: &mut App) -> Self { - let base_text = language::Buffer::build_empty_snapshot(cx); - Self { - hunks: SumTree::from_item( - InternalDiffHunk { - buffer_range: Anchor::MIN..Anchor::MAX, - diff_base_byte_range: 0..0, - }, - &base_text, - ), - base_text: Some(base_text), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn build_sync( - buffer: text::BufferSnapshot, - diff_base: String, - cx: &mut gpui::TestAppContext, - ) -> Self { - let snapshot = - cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx)); - cx.executor().block(snapshot) - } - - pub fn build( - buffer: text::BufferSnapshot, - diff_base: Option>, - language: Option>, - language_registry: Option>, - cx: &mut App, - ) -> impl Future { - let base_text_snapshot = diff_base.as_ref().map(|base_text| { - language::Buffer::build_snapshot( - Rope::from(base_text.as_str()), - language.clone(), - language_registry.clone(), - cx, - ) - }); - let base_text_snapshot = cx - .background_executor() - .spawn(OptionFuture::from(base_text_snapshot)); - - let hunks = cx.background_executor().spawn({ - let buffer = buffer.clone(); - async move { Self::recalculate_hunks(diff_base, buffer) } - }); - - async move { - let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); - Self { base_text, hunks } - } - } - - pub fn build_with_base_buffer( - buffer: text::BufferSnapshot, - diff_base: Option>, - diff_base_buffer: Option, - cx: &App, - ) -> impl Future { - cx.background_executor().spawn({ - let buffer = buffer.clone(); - async move { - let hunks = Self::recalculate_hunks(diff_base, buffer); - Self { - hunks, - base_text: diff_base_buffer, - } - } - }) - } - - fn recalculate_hunks( - diff_base: Option>, - buffer: text::BufferSnapshot, - ) -> SumTree { - 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 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); - } - } - } - - tree - } - pub fn is_empty(&self) -> bool { - self.hunks.is_empty() + self.inner.hunks.is_empty() } - pub fn hunks_in_row_range<'a>( - &'a self, - range: Range, - buffer: &'a text::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 secondary_diff(&self) -> Option<&BufferDiffSnapshot> { + self.secondary_diff.as_deref() } pub fn hunks_intersecting_range<'a>( &'a self, range: Range, buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); + self.inner + .hunks_intersecting_range(range, buffer, unstaged_counterpart) + } + + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.inner.hunks_intersecting_range_rev(range, buffer) + } + + pub fn base_text(&self) -> Option<&language::BufferSnapshot> { + self.inner.base_text.as_ref() + } + + pub fn base_texts_eq(&self, other: &Self) -> bool { + match (other.base_text(), self.base_text()) { + (None, None) => true, + (None, Some(_)) => false, + (Some(_), None) => false, + (Some(old), Some(new)) => { + let (old_id, old_empty) = (old.remote_id(), old.is_empty()); + let (new_id, new_empty) = (new.remote_id(), new.is_empty()); + new_id == old_id || (new_empty && old_empty) + } + } + } +} + +impl BufferDiffInner { + fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + secondary: Option<&'a Self>, ) -> impl 'a + Iterator { let range = range.to_offset(buffer); @@ -244,6 +203,12 @@ impl BufferDiffSnapshot { ] }); + let mut secondary_cursor = secondary.as_ref().map(|diff| { + let mut cursor = diff.hunks.cursor::(buffer); + cursor.next(buffer); + cursor + }); + 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()?; @@ -259,15 +224,35 @@ impl BufferDiffSnapshot { end_anchor = buffer.anchor_before(end_point); } + let mut secondary_status = DiffHunkSecondaryStatus::None; + if let Some(secondary_cursor) = secondary_cursor.as_mut() { + if start_anchor + .cmp(&secondary_cursor.start().buffer_range.start, buffer) + .is_gt() + { + secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer); + } + + if let Some(secondary_hunk) = secondary_cursor.item() { + let secondary_range = secondary_hunk.buffer_range.to_point(buffer); + if secondary_range == (start_point..end_point) { + secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; + } else if secondary_range.start <= end_point { + secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk; + } + } + } + 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, + secondary_status, }); }) } - pub fn hunks_intersecting_range_rev<'a>( + fn hunks_intersecting_range_rev<'a>( &'a self, range: Range, buffer: &'a text::BufferSnapshot, @@ -295,15 +280,13 @@ impl BufferDiffSnapshot { row_range: range.start.row..end_row, diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), + // The secondary status is not used by callers of this method. + secondary_status: DiffHunkSecondaryStatus::None, }) }) } - pub fn compare( - &self, - old: &Self, - new_snapshot: &text::BufferSnapshot, - ) -> Option> { + fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option> { let mut new_cursor = self.hunks.cursor::<()>(new_snapshot); let mut old_cursor = old.hunks.cursor::<()>(new_snapshot); old_cursor.next(new_snapshot); @@ -365,174 +348,370 @@ impl BufferDiffSnapshot { start.zip(end).map(|(start, end)| start..end) } - - #[cfg(test)] - fn clear(&mut self, buffer: &text::BufferSnapshot) { - self.hunks = SumTree::new(buffer); - } - - #[cfg(test)] - fn hunks<'a>(&'a self, text: &'a text::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 snapshot: BufferDiffSnapshot, - pub unstaged_diff: Option>, +fn compute_hunks( + diff_base: Option>, + buffer: text::BufferSnapshot, +) -> SumTree { + let mut tree = SumTree::new(&buffer); + + if let Some(diff_base) = diff_base { + let buffer_text = buffer.as_rope().to_string(); + + let mut options = GitOptions::default(); + options.context_lines(0); + let patch = GitPatch::from_buffers( + diff_base.as_bytes(), + None, + buffer_text.as_bytes(), + None, + Some(&mut options), + ) + .log_err(); + + // 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 tree; + } + + if let Some(patch) = patch { + let mut divergence = 0; + for hunk_index in 0..patch.num_hunks() { + let hunk = process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence); + tree.push(hunk, &buffer); + } + } + } + + tree +} + +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, + } } 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("snapshot", &self.snapshot) + .field("snapshot", &self.inner) .finish() } } pub enum BufferDiffEvent { - DiffChanged { changed_range: Range }, + DiffChanged { + changed_range: Option>, + }, LanguageChanged, } impl EventEmitter for BufferDiff {} impl BufferDiff { - pub fn set_state( - &mut self, - snapshot: BufferDiffSnapshot, - buffer: &text::BufferSnapshot, - cx: &mut Context, - ) { - if let Some(base_text) = snapshot.base_text.as_ref() { - let changed_range = if Some(base_text.remote_id()) - != self - .snapshot - .base_text - .as_ref() - .map(|buffer| buffer.remote_id()) - { - Some(text::Anchor::MIN..text::Anchor::MAX) - } else { - snapshot.compare(&self.snapshot, buffer) - }; - if let Some(changed_range) = changed_range { - cx.emit(BufferDiffEvent::DiffChanged { changed_range }); - } + #[cfg(test)] + fn build_sync( + buffer: text::BufferSnapshot, + diff_base: String, + cx: &mut gpui::TestAppContext, + ) -> BufferDiffInner { + let snapshot = + cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx)); + cx.executor().block(snapshot) + } + + fn build( + buffer: text::BufferSnapshot, + diff_base: Option>, + language: Option>, + language_registry: Option>, + cx: &mut App, + ) -> impl Future { + let base_text_snapshot = diff_base.as_ref().map(|base_text| { + language::Buffer::build_snapshot( + Rope::from(base_text.as_str()), + language.clone(), + language_registry.clone(), + cx, + ) + }); + let base_text_snapshot = cx + .background_executor() + .spawn(OptionFuture::from(base_text_snapshot)); + + let hunks = cx.background_executor().spawn({ + let buffer = buffer.clone(); + async move { compute_hunks(diff_base, buffer) } + }); + + async move { + let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); + BufferDiffInner { base_text, hunks } } - self.snapshot = snapshot; } - pub fn diff_hunks_intersecting_range<'a>( + fn build_with_base_buffer( + buffer: text::BufferSnapshot, + diff_base: Option>, + diff_base_buffer: Option, + cx: &App, + ) -> impl Future { + cx.background_executor().spawn(async move { + BufferDiffInner { + hunks: compute_hunks(diff_base, buffer), + base_text: diff_base_buffer, + } + }) + } + + fn build_empty(buffer: &text::BufferSnapshot) -> BufferDiffInner { + BufferDiffInner { + hunks: SumTree::new(buffer), + base_text: None, + } + } + + pub fn build_with_single_insertion( + insertion_present_in_secondary_diff: bool, + cx: &mut App, + ) -> BufferDiffSnapshot { + let base_text = language::Buffer::build_empty_snapshot(cx); + let hunks = SumTree::from_item( + InternalDiffHunk { + buffer_range: Anchor::MIN..Anchor::MAX, + diff_base_byte_range: 0..0, + }, + &base_text, + ); + BufferDiffSnapshot { + inner: BufferDiffInner { + hunks: hunks.clone(), + base_text: Some(base_text.clone()), + }, + secondary_diff: if insertion_present_in_secondary_diff { + Some(Box::new(BufferDiffSnapshot { + inner: BufferDiffInner { + hunks, + base_text: Some(base_text), + }, + secondary_diff: None, + })) + } else { + None + }, + } + } + + pub fn set_secondary_diff(&mut self, diff: Entity) { + self.secondary_diff = Some(diff); + } + + pub fn secondary_diff(&self) -> Option> { + Some(self.secondary_diff.as_ref()?.clone()) + } + + pub fn range_to_hunk_range( + &self, + range: Range, + buffer: &text::BufferSnapshot, + cx: &App, + ) -> Option> { + let start = self + .hunks_intersecting_range(range.clone(), &buffer, cx) + .next()? + .buffer_range + .start; + let end = self + .hunks_intersecting_range_rev(range.clone(), &buffer) + .next()? + .buffer_range + .end; + Some(start..end) + } + + #[allow(clippy::too_many_arguments)] + pub async fn update_diff( + this: Entity, + buffer: text::BufferSnapshot, + base_text: Option>, + base_text_changed: bool, + language_changed: bool, + language: Option>, + language_registry: Option>, + cx: &mut AsyncApp, + ) -> anyhow::Result>> { + let snapshot = if base_text_changed || language_changed { + cx.update(|cx| { + Self::build( + buffer.clone(), + base_text, + language.clone(), + language_registry.clone(), + cx, + ) + })? + .await + } else { + this.read_with(cx, |this, cx| { + Self::build_with_base_buffer( + buffer.clone(), + base_text, + this.base_text().cloned(), + cx, + ) + })? + .await + }; + + this.update(cx, |this, _| this.set_state(snapshot, &buffer)) + } + + pub fn update_diff_from( + &mut self, + buffer: &text::BufferSnapshot, + other: &Entity, + cx: &mut Context, + ) -> Option> { + let other = other.read(cx).inner.clone(); + self.set_state(other, buffer) + } + + fn set_state( + &mut self, + inner: BufferDiffInner, + buffer: &text::BufferSnapshot, + ) -> Option> { + let changed_range = match (self.inner.base_text.as_ref(), inner.base_text.as_ref()) { + (None, None) => None, + (Some(old), Some(new)) if old.remote_id() == new.remote_id() => { + inner.compare(&self.inner, buffer) + } + _ => Some(text::Anchor::MIN..text::Anchor::MAX), + }; + self.inner = inner; + changed_range + } + + pub fn base_text(&self) -> Option<&language::BufferSnapshot> { + self.inner.base_text.as_ref() + } + + pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot { + BufferDiffSnapshot { + inner: self.inner.clone(), + secondary_diff: self + .secondary_diff + .as_ref() + .map(|diff| Box::new(diff.read(cx).snapshot(cx))), + } + } + + pub fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer_snapshot: &'a text::BufferSnapshot, + cx: &'a App, + ) -> impl 'a + Iterator { + let unstaged_counterpart = self + .secondary_diff + .as_ref() + .map(|diff| &diff.read(cx).inner); + self.inner + .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart) + } + + pub fn hunks_intersecting_range_rev<'a>( &'a self, range: Range, buffer_snapshot: &'a text::BufferSnapshot, ) -> impl 'a + Iterator { - self.snapshot - .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.snapshot + self.inner .hunks_intersecting_range_rev(range, buffer_snapshot) } + pub fn hunks_in_row_range<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + cx: &'a App, + ) -> 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, cx) + } + /// Used in cases where the change set isn't derived from git. pub fn set_base_text( &mut self, @@ -547,7 +726,7 @@ impl BufferDiff { let base_buffer = base_buffer.snapshot(); let base_text = Arc::new(base_buffer.text()); - let snapshot = BufferDiffSnapshot::build( + let snapshot = BufferDiff::build( buffer.clone(), Some(base_text), base_buffer.language().cloned(), @@ -562,8 +741,8 @@ impl BufferDiff { let Some(this) = this.upgrade() else { return; }; - this.update(&mut cx, |this, cx| { - this.set_state(snapshot, &buffer, cx); + this.update(&mut cx, |this, _| { + this.set_state(snapshot, &buffer); }) .log_err(); drop(complete_on_drop) @@ -574,14 +753,14 @@ impl BufferDiff { #[cfg(any(test, feature = "test-support"))] pub fn base_text_string(&self) -> Option { - self.snapshot.base_text.as_ref().map(|buffer| buffer.text()) + self.inner.base_text.as_ref().map(|buffer| buffer.text()) } - pub fn new(buffer: &Entity, cx: &mut App) -> Self { + pub fn new(buffer: &text::BufferSnapshot) -> Self { BufferDiff { - buffer_id: buffer.read(cx).remote_id(), - snapshot: BufferDiffSnapshot::new(&buffer.read(cx)), - unstaged_diff: None, + buffer_id: buffer.remote_id(), + inner: BufferDiff::build_empty(buffer), + secondary_diff: None, } } @@ -593,7 +772,7 @@ impl BufferDiff { ) -> Self { let mut base_text = base_text.to_owned(); text::LineEnding::normalize(&mut base_text); - let snapshot = BufferDiffSnapshot::build( + let snapshot = BufferDiff::build( buffer.read(cx).text_snapshot(), Some(base_text.into()), None, @@ -603,26 +782,60 @@ impl BufferDiff { let snapshot = cx.background_executor().block(snapshot); BufferDiff { buffer_id: buffer.read(cx).remote_id(), - snapshot, - unstaged_diff: None, + inner: snapshot, + secondary_diff: None, } } #[cfg(any(test, feature = "test-support"))] pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context) { let base_text = self - .snapshot + .inner .base_text .as_ref() .map(|base_text| base_text.text()); - let snapshot = BufferDiffSnapshot::build_with_base_buffer( + let snapshot = BufferDiff::build_with_base_buffer( buffer.clone(), base_text.clone().map(Arc::new), - self.snapshot.base_text.clone(), + self.inner.base_text.clone(), cx, ); let snapshot = cx.background_executor().block(snapshot); - self.set_state(snapshot, &buffer, cx); + let changed_range = self.set_state(snapshot, &buffer); + cx.emit(BufferDiffEvent::DiffChanged { changed_range }); + } +} + +impl DiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.buffer_range.start == self.buffer_range.end { + DiffHunkStatus::Removed(self.secondary_status) + } else if self.diff_base_byte_range.is_empty() { + DiffHunkStatus::Added(self.secondary_status) + } else { + DiffHunkStatus::Modified(self.secondary_status) + } + } +} + +impl DiffHunkStatus { + pub fn is_removed(&self) -> bool { + matches!(self, DiffHunkStatus::Removed(_)) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn removed() -> Self { + DiffHunkStatus::Removed(DiffHunkSecondaryStatus::None) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn added() -> Self { + DiffHunkStatus::Added(DiffHunkSecondaryStatus::None) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn modified() -> Self { + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None) } } @@ -633,7 +846,7 @@ pub fn assert_hunks( diff_hunks: Iter, buffer: &text::BufferSnapshot, diff_base: &str, - expected_hunks: &[(Range, &str, &str)], + expected_hunks: &[(Range, &str, &str, DiffHunkStatus)], ) where Iter: Iterator, { @@ -641,19 +854,20 @@ pub fn assert_hunks( .map(|hunk| { ( hunk.row_range.clone(), - &diff_base[hunk.diff_base_byte_range], + &diff_base[hunk.diff_base_byte_range.clone()], buffer .text_for_range( Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0), ) .collect::(), + hunk.status(), ) }) .collect::>(); let expected_hunks: Vec<_> = expected_hunks .iter() - .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status)) .collect(); assert_eq!(actual_hunks, expected_hunks); @@ -685,25 +899,115 @@ mod tests { .unindent(); let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); - let mut diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx); + let mut diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx); assert_hunks( - diff.hunks(&buffer), + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), &buffer, &diff_base, - &[(1..2, "two\n", "HELLO\n")], + &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified())], ); buffer.edit([(0..0, "point five\n")]); - diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx); + diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx); assert_hunks( - diff.hunks(&buffer), + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), &buffer, &diff_base, - &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], + &[ + (0..1, "", "point five\n", DiffHunkStatus::added()), + (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified()), + ], ); - diff.clear(&buffer); - assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]); + diff = BufferDiff::build_empty(&buffer); + assert_hunks( + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), + &buffer, + &diff_base, + &[], + ); + } + + #[gpui::test] + async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) { + let head_text = " + zero + one + two + three + four + five + six + seven + eight + nine + " + .unindent(); + + let index_text = " + zero + one + TWO + three + FOUR + five + six + seven + eight + NINE + " + .unindent(); + + let buffer_text = " + zero + one + TWO + three + FOUR + FIVE + six + SEVEN + eight + nine + " + .unindent(); + + let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx); + + let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx); + + let expected_hunks = vec![ + ( + 2..3, + "two\n", + "TWO\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None), + ), + ( + 4..6, + "four\nfive\n", + "FOUR\nFIVE\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk), + ), + ( + 7..8, + "seven\n", + "SEVEN\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::HasSecondaryHunk), + ), + ]; + + assert_hunks( + uncommitted_diff.hunks_intersecting_range( + Anchor::MIN..Anchor::MAX, + &buffer, + Some(&unstaged_diff), + ), + &buffer, + &head_text, + &expected_hunks, + ); } #[gpui::test] @@ -748,25 +1052,27 @@ mod tests { let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); let diff = cx .update(|cx| { - BufferDiffSnapshot::build( - buffer.snapshot(), - Some(diff_base.clone()), - None, - None, - cx, - ) + BufferDiff::build(buffer.snapshot(), Some(diff_base.clone()), None, None, cx) }) .await; - assert_eq!(diff.hunks(&buffer).count(), 8); + assert_eq!( + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None) + .count(), + 8 + ); assert_hunks( - diff.hunks_in_row_range(7..12, &buffer), + diff.hunks_intersecting_range( + buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)), + &buffer, + None, + ), &buffer, &diff_base, &[ - (6..7, "", "HELLO\n"), - (9..10, "six\n", "SIXTEEN\n"), - (12..13, "", "WORLD\n"), + (6..7, "", "HELLO\n", DiffHunkStatus::added()), + (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified()), + (12..13, "", "WORLD\n", DiffHunkStatus::added()), ], ); } @@ -801,8 +1107,8 @@ mod tests { let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1); - let empty_diff = BufferDiffSnapshot::new(&buffer); - let diff_1 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let empty_diff = BufferDiff::build_empty(&buffer); + let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); let range = diff_1.compare(&empty_diff, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0)); @@ -820,7 +1126,7 @@ mod tests { " .unindent(), ); - let diff_2 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); assert_eq!(None, diff_2.compare(&diff_1, &buffer)); // Edit turns a deletion hunk into a modification. @@ -837,7 +1143,7 @@ mod tests { " .unindent(), ); - let diff_3 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); let range = diff_3.compare(&diff_2, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0)); @@ -854,7 +1160,7 @@ mod tests { " .unindent(), ); - let diff_4 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); let range = diff_4.compare(&diff_3, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0)); @@ -872,7 +1178,7 @@ mod tests { " .unindent(), ); - let diff_5 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text.clone(), cx); + let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx); let range = diff_5.compare(&diff_4, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0)); @@ -890,7 +1196,7 @@ mod tests { " .unindent(), ); - let diff_6 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text, cx); + let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx); 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/collab/Cargo.toml b/crates/collab/Cargo.toml index 8f133affcb..2dd571677e 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -33,7 +33,7 @@ clock.workspace = true collections.workspace = true dashmap.workspace = true derive_more.workspace = true -diff.workspace = true +buffer_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 e9c8ab39f1..be9890dcb8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -8,6 +8,7 @@ use crate::{ use anyhow::{anyhow, Result}; use assistant_context_editor::ContextStore; use assistant_slash_command::SlashCommandWorkingSet; +use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus}; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; @@ -2613,11 +2614,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2641,11 +2642,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2663,11 +2664,16 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(committed_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "TWO\n", "two\n")], + &[( + 1..2, + "TWO\n", + "two\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::HasSecondaryHunk), + )], ); }); @@ -2689,11 +2695,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); @@ -2703,11 +2709,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); @@ -2717,11 +2723,16 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_committed_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "TWO_HUNDRED\n", "two\n")], + &[( + 1..2, + "TWO_HUNDRED\n", + "two\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk), + )], ); }); @@ -2763,11 +2774,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2790,11 +2801,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &staged_text, - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2812,11 +2823,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &new_staged_text, - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); @@ -2826,11 +2837,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &new_staged_text, - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 282c10eb60..52ea66313c 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -38,7 +38,7 @@ clock.workspace = true collections.workspace = true convert_case.workspace = true db.workspace = true -diff.workspace = true +buffer_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 43a3b6acad..f10b893d08 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -73,17 +73,16 @@ 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, px, relative, size, Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, - AvailableSpace, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, - Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, - FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, - MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, - StyledText, Subscription, Task, TextRun, TextStyle, TextStyleRefinement, UTF16Selection, - UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + AvailableSpace, Background, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, + ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, + FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, + MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, + Styled, StyledText, Subscription, Task, TextRun, TextStyle, TextStyleRefinement, + UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -722,6 +721,7 @@ pub struct Editor { show_git_blame_gutter: bool, show_git_blame_inline: bool, show_git_blame_inline_delay_task: Option>, + distinguish_unstaged_diff_hunks: bool, git_blame_inline_enabled: bool, serialize_dirty_buffers: bool, show_selection_menu: Option, @@ -1418,6 +1418,7 @@ impl Editor { custom_context_menu: None, show_git_blame_gutter: false, show_git_blame_inline: false, + distinguish_unstaged_diff_hunks: false, show_selection_menu: None, show_git_blame_inline_delay_task: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), @@ -6878,8 +6879,7 @@ impl Editor { let buffer = buffer.read(cx); let original_text = diff .read(cx) - .snapshot - .base_text + .base_text() .as_ref()? .as_rope() .slice(hunk.diff_base_byte_range.clone()); @@ -12290,6 +12290,10 @@ impl Editor { }); } + pub fn set_distinguish_unstaged_diff_hunks(&mut self) { + self.distinguish_unstaged_diff_hunks = true; + } + pub fn expand_all_diff_hunks( &mut self, _: &ExpandAllHunkDiffs, @@ -13332,14 +13336,14 @@ impl Editor { &self, window: &mut Window, cx: &mut App, - ) -> BTreeMap { + ) -> BTreeMap { let snapshot = self.snapshot(window, cx); let mut used_highlight_orders = HashMap::default(); self.highlighted_rows .iter() .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) .fold( - BTreeMap::::new(), + BTreeMap::::new(), |mut unique_rows, highlight| { let start = highlight.range.start.to_display_point(&snapshot); let end = highlight.range.end.to_display_point(&snapshot); @@ -13356,7 +13360,7 @@ impl Editor { used_highlight_orders.entry(row).or_insert(highlight.index); if highlight.index >= *used_index { *used_index = highlight.index; - unique_rows.insert(DisplayRow(row), highlight.color); + unique_rows.insert(DisplayRow(row), highlight.color.into()); } } unique_rows @@ -15518,7 +15522,7 @@ impl EditorSnapshot { ) { // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk.status() == DiffHunkStatus::Removed; + let allow_adjacent = hunk.status().is_removed(); let related_to_selection = if allow_adjacent { hunk.row_range.overlaps(&query_rows) || hunk.row_range.start == query_rows.end diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6cf4526f3c..03388a19d0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7,7 +7,7 @@ use crate::{ }, JoinLines, }; -use diff::{BufferDiff, DiffHunkStatus}; +use buffer_diff::{BufferDiff, DiffHunkStatus}; use futures::StreamExt; use gpui::{ div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, @@ -11989,7 +11989,7 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { struct Row9.2; struct Row9.3; struct Row10;"#}, - vec![DiffHunkStatus::Added, DiffHunkStatus::Added], + vec![DiffHunkStatus::added(), DiffHunkStatus::added()], indoc! {r#"struct Row; struct Row1; struct Row1.1; @@ -12027,7 +12027,7 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { struct Row8; struct Row9; struct Row10;"#}, - vec![DiffHunkStatus::Added, DiffHunkStatus::Added], + vec![DiffHunkStatus::added(), DiffHunkStatus::added()], indoc! {r#"struct Row; struct Row1; struct Row2; @@ -12074,11 +12074,11 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { «ˇ// something on bottom» struct Row10;"#}, vec![ - DiffHunkStatus::Added, - DiffHunkStatus::Added, - DiffHunkStatus::Added, - DiffHunkStatus::Added, - DiffHunkStatus::Added, + DiffHunkStatus::added(), + DiffHunkStatus::added(), + DiffHunkStatus::added(), + DiffHunkStatus::added(), + DiffHunkStatus::added(), ], indoc! {r#"struct Row; ˇstruct Row1; @@ -12126,7 +12126,7 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { struct Row99; struct Row9; struct Row10;"#}, - vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified], + vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()], indoc! {r#"struct Row; struct Row1; struct Row33; @@ -12153,7 +12153,7 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { struct Row99; struct Row9; struct Row10;"#}, - vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified], + vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()], indoc! {r#"struct Row; struct Row1; struct Row33; @@ -12182,12 +12182,12 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { struct Row9; struct Row1011;ˇ"#}, vec![ - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), ], indoc! {r#"struct Row; ˇstruct Row1; @@ -12265,7 +12265,7 @@ struct Row10;"#}; ˇ struct Row8; struct Row10;"#}, - vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed], + vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()], indoc! {r#"struct Row; struct Row2; @@ -12288,7 +12288,7 @@ struct Row10;"#}; ˇ» struct Row8; struct Row10;"#}, - vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed], + vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()], indoc! {r#"struct Row; struct Row2; @@ -12313,7 +12313,7 @@ struct Row10;"#}; struct Row8;ˇ struct Row10;"#}, - vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed], + vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()], indoc! {r#"struct Row; struct Row1; ˇstruct Row2; @@ -12338,9 +12338,9 @@ struct Row10;"#}; struct Row8;ˇ» struct Row10;"#}, vec![ - DiffHunkStatus::Removed, - DiffHunkStatus::Removed, - DiffHunkStatus::Removed, + DiffHunkStatus::removed(), + DiffHunkStatus::removed(), + DiffHunkStatus::removed(), ], indoc! {r#"struct Row; struct Row1; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c6fd163a46..646822ab46 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -25,21 +25,21 @@ use crate::{ EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; +use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus}; use client::ParticipantIndex; use collections::{BTreeMap, HashMap, HashSet}; -use diff::DiffHunkStatus; use file_icons::FileIcons; 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, - ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, - Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, - Hsla, InteractiveElement, IntoElement, KeyBindingContextPredicate, Keystroke, Length, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, - StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, - WeakEntity, Window, + anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash, + point, px, quad, relative, size, svg, transparent_black, Action, AnyElement, App, + AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, + CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, + FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, + KeyBindingContextPredicate, Keystroke, Length, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + Subscription, TextRun, TextStyleRefinement, WeakEntity, Window, }; use itertools::Itertools; use language::{ @@ -85,7 +85,6 @@ enum DisplayDiffHunk { Folded { display_row: DisplayRow, }, - Unfolded { diff_base_byte_range: Range, display_row_range: Range, @@ -2116,7 +2115,7 @@ impl EditorElement { .get(&display_row) .unwrap_or(&non_relative_number); write!(&mut line_number, "{number}").unwrap(); - if row_info.diff_status == Some(DiffHunkStatus::Removed) { + if matches!(row_info.diff_status, Some(DiffHunkStatus::Removed(_))) { return None; } @@ -4007,8 +4006,10 @@ impl EditorElement { if row_infos[row_ix].diff_status.is_none() { continue; } - if row_infos[row_ix].diff_status == Some(DiffHunkStatus::Added) - && *status != DiffHunkStatus::Added + if matches!( + row_infos[row_ix].diff_status, + Some(DiffHunkStatus::Added(_)) + ) && !matches!(*status, DiffHunkStatus::Added(_)) { continue; } @@ -4191,26 +4192,26 @@ impl EditorElement { window.paint_quad(fill(Bounds { origin, size }, color)); }; - let mut current_paint: Option<(Hsla, Range)> = None; - for (&new_row, &new_color) in &layout.highlighted_rows { + let mut current_paint: Option<(gpui::Background, Range)> = None; + for (&new_row, &new_background) in &layout.highlighted_rows { match &mut current_paint { - Some((current_color, current_range)) => { - let current_color = *current_color; - let new_range_started = current_color != new_color + Some((current_background, current_range)) => { + let current_background = *current_background; + let new_range_started = current_background != new_background || current_range.end.next_row() != new_row; if new_range_started { paint_highlight( current_range.start, current_range.end, - current_color, + current_background, ); - current_paint = Some((new_color, new_row..new_row)); + current_paint = Some((new_background, new_row..new_row)); continue; } else { current_range.end = current_range.end.next_row(); } } - None => current_paint = Some((new_color, new_row..new_row)), + None => current_paint = Some((new_background, new_row..new_row)), }; } if let Some((color, range)) = current_paint { @@ -4409,6 +4410,7 @@ impl EditorElement { hunk_bounds, cx.theme().status().modified, Corners::all(px(0.)), + &DiffHunkSecondaryStatus::None, )) } DisplayDiffHunk::Unfolded { @@ -4416,22 +4418,29 @@ impl EditorElement { display_row_range, .. } => hitbox.as_ref().map(|hunk_hitbox| match status { - DiffHunkStatus::Added => ( + DiffHunkStatus::Added(secondary_status) => ( hunk_hitbox.bounds, cx.theme().status().created, Corners::all(px(0.)), + secondary_status, ), - DiffHunkStatus::Modified => ( + DiffHunkStatus::Modified(secondary_status) => ( hunk_hitbox.bounds, cx.theme().status().modified, Corners::all(px(0.)), + secondary_status, ), - DiffHunkStatus::Removed if !display_row_range.is_empty() => ( - hunk_hitbox.bounds, - cx.theme().status().deleted, - Corners::all(px(0.)), - ), - DiffHunkStatus::Removed => ( + DiffHunkStatus::Removed(secondary_status) + if !display_row_range.is_empty() => + { + ( + hunk_hitbox.bounds, + cx.theme().status().deleted, + Corners::all(px(0.)), + secondary_status, + ) + } + DiffHunkStatus::Removed(secondary_status) => ( Bounds::new( point( hunk_hitbox.origin.x - hunk_hitbox.size.width, @@ -4441,11 +4450,17 @@ impl EditorElement { ), cx.theme().status().deleted, Corners::all(1. * line_height), + secondary_status, ), }), }; - if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint { + if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) = + hunk_to_paint + { + if *secondary_status != DiffHunkSecondaryStatus::None { + background_color.a *= 0.6; + } window.paint_quad(quad( hunk_bounds, corner_radii, @@ -4481,7 +4496,7 @@ impl EditorElement { status, .. } => { - if *status == DiffHunkStatus::Removed && display_row_range.is_empty() { + if status.is_removed() && display_row_range.is_empty() { let row = display_row_range.start; let offset = line_height / 2.; @@ -5128,9 +5143,9 @@ impl EditorElement { end_display_row.0 -= 1; } let color = match &hunk.status() { - DiffHunkStatus::Added => theme.status().created, - DiffHunkStatus::Modified => theme.status().modified, - DiffHunkStatus::Removed => theme.status().deleted, + DiffHunkStatus::Added(_) => theme.status().created, + DiffHunkStatus::Modified(_) => theme.status().modified, + DiffHunkStatus::Removed(_) => theme.status().deleted, }; ColoredRange { start: start_display_row, @@ -6798,19 +6813,46 @@ impl Element for EditorElement { ) }; - let mut highlighted_rows = self - .editor - .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx)); + let (mut highlighted_rows, distinguish_unstaged_hunks) = + self.editor.update(cx, |editor, cx| { + ( + editor.highlighted_display_rows(window, cx), + editor.distinguish_unstaged_diff_hunks, + ) + }); for (ix, row_info) in row_infos.iter().enumerate() { - let color = match row_info.diff_status { - Some(DiffHunkStatus::Added) => style.status.created_background, - Some(DiffHunkStatus::Removed) => style.status.deleted_background, + let background = match row_info.diff_status { + Some(DiffHunkStatus::Added(secondary_status)) => { + let color = style.status.created_background; + match secondary_status { + DiffHunkSecondaryStatus::HasSecondaryHunk + | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk + if distinguish_unstaged_hunks => + { + pattern_slash(color, line_height.0 / 4.0) + } + _ => color.into(), + } + } + Some(DiffHunkStatus::Removed(secondary_status)) => { + let color = style.status.deleted_background; + match secondary_status { + DiffHunkSecondaryStatus::HasSecondaryHunk + | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk + if distinguish_unstaged_hunks => + { + pattern_slash(color, line_height.0 / 4.0) + } + _ => color.into(), + } + } _ => continue, }; + highlighted_rows .entry(start_row + DisplayRow(ix as u32)) - .or_insert(color); + .or_insert(background); } let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( @@ -7643,7 +7685,7 @@ pub struct EditorLayout { indent_guides: Option>, visible_display_row_range: Range, active_rows: BTreeMap, - highlighted_rows: BTreeMap, + highlighted_rows: BTreeMap, line_elements: SmallVec<[AnyElement; 1]>, line_numbers: Arc>, display_hunks: Vec<(DisplayDiffHunk, Option)>, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 2c7903296b..1627d471c0 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -1,6 +1,6 @@ use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider}; +use buffer_diff::BufferDiff; 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}; @@ -185,7 +185,7 @@ impl ProposedChangesEditor { } else { branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx)); new_diffs.push(cx.new(|cx| { - let mut diff = BufferDiff::new(&branch_buffer, cx); + let mut diff = BufferDiff::new(branch_buffer.read(cx)); let _ = diff.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 7cfaf56224..a632d9fa1e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -2,8 +2,8 @@ use crate::{ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt, }; +use buffer_diff::DiffHunkStatus; use collections::BTreeMap; -use diff::DiffHunkStatus; use futures::Future; use gpui::{ @@ -459,9 +459,9 @@ pub fn assert_state_with_diff( .zip(line_infos) .map(|(line, info)| { let mut marker = match info.diff_status { - Some(DiffHunkStatus::Added) => "+ ", - Some(DiffHunkStatus::Removed) => "- ", - Some(DiffHunkStatus::Modified) => unreachable!(), + Some(DiffHunkStatus::Added(_)) => "+ ", + Some(DiffHunkStatus::Removed(_)) => "- ", + Some(DiffHunkStatus::Modified(_)) => unreachable!(), None => { if has_diff { " " diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index a30792fe10..4f10e067b8 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -16,7 +16,7 @@ path = "src/git_ui.rs" anyhow.workspace = true collections.workspace = true db.workspace = true -diff.workspace = true +buffer_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 ea54fb1bfd..7d4a8c7a46 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1,8 +1,8 @@ use std::any::{Any, TypeId}; use anyhow::Result; +use buffer_diff::BufferDiff; use collections::HashSet; -use diff::BufferDiff; use editor::{scroll::Autoscroll, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use futures::StreamExt; @@ -126,6 +126,7 @@ impl ProjectDiff { window, cx, ); + diff_display_editor.set_distinguish_unstaged_diff_hunks(); diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.register_addon(GitPanelAddon { git_panel: git_panel.clone(), @@ -317,10 +318,10 @@ impl ProjectDiff { let snapshot = buffer.read(cx).snapshot(); let diff = diff.read(cx); - let diff_hunk_ranges = if diff.snapshot.base_text.is_none() { + let diff_hunk_ranges = if diff.base_text().is_none() { vec![Point::zero()..snapshot.max_point()] } else { - diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot) + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::>() }; diff --git a/crates/gpui/examples/pattern.rs b/crates/gpui/examples/pattern.rs index b872d6b6ad..724e2dc5db 100644 --- a/crates/gpui/examples/pattern.rs +++ b/crates/gpui/examples/pattern.rs @@ -25,10 +25,30 @@ impl Render for PatternExample { .flex_col() .border_1() .border_color(gpui::blue()) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))), + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ) + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ) + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ) + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ), ) .child( div() @@ -42,25 +62,25 @@ impl Render for PatternExample { div() .w(px(256.0)) .h(px(56.0)) - .bg(pattern_slash(gpui::red())), + .bg(pattern_slash(gpui::red(), 56.0 / 3.0)), ) .child( div() .w(px(256.0)) .h(px(56.0)) - .bg(pattern_slash(gpui::green())), + .bg(pattern_slash(gpui::green(), 56.0 / 3.0)), ) .child( div() .w(px(256.0)) .h(px(56.0)) - .bg(pattern_slash(gpui::blue())), + .bg(pattern_slash(gpui::blue(), 56.0 / 3.0)), ) .child( div() .w(px(256.0)) .h(px(26.0)) - .bg(pattern_slash(gpui::yellow())), + .bg(pattern_slash(gpui::yellow(), 56.0 / 3.0)), ), ) .child( diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 089caf1585..daec9440a4 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -587,7 +587,7 @@ pub struct Background { pub(crate) tag: BackgroundTag, pub(crate) color_space: ColorSpace, pub(crate) solid: Hsla, - pub(crate) angle: f32, + pub(crate) gradient_angle_or_pattern_height: f32, pub(crate) colors: [LinearColorStop; 2], /// Padding for alignment for repr(C) layout. pad: u32, @@ -600,7 +600,7 @@ impl Default for Background { tag: BackgroundTag::Solid, solid: Hsla::default(), color_space: ColorSpace::default(), - angle: 0.0, + gradient_angle_or_pattern_height: 0.0, colors: [LinearColorStop::default(), LinearColorStop::default()], pad: 0, } @@ -608,10 +608,11 @@ impl Default for Background { } /// Creates a hash pattern background -pub fn pattern_slash(color: Hsla) -> Background { +pub fn pattern_slash(color: Hsla, thickness: f32) -> Background { Background { tag: BackgroundTag::PatternSlash, solid: color, + gradient_angle_or_pattern_height: thickness, ..Default::default() } } @@ -630,7 +631,7 @@ pub fn linear_gradient( ) -> Background { Background { tag: BackgroundTag::LinearGradient, - angle, + gradient_angle_or_pattern_height: angle, colors: [from.into(), to.into()], ..Default::default() } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index b41ffb26ef..e7fe99a515 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -51,7 +51,7 @@ struct Background { // 1u is Oklab color color_space: u32, solid: Hsla, - angle: f32, + gradient_angle_or_pattern_height: f32, colors: array, pad: u32, } @@ -310,17 +310,18 @@ fn prepare_gradient_color(tag: u32, color_space: u32, } fn gradient_color(background: Background, position: vec2, bounds: Bounds, - sold_color: vec4, color0: vec4, color1: vec4) -> vec4 { + solid_color: vec4, color0: vec4, color1: vec4) -> vec4 { var background_color = vec4(0.0); switch (background.tag) { default: { - return sold_color; + return solid_color; } case 1u: { // Linear gradient background. // -90 degrees to match the CSS gradient angle. - let radians = (background.angle % 360.0 - 90.0) * M_PI_F / 180.0; + let angle = background.gradient_angle_or_pattern_height; + let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0; var direction = vec2(cos(radians), sin(radians)); let stop0_percentage = background.colors[0].percentage; let stop1_percentage = background.colors[1].percentage; @@ -359,19 +360,18 @@ fn gradient_color(background: Background, position: vec2, bounds: Bounds, } } case 2u: { - let base_pattern_size = bounds.size.y / 5.0; - let width = base_pattern_size * 0.5; - let slash_spacing = 0.89; - let radians = M_PI_F / 4.0; + let pattern_height = background.gradient_angle_or_pattern_height; + let stripe_angle = M_PI_F / 4.0; + let pattern_period = pattern_height * sin(stripe_angle); let rotation = mat2x2( - cos(radians), -sin(radians), - sin(radians), cos(radians) + cos(stripe_angle), -sin(stripe_angle), + sin(stripe_angle), cos(stripe_angle) ); let relative_position = position - bounds.origin; let rotated_point = rotation * relative_position; - let pattern = (rotated_point.x / slash_spacing) % (base_pattern_size * 2.0); - let distance = min(pattern, base_pattern_size * 2.0 - pattern) - width; - background_color = sold_color; + let pattern = rotated_point.x % pattern_period; + let distance = min(pattern, pattern_period - pattern) - pattern_period / 4; + background_color = solid_color; background_color.a *= saturate(0.5 - distance); } } diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 941c20c33d..2ff64189fc 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -833,7 +833,8 @@ float4 fill_color(Background background, break; case 1: { // -90 degrees to match the CSS gradient angle. - float radians = (fmod(background.angle, 360.0) - 90.0) * (M_PI_F / 180.0); + float gradient_angle = background.gradient_angle_or_pattern_height; + float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0); float2 direction = float2(cos(radians), sin(radians)); // Expand the short side to be the same as the long side @@ -874,19 +875,14 @@ float4 fill_color(Background background, break; } case 2: { - // This pattern is full of magic numbers to make it line up perfectly - // when vertically stacked. Make sure you know what you are doing - // if you change this! - - float base_pattern_size = bounds.size.height / 5; - float width = base_pattern_size * 0.5; - float slash_spacing = .89; - float radians = M_PI_F / 4.0; - float2x2 rotation = rotate2d(radians); + float pattern_height = background.gradient_angle_or_pattern_height; + float stripe_angle = M_PI_F / 4.0; + float pattern_period = pattern_height * sin(stripe_angle); + float2x2 rotation = rotate2d(stripe_angle); float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); float2 rotated_point = rotation * relative_position; - float pattern = fmod(rotated_point.x / slash_spacing, base_pattern_size * 2.0); - float distance = min(pattern, base_pattern_size * 2.0 - pattern) - width; + float pattern = fmod(rotated_point.x, pattern_period); + float distance = min(pattern, pattern_period - pattern) - pattern_period / 4.0; color = solid_color; color.a *= saturate(0.5 - distance); break; diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index c9e1be241e..e2e9d55402 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -14,7 +14,7 @@ doctest = false [features] test-support = [ - "diff/test-support", + "buffer_diff/test-support", "gpui/test-support", "language/test-support", "text/test-support", @@ -26,7 +26,7 @@ anyhow.workspace = true clock.workspace = true collections.workspace = true ctor.workspace = true -diff.workspace = true +buffer_diff.workspace = true env_logger.workspace = true futures.workspace = true gpui.workspace = true @@ -47,7 +47,7 @@ tree-sitter.workspace = true util.workspace = true [dev-dependencies] -diff = { workspace = true, features = ["test-support"] } +buffer_diff = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } indoc.workspace = true language = { workspace = true, features = ["test-support"] } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index ca6bc8cbf6..4357dbc7ac 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -73,7 +73,7 @@ impl Anchor { if let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) { let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); @@ -110,7 +110,7 @@ impl Anchor { if let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) { if a.buffer_id == Some(base_text.remote_id()) { return a.bias_left(base_text); @@ -135,7 +135,7 @@ impl Anchor { if let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) { if a.buffer_id == Some(base_text.remote_id()) { return a.bias_right(&base_text); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0e8359412a..2adbb6a81c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7,9 +7,11 @@ pub use anchor::{Anchor, AnchorRangeExt, Offset}; pub use position::{TypedOffset, TypedPoint, TypedRow}; use anyhow::{anyhow, Result}; +use buffer_diff::{ + BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkSecondaryStatus, DiffHunkStatus, +}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; -use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkStatus}; use futures::{channel::mpsc, SinkExt}; use gpui::{App, Context, Entity, EntityId, EventEmitter, Task}; use itertools::Itertools; @@ -129,16 +131,18 @@ pub struct MultiBufferDiffHunk { pub excerpt_id: ExcerptId, /// The range within the buffer's diff base that this hunk corresponds to. pub diff_base_byte_range: Range, + /// Whether or not this hunk also appears in the 'secondary diff'. + pub secondary_status: DiffHunkSecondaryStatus, } impl MultiBufferDiffHunk { pub fn status(&self) -> DiffHunkStatus { if self.buffer_range.start == self.buffer_range.end { - DiffHunkStatus::Removed + DiffHunkStatus::Removed(self.secondary_status) } else if self.diff_base_byte_range.is_empty() { - DiffHunkStatus::Added + DiffHunkStatus::Added(self.secondary_status) } else { - DiffHunkStatus::Modified + DiffHunkStatus::Modified(self.secondary_status) } } } @@ -225,7 +229,14 @@ impl DiffState { DiffState { _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event { BufferDiffEvent::DiffChanged { changed_range } => { - this.buffer_diff_changed(diff, changed_range.clone(), cx) + let changed_range = if let Some(changed_range) = changed_range { + changed_range.clone() + } else if diff.read(cx).base_text().is_none() && this.all_diff_hunks_expanded { + text::Anchor::MIN..text::Anchor::MAX + } else { + return; + }; + this.buffer_diff_changed(diff, changed_range, cx) } BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx), }), @@ -241,7 +252,7 @@ pub struct MultiBufferSnapshot { excerpts: SumTree, excerpt_ids: SumTree, diffs: TreeMap, - pub diff_transforms: SumTree, + diff_transforms: SumTree, trailing_excerpt_update_count: usize, non_text_state_update_count: usize, edit_count: usize, @@ -252,20 +263,27 @@ pub struct MultiBufferSnapshot { } #[derive(Debug, Clone)] -pub enum DiffTransform { +enum DiffTransform { BufferContent { summary: TextSummary, - inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>, + inserted_hunk_info: Option, }, DeletedHunk { summary: TextSummary, buffer_id: BufferId, - hunk_anchor: (ExcerptId, text::Anchor), + hunk_info: DiffTransformHunkInfo, base_text_byte_range: Range, has_trailing_newline: bool, }, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct DiffTransformHunkInfo { + excerpt_id: ExcerptId, + hunk_start_anchor: text::Anchor, + hunk_secondary_status: DiffHunkSecondaryStatus, +} + #[derive(Clone)] pub struct ExcerptInfo { pub id: ExcerptId, @@ -310,7 +328,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`]. @@ -431,7 +449,7 @@ struct MultiBufferCursor<'a, D: TextDimension> { struct MultiBufferRegion<'a, D: TextDimension> { buffer: &'a BufferSnapshot, is_main_buffer: bool, - is_inserted_hunk: bool, + diff_hunk_status: Option, excerpt: &'a Excerpt, buffer_range: Range, range: Range, @@ -2146,7 +2164,7 @@ impl MultiBuffer { let mut snapshot = self.snapshot.borrow_mut(); let diff = diff.read(cx); let buffer_id = diff.buffer_id; - let diff = diff.snapshot.clone(); + let diff = diff.snapshot(cx); snapshot.diffs.insert(buffer_id, diff); } @@ -2160,36 +2178,29 @@ impl MultiBuffer { let diff = diff.read(cx); let buffer_id = diff.buffer_id; - let mut diff = diff.snapshot.clone(); - if diff.base_text.is_none() && self.all_diff_hunks_expanded { - diff = BufferDiffSnapshot::new_with_single_insertion(cx); - } - - let mut snapshot = self.snapshot.borrow_mut(); - let base_text_changed = - snapshot - .diffs - .get(&buffer_id) - .map_or(true, |diff_snapshot| { - match (&diff_snapshot.base_text, &diff.base_text) { - (None, None) => false, - (None, Some(_)) => true, - (Some(_), None) => true, - (Some(old), Some(new)) => { - let (old_id, old_empty) = (old.remote_id(), old.is_empty()); - let (new_id, new_empty) = (new.remote_id(), new.is_empty()); - new_id != old_id && (!new_empty || !old_empty) - } - } - }); - snapshot.diffs.insert(buffer_id, diff); - let buffers = self.buffers.borrow(); let Some(buffer_state) = buffers.get(&buffer_id) else { return; }; - let diff_change_range = range.to_offset(buffer_state.buffer.read(cx)); + let buffer = buffer_state.buffer.read(cx); + let diff_change_range = range.to_offset(buffer); + + let mut new_diff = diff.snapshot(cx); + if new_diff.base_text().is_none() && self.all_diff_hunks_expanded { + let secondary_diff_insertion = new_diff + .secondary_diff() + .map_or(true, |secondary_diff| secondary_diff.base_text().is_none()); + new_diff = BufferDiff::build_with_single_insertion(secondary_diff_insertion, cx); + } + + let mut snapshot = self.snapshot.borrow_mut(); + let base_text_changed = snapshot + .diffs + .get(&buffer_id) + .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff)); + + snapshot.diffs.insert(buffer_id, new_diff); let mut excerpt_edits = Vec::new(); for locator in &buffer_state.excerpts { @@ -2367,7 +2378,7 @@ impl MultiBuffer { if *cursor.start() >= end { break; } - if item.hunk_anchor().is_some() { + if item.hunk_info().is_some() { return true; } cursor.next(&()); @@ -2820,11 +2831,11 @@ impl MultiBuffer { let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) && match old_diff_transforms.item() { Some(DiffTransform::BufferContent { - inserted_hunk_anchor: Some(hunk_anchor), + inserted_hunk_info: Some(hunk), .. - }) => excerpts - .item() - .is_some_and(|excerpt| hunk_anchor.1.is_valid(&excerpt.buffer)), + }) => excerpts.item().is_some_and(|excerpt| { + hunk.hunk_start_anchor.is_valid(&excerpt.buffer) + }), _ => true, }; @@ -2853,7 +2864,7 @@ impl MultiBuffer { new_diff_transforms.push( DiffTransform::BufferContent { summary: Default::default(), - inserted_hunk_anchor: None, + inserted_hunk_info: None, }, &(), ); @@ -2876,8 +2887,8 @@ impl MultiBuffer { excerpts: &mut Cursor>, old_diff_transforms: &mut Cursor, usize)>, new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option<(TypedOffset, ExcerptId, text::Anchor)>, - old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>, + end_of_current_insert: &mut Option<(TypedOffset, DiffTransformHunkInfo)>, + old_expanded_hunks: &mut HashSet, snapshot: &MultiBufferSnapshot, change_kind: DiffChangeKind, ) -> bool { @@ -2889,12 +2900,12 @@ impl MultiBuffer { // Record which hunks were previously expanded. while let Some(item) = old_diff_transforms.item() { - if let Some(hunk_anchor) = item.hunk_anchor() { + if let Some(hunk_info) = item.hunk_info() { log::trace!( "previously expanded hunk at {}", old_diff_transforms.start().0 ); - old_expanded_hunks.insert(hunk_anchor); + old_expanded_hunks.insert(hunk_info); } if old_diff_transforms.end(&()).0 > edit.old.end { break; @@ -2918,7 +2929,7 @@ impl MultiBuffer { if let Some((diff, base_text)) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| Some((diff, diff.base_text.as_ref()?))) + .and_then(|diff| Some((diff, diff.base_text()?))) { let buffer = &excerpt.buffer; let excerpt_start = *excerpts.start(); @@ -2936,7 +2947,11 @@ impl MultiBuffer { for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); - let hunk_anchor = (excerpt.id, hunk.buffer_range.start); + let hunk_info = DiffTransformHunkInfo { + excerpt_id: excerpt.id, + hunk_start_anchor: hunk.buffer_range.start, + hunk_secondary_status: hunk.secondary_status, + }; if hunk_buffer_range.start < excerpt_buffer_start { log::trace!("skipping hunk that starts before excerpt"); continue; @@ -2960,7 +2975,7 @@ impl MultiBuffer { // For every existing hunk, determine if it was previously expanded // and if it should currently be expanded. - let was_previously_expanded = old_expanded_hunks.contains(&hunk_anchor); + let was_previously_expanded = old_expanded_hunks.contains(&hunk_info); let should_expand_hunk = match &change_kind { DiffChangeKind::DiffUpdated { base_changed: true } => { self.all_diff_hunks_expanded @@ -3008,7 +3023,7 @@ impl MultiBuffer { base_text_byte_range: hunk.diff_base_byte_range.clone(), summary: base_text_summary, buffer_id: excerpt.buffer_id, - hunk_anchor, + hunk_info, has_trailing_newline, }, &(), @@ -3016,11 +3031,8 @@ impl MultiBuffer { } if !hunk_buffer_range.is_empty() { - *end_of_current_insert = Some(( - hunk_excerpt_end.min(excerpt_end), - hunk_anchor.0, - hunk_anchor.1, - )); + *end_of_current_insert = + Some((hunk_excerpt_end.min(excerpt_end), hunk_info)); } } } @@ -3042,13 +3054,13 @@ impl MultiBuffer { subtree: SumTree, ) { if let Some(DiffTransform::BufferContent { - inserted_hunk_anchor, + inserted_hunk_info, summary, }) = subtree.first() { if self.extend_last_buffer_content_transform( new_transforms, - *inserted_hunk_anchor, + *inserted_hunk_info, *summary, ) { let mut cursor = subtree.cursor::<()>(&()); @@ -3067,7 +3079,7 @@ impl MultiBuffer { transform: DiffTransform, ) { if let DiffTransform::BufferContent { - inserted_hunk_anchor, + inserted_hunk_info: inserted_hunk_anchor, summary, } = transform { @@ -3087,19 +3099,14 @@ impl MultiBuffer { old_snapshot: &MultiBufferSnapshot, new_transforms: &mut SumTree, end_offset: ExcerptOffset, - current_inserted_hunk: Option<(ExcerptOffset, ExcerptId, text::Anchor)>, + current_inserted_hunk: Option<(ExcerptOffset, DiffTransformHunkInfo)>, ) { - let inserted_region = - current_inserted_hunk.map(|(insertion_end_offset, excerpt_id, anchor)| { - ( - end_offset.min(insertion_end_offset), - Some((excerpt_id, anchor)), - ) - }); + let inserted_region = current_inserted_hunk.map(|(insertion_end_offset, hunk_info)| { + (end_offset.min(insertion_end_offset), Some(hunk_info)) + }); let unchanged_region = [(end_offset, None)]; - for (end_offset, inserted_hunk_anchor) in - inserted_region.into_iter().chain(unchanged_region) + for (end_offset, inserted_hunk_info) in inserted_region.into_iter().chain(unchanged_region) { let start_offset = new_transforms.summary().excerpt_len(); if end_offset <= start_offset { @@ -3110,13 +3117,13 @@ impl MultiBuffer { if !self.extend_last_buffer_content_transform( new_transforms, - inserted_hunk_anchor, + inserted_hunk_info, summary_to_add, ) { new_transforms.push( DiffTransform::BufferContent { summary: summary_to_add, - inserted_hunk_anchor, + inserted_hunk_info, }, &(), ) @@ -3127,7 +3134,7 @@ impl MultiBuffer { fn extend_last_buffer_content_transform( &self, new_transforms: &mut SumTree, - new_inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>, + new_inserted_hunk_info: Option, summary_to_add: TextSummary, ) -> bool { let mut did_extend = false; @@ -3135,10 +3142,10 @@ impl MultiBuffer { |last_transform| { if let DiffTransform::BufferContent { summary, - inserted_hunk_anchor, + inserted_hunk_info: inserted_hunk_anchor, } = last_transform { - if *inserted_hunk_anchor == new_inserted_hunk_anchor { + if *inserted_hunk_anchor == new_inserted_hunk_info { *summary += summary_to_add; did_extend = true; } @@ -3469,6 +3476,7 @@ impl MultiBufferSnapshot { excerpt_id: excerpt.id, buffer_range: hunk.buffer_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(), + secondary_status: hunk.secondary_status, }) }) } @@ -3837,6 +3845,7 @@ impl MultiBufferSnapshot { excerpt_id: excerpt.id, buffer_range: hunk.buffer_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(), + secondary_status: hunk.secondary_status, }); } } @@ -4309,10 +4318,7 @@ impl MultiBufferSnapshot { } => { let buffer_start = base_text_byte_range.start + start_overshoot; let mut buffer_end = base_text_byte_range.start + end_overshoot; - let Some(base_text) = self - .diffs - .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + let Some(base_text) = self.diffs.get(buffer_id).and_then(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.start) }; @@ -4361,10 +4367,7 @@ impl MultiBufferSnapshot { .. } => { let buffer_end = base_text_byte_range.start + overshoot; - let Some(base_text) = self - .diffs - .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + let Some(base_text) = self.diffs.get(buffer_id).and_then(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.end) }; @@ -4469,10 +4472,8 @@ impl MultiBufferSnapshot { }) => { let mut in_deleted_hunk = false; if let Some(diff_base_anchor) = &anchor.diff_base_anchor { - if let Some(base_text) = self - .diffs - .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + if let Some(base_text) = + self.diffs.get(buffer_id).and_then(|diff| diff.base_text()) { if base_text.can_resolve(&diff_base_anchor) { let base_text_offset = diff_base_anchor.to_offset(&base_text); @@ -4809,7 +4810,7 @@ impl MultiBufferSnapshot { let base_text = self .diffs .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) .expect("missing diff base"); if offset_in_transform > base_text_byte_range.len() { debug_assert!(*has_trailing_newline); @@ -5969,17 +5970,17 @@ impl MultiBufferSnapshot { for item in self.diff_transforms.iter() { if let DiffTransform::BufferContent { summary, - inserted_hunk_anchor, + inserted_hunk_info, } = item { if let Some(DiffTransform::BufferContent { - inserted_hunk_anchor: prev_inserted_hunk_anchor, + inserted_hunk_info: prev_inserted_hunk_info, .. }) = prev_transform { - if *inserted_hunk_anchor == *prev_inserted_hunk_anchor { + if *inserted_hunk_info == *prev_inserted_hunk_info { panic!( - "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_anchor:?}. transforms: {:+?}", + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", self.diff_transforms.items(&())); } } @@ -6149,10 +6150,11 @@ where buffer_id, base_text_byte_range, has_trailing_newline, + hunk_info, .. } => { let diff = self.diffs.get(&buffer_id)?; - let buffer = diff.base_text.as_ref()?; + let buffer = diff.base_text()?; let mut rope_cursor = buffer.as_rope().cursor(0); let buffer_start = rope_cursor.summary::(base_text_byte_range.start); let buffer_range_len = rope_cursor.summary::(base_text_byte_range.end); @@ -6165,14 +6167,15 @@ where excerpt, has_trailing_newline: *has_trailing_newline, is_main_buffer: false, - is_inserted_hunk: false, + diff_hunk_status: Some(DiffHunkStatus::Removed( + hunk_info.hunk_secondary_status, + )), buffer_range: buffer_start..buffer_end, range: start..end, }); } DiffTransform::BufferContent { - inserted_hunk_anchor, - .. + inserted_hunk_info, .. } => { let buffer = &excerpt.buffer; let buffer_context_start = excerpt.range.context.start.summary::(buffer); @@ -6209,7 +6212,8 @@ where excerpt, has_trailing_newline, is_main_buffer: true, - is_inserted_hunk: inserted_hunk_anchor.is_some(), + diff_hunk_status: inserted_hunk_info + .map(|info| DiffHunkStatus::Added(info.hunk_secondary_status)), buffer_range: buffer_start..buffer_end, range: start..end, }) @@ -6717,13 +6721,12 @@ impl sum_tree::KeyedItem for ExcerptIdMapping { } impl DiffTransform { - fn hunk_anchor(&self) -> Option<(ExcerptId, text::Anchor)> { + fn hunk_info(&self) -> Option { match self { - DiffTransform::DeletedHunk { hunk_anchor, .. } => Some(*hunk_anchor), + DiffTransform::DeletedHunk { hunk_info, .. } => Some(*hunk_info), DiffTransform::BufferContent { - inserted_hunk_anchor, - .. - } => *inserted_hunk_anchor, + inserted_hunk_info, .. + } => *inserted_hunk_info, } } } @@ -7020,13 +7023,9 @@ impl<'a> Iterator for MultiBufferRows<'a> { buffer_id: Some(region.buffer.remote_id()), buffer_row: Some(buffer_point.row), multibuffer_row: Some(MultiBufferRow(self.point.row)), - diff_status: if region.is_inserted_hunk && self.point < region.range.end { - Some(DiffHunkStatus::Added) - } else if !region.is_main_buffer { - Some(DiffHunkStatus::Removed) - } else { - None - }, + diff_status: region + .diff_hunk_status + .filter(|_| self.point < region.range.end), }); self.point += Point::new(1, 0); result @@ -7194,7 +7193,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } chunks } else { - let base_buffer = &self.diffs.get(&buffer_id)?.base_text.as_ref()?; + let base_buffer = &self.diffs.get(&buffer_id)?.base_text()?; base_buffer.chunks(base_text_start..base_text_end, self.language_aware) }; diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index d98a9db30a..3b8d0e2a16 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 diff::DiffHunkStatus; +use buffer_diff::DiffHunkStatus; use gpui::{App, TestAppContext}; use indoc::indoc; use language::{Buffer, Rope}; @@ -979,8 +979,6 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) { let diff = 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_diff(diff.clone(), cx); multibuffer.push_excerpts( buffer.clone(), [ExcerptRange { @@ -989,6 +987,8 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) { }], cx, ); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.add_diff(diff.clone(), cx); }); cx.run_until_parked(); @@ -1325,13 +1325,13 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) { .map(|info| (info.buffer_row, info.diff_status)) .collect::>(), vec![ - (Some(0), Some(DiffHunkStatus::Added)), + (Some(0), Some(DiffHunkStatus::added())), (Some(1), None), - (Some(1), Some(DiffHunkStatus::Removed)), - (Some(2), Some(DiffHunkStatus::Added)), + (Some(1), Some(DiffHunkStatus::removed())), + (Some(2), Some(DiffHunkStatus::added())), (Some(3), None), - (Some(3), Some(DiffHunkStatus::Removed)), - (Some(4), Some(DiffHunkStatus::Removed)), + (Some(3), Some(DiffHunkStatus::removed())), + (Some(4), Some(DiffHunkStatus::removed())), (Some(4), None), (Some(5), None) ] @@ -1999,12 +1999,8 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id()); let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id()); - let base_id_1 = diff_1.read_with(cx, |diff, _| { - diff.snapshot.base_text.as_ref().unwrap().remote_id() - }); - let base_id_2 = diff_2.read_with(cx, |diff, _| { - diff.snapshot.base_text.as_ref().unwrap().remote_id() - }); + let base_id_1 = diff_1.read_with(cx, |diff, _| diff.base_text().as_ref().unwrap().remote_id()); + let base_id_2 = diff_2.read_with(cx, |diff, _| diff.base_text().as_ref().unwrap().remote_id()); let buffer_lines = (0..=snapshot.max_row().0) .map(|row| { @@ -2191,9 +2187,8 @@ impl ReferenceMultibuffer { let Some(diff) = self.diffs.get(&buffer_id) else { return; }; - let diff = diff.read(cx).snapshot.clone(); let excerpt_range = excerpt.range.to_offset(&buffer); - for hunk in diff.hunks_intersecting_range(range, &buffer) { + for hunk in diff.read(cx).hunks_intersecting_range(range, &buffer, cx) { let hunk_range = hunk.buffer_range.to_offset(&buffer); if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end { continue; @@ -2226,12 +2221,12 @@ impl ReferenceMultibuffer { let buffer = excerpt.buffer.read(cx); let buffer_range = excerpt.range.to_offset(buffer); let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx); - let diff = diff.snapshot.clone(); - let base_buffer = diff.base_text.as_ref().unwrap(); + // let diff = diff.snapshot.clone(); + let base_buffer = diff.base_text().unwrap(); let mut offset = buffer_range.start; let mut hunks = diff - .hunks_intersecting_range(excerpt.range.clone(), buffer) + .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .peekable(); while let Some(hunk) = hunks.next() { @@ -2284,7 +2279,7 @@ impl ReferenceMultibuffer { buffer_start: Some( base_buffer.offset_to_point(hunk.diff_base_byte_range.start), ), - status: Some(DiffHunkStatus::Removed), + status: Some(DiffHunkStatus::Removed(hunk.secondary_status)), }); } @@ -2299,7 +2294,7 @@ impl ReferenceMultibuffer { buffer_id: Some(buffer.remote_id()), range: len..text.len(), buffer_start: Some(buffer.offset_to_point(offset)), - status: Some(DiffHunkStatus::Added), + status: Some(DiffHunkStatus::Added(hunk.secondary_status)), }); offset = hunk_range.end; } @@ -2365,8 +2360,8 @@ impl ReferenceMultibuffer { let buffer = excerpt.buffer.read(cx).snapshot(); let excerpt_range = excerpt.range.to_offset(&buffer); let buffer_id = buffer.remote_id(); - let diff = &self.diffs.get(&buffer_id).unwrap().read(cx).snapshot; - let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable(); + let diff = self.diffs.get(&buffer_id).unwrap().read(cx); + let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer, cx).peekable(); excerpt.expanded_diff_hunks.retain(|hunk_anchor| { if !hunk_anchor.is_valid(&buffer) { return false; @@ -2670,7 +2665,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { expected_row_infos .into_iter() .filter_map( - |info| if info.diff_status == Some(DiffHunkStatus::Removed) { + |info| if matches!(info.diff_status, Some(DiffHunkStatus::Removed(_))) { None } else { info.buffer_row @@ -3027,9 +3022,9 @@ fn format_diff( .zip(row_infos) .map(|((ix, line), info)| { let marker = match info.diff_status { - Some(DiffHunkStatus::Added) => "+ ", - Some(DiffHunkStatus::Removed) => "- ", - Some(DiffHunkStatus::Modified) => unreachable!(), + Some(DiffHunkStatus::Added(_)) => "+ ", + Some(DiffHunkStatus::Removed(_)) => "- ", + Some(DiffHunkStatus::Modified(_)) => unreachable!(), None => { if has_diff && !line.is_empty() { " " diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index bb96d1b518..23ab43c46b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -30,7 +30,7 @@ async-trait.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -diff.workspace = true +buffer_diff.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -78,7 +78,7 @@ fancy-regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } -diff = { workspace = true, features = ["test-support"] } +buffer_diff = { workspace = true, features = ["test-support"] } env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } git2.workspace = true diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 7eda617c88..0a8721b4b0 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -6,9 +6,9 @@ use crate::{ }; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; use anyhow::{anyhow, bail, Context as _, Result}; +use buffer_diff::{BufferDiff, BufferDiffEvent}; use client::Client; use collections::{hash_map, HashMap, HashSet}; -use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot}; use fs::Fs; use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt}; use git::{blame::Blame, repository::RepoPath}; @@ -207,72 +207,74 @@ impl BufferDiffState { _ => false, }; self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move { + let mut unstaged_changed_range = None; if let Some(unstaged_diff) = &unstaged_diff { - let snapshot = if index_changed || language_changed { - cx.update(|cx| { - BufferDiffSnapshot::build( - buffer.clone(), - index, - language.clone(), - language_registry.clone(), - cx, - ) - })? - .await - } else { - unstaged_diff - .read_with(&cx, |changes, cx| { - BufferDiffSnapshot::build_with_base_buffer( - buffer.clone(), - index, - changes.snapshot.base_text.clone(), - cx, - ) - })? - .await - }; + unstaged_changed_range = BufferDiff::update_diff( + unstaged_diff.clone(), + buffer.clone(), + index, + index_changed, + language_changed, + language.clone(), + language_registry.clone(), + &mut cx, + ) + .await?; - unstaged_diff.update(&mut cx, |unstaged_diff, cx| { - unstaged_diff.set_state(snapshot, &buffer, cx); + unstaged_diff.update(&mut cx, |_, cx| { if language_changed { cx.emit(BufferDiffEvent::LanguageChanged); } + if let Some(changed_range) = unstaged_changed_range.clone() { + cx.emit(BufferDiffEvent::DiffChanged { + changed_range: Some(changed_range), + }) + } })?; } if let Some(uncommitted_diff) = &uncommitted_diff { - let snapshot = + let uncommitted_changed_range = if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) { - unstaged_diff.read_with(&cx, |diff, _| diff.snapshot.clone())? - } else if head_changed || language_changed { - cx.update(|cx| { - BufferDiffSnapshot::build( - buffer.clone(), - head, - language.clone(), - language_registry.clone(), - cx, - ) + uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| { + uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx) })? - .await } else { - uncommitted_diff - .read_with(&cx, |changes, cx| { - BufferDiffSnapshot::build_with_base_buffer( - buffer.clone(), - head, - changes.snapshot.base_text.clone(), - cx, - ) - })? - .await + BufferDiff::update_diff( + uncommitted_diff.clone(), + buffer.clone(), + head, + head_changed, + language_changed, + language.clone(), + language_registry.clone(), + &mut cx, + ) + .await? }; - uncommitted_diff.update(&mut cx, |diff, cx| { - diff.set_state(snapshot, &buffer, cx); + uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| { if language_changed { cx.emit(BufferDiffEvent::LanguageChanged); } + let changed_range = match (unstaged_changed_range, uncommitted_changed_range) { + (None, None) => None, + (Some(unstaged_range), None) => { + uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx) + } + (None, Some(uncommitted_range)) => Some(uncommitted_range), + (Some(unstaged_range), Some(uncommitted_range)) => maybe!({ + let expanded_range = uncommitted_diff.range_to_hunk_range( + unstaged_range, + &buffer, + cx, + )?; + let start = expanded_range.start.min(&uncommitted_range.start, &buffer); + let end = expanded_range.end.max(&uncommitted_range.end, &buffer); + Some(start..end) + }), + }; + cx.emit(BufferDiffEvent::DiffChanged { changed_range }); })?; } @@ -280,6 +282,7 @@ impl BufferDiffState { this.update(&mut cx, |this, _| { this.index_changed = false; this.head_changed = false; + this.language_changed = false; for tx in this.diff_updated_futures.drain(..) { tx.send(()).ok(); } @@ -1478,29 +1481,19 @@ impl BufferStore { diff_state.language = language; diff_state.language_registry = language_registry; - let diff = cx.new(|_| BufferDiff { - buffer_id, - snapshot: BufferDiffSnapshot::new(&text_snapshot), - unstaged_diff: None, - }); + let diff = cx.new(|_| BufferDiff::new(&text_snapshot)); match kind { DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()), DiffKind::Uncommitted => { let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() { diff } else { - let unstaged_diff = cx.new(|_| BufferDiff { - buffer_id, - snapshot: BufferDiffSnapshot::new(&text_snapshot), - unstaged_diff: None, - }); + let unstaged_diff = cx.new(|_| BufferDiff::new(&text_snapshot)); diff_state.unstaged_diff = Some(unstaged_diff.downgrade()); unstaged_diff }; - diff.update(cx, |diff, _| { - diff.unstaged_diff = Some(unstaged_diff); - }); + diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff)); diff_state.uncommitted_diff = Some(diff.downgrade()) } }; @@ -2397,9 +2390,8 @@ impl BufferStore { shared.diff = Some(diff.clone()); } })?; - let staged_text = diff.read_with(&cx, |diff, _| { - diff.snapshot.base_text.as_ref().map(|buffer| buffer.text()) - })?; + let staged_text = + diff.read_with(&cx, |diff, _| diff.base_text().map(|buffer| buffer.text()))?; Ok(proto::OpenUnstagedDiffResponse { staged_text }) } @@ -2430,14 +2422,13 @@ impl BufferStore { use proto::open_uncommitted_diff_response::Mode; let staged_buffer = diff - .unstaged_diff - .as_ref() - .and_then(|diff| diff.read(cx).snapshot.base_text.as_ref()); + .secondary_diff() + .and_then(|diff| diff.read(cx).base_text()); let mode; let staged_text; let committed_text; - if let Some(committed_buffer) = &diff.snapshot.base_text { + if let Some(committed_buffer) = diff.base_text() { committed_text = Some(committed_buffer.text()); if let Some(staged_buffer) = staged_buffer { if staged_buffer.remote_id() == committed_buffer.remote_id() { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7d0c0f6796..5446471b90 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -21,7 +21,7 @@ mod project_tests; mod direnv; mod environment; -use diff::BufferDiff; +use buffer_diff::BufferDiff; pub use environment::EnvironmentErrorMessage; use git::Repository; pub mod search_history; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 32fff6f1aa..a002431253 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,5 +1,5 @@ use crate::{Event, *}; -use diff::assert_hunks; +use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus}; use fs::FakeFs; use futures::{future, StreamExt}; use gpui::{App, SemanticVersion, UpdateGlobal}; @@ -5692,15 +5692,16 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, &unstaged_diff.base_text_string().unwrap(), &[ - (0..1, "", "// print goodbye\n"), + (0..1, "", "// print goodbye\n", DiffHunkStatus::added()), ( 2..3, " println!(\"hello world\");\n", " println!(\"goodbye world\");\n", + DiffHunkStatus::modified(), ), ], ); @@ -5722,10 +5723,15 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, - &unstaged_diff.snapshot.base_text.as_ref().unwrap().text(), - &[(2..3, "", " println!(\"goodbye world\");\n")], + &unstaged_diff.base_text().unwrap().text(), + &[( + 2..3, + "", + " println!(\"goodbye world\");\n", + DiffHunkStatus::added(), + )], ); }); } @@ -5795,10 +5801,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { uncommitted_diff.read_with(cx, |diff, _| { assert_eq!( - diff.snapshot - .base_text - .as_ref() - .and_then(|base| base.language().cloned()), + diff.base_text().and_then(|base| base.language().cloned()), Some(language) ) }); @@ -5807,15 +5810,21 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, &uncommitted_diff.base_text_string().unwrap(), &[ - (0..1, "", "// print goodbye\n"), + ( + 0..1, + "", + "// print goodbye\n", + DiffHunkStatus::Added(DiffHunkSecondaryStatus::HasSecondaryHunk), + ), ( 2..3, " println!(\"hello world\");\n", " println!(\"goodbye world\");\n", + DiffHunkStatus::modified(), ), ], ); @@ -5837,10 +5846,15 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, - &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(), - &[(2..3, "", " println!(\"goodbye world\");\n")], + &uncommitted_diff.base_text().unwrap().text(), + &[( + 2..3, + "", + " println!(\"goodbye world\");\n", + DiffHunkStatus::added(), + )], ); }); } @@ -5898,13 +5912,14 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, - &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(), + &uncommitted_diff.base_text_string().unwrap(), &[( 1..2, " println!(\"hello from HEAD\");\n", " println!(\"hello from the working copy\");\n", + DiffHunkStatus::modified(), )], ); }); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 1aed77e343..c1a22b2c8a 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1246,8 +1246,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC diff.read_with(cx, |diff, cx| { assert_eq!(diff.base_text_string().unwrap(), text_1); assert_eq!( - diff.unstaged_diff - .as_ref() + diff.secondary_diff() .unwrap() .read(cx) .base_text_string() @@ -1266,8 +1265,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC diff.read_with(cx, |diff, cx| { assert_eq!(diff.base_text_string().unwrap(), text_1); assert_eq!( - diff.unstaged_diff - .as_ref() + diff.secondary_diff() .unwrap() .read(cx) .base_text_string() @@ -1286,8 +1284,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC diff.read_with(cx, |diff, cx| { assert_eq!(diff.base_text_string().unwrap(), text_2); assert_eq!( - diff.unstaged_diff - .as_ref() + diff.secondary_diff() .unwrap() .read(cx) .base_text_string()