From 7a469725d370a067ffdbbb4e1b0de13029e3ba76 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 9 Jun 2026 16:13:13 -0400 Subject: [PATCH] Refactor `BufferDiff` to allow multiple diffs to share the same base text buffer (#58266) This PR changes how base texts are managed by the `buffer_diff` crate, to enable keeping two diff entities alive that share the same base text buffer entity. Previously, each diff owned its own base text buffer and edited it when calling `BufferDiff::set_snapshot`, so the only way to reuse the same base text between two diffs was to have two independent buffers for it, which is pretty inefficient. After this PR, each diff still has a base text buffer, but `set_snapshot` doesn't edit it. Instead, that responsibility moves into the caller. For updating the base text buffer, this PR also introduces a new pair of APIs, `Buffer::snapshot_with_edits` and `Buffer::fast_forward`, which allow us to move the parsing of the new base text into the background and then install the new syntax tree synchronously on the foreground. The git store uses the low-level APIs `set_snapshot` and `fast_forward` directly, and manages the head text and index text buffers itself (garbage-collecting them when they're no longer needed); this enables adding an `open_staged_diff` API which returns a diff between the managed index buffer and the managed head buffer (the latter is also used for the uncommitted diff's base text). Other downstreams don't need to reuse a base text buffer, and those have been migrated to use the high-level `set_base_text` API, which now calls `set_snapshot` and `fast_forward` internally, with a guard to prevent concurrent updates. Another change worthy of note is that we now always diff the old base text with the new base text to create `snapshot_with_edits`. There are also some incidental bug fixes: - Fixed an issue where a dangling weak unstaged diff could stick around in the git store forever - Restored the `IndexMatchesHead` optimization that had become inoperative in the remote case - Fixed a crash in the multibuffer due to the handling of `BufferDiffEvent::LanguageChanged`, which could cause the multibuffer to have transforms that were inconsistent with the diff base text. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes ZED-81P Release Notes: - Fixed a rare crash that could occur while using the uncommitted diff. --------- Co-authored-by: Ben Kunkle Co-authored-by: Anthony Eid --- Cargo.lock | 1 - crates/acp_thread/src/diff.rs | 64 +- crates/action_log/src/action_log.rs | 27 +- crates/agent_ui/src/agent_diff.rs | 2 + crates/agent_ui/src/entry_view_state.rs | 1 + crates/buffer_diff/Cargo.toml | 1 - crates/buffer_diff/src/buffer_diff.rs | 807 +++++++++--------- .../tests/integration/integration_tests.rs | 101 +++ .../src/rate_prediction_modal.rs | 63 +- crates/editor/src/editor.rs | 5 + crates/editor/src/editor_tests.rs | 89 -- crates/editor/src/element.rs | 12 +- crates/editor/src/git.rs | 13 + crates/editor/src/split.rs | 84 +- crates/git_ui/src/commit_view.rs | 31 +- crates/git_ui/src/file_diff_view.rs | 44 +- crates/git_ui/src/multi_diff_view.rs | 32 +- crates/git_ui/src/project_diff.rs | 1 - crates/git_ui/src/text_diff_view.rs | 39 +- crates/language/src/buffer.rs | 70 ++ crates/multi_buffer/src/multi_buffer.rs | 50 +- crates/multi_buffer/src/multi_buffer_tests.rs | 65 +- crates/project/src/git_store.rs | 573 ++++++++++--- crates/project/src/project.rs | 13 + .../tests/integration/project_tests.rs | 435 +++++++++- crates/text/src/text.rs | 449 +++++----- 26 files changed, 1948 insertions(+), 1124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 829bb12da6..ab2c986115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2440,7 +2440,6 @@ version = "0.1.0" dependencies = [ "clock", "ctor", - "futures 0.3.32", "gpui", "imara-diff", "language", diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index a6d3b86db7..d297b5fa98 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -24,6 +24,7 @@ impl Diff { ) -> Self { let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); + let base_text_exists = old_text.is_some(); let base_text = old_text.clone().unwrap_or(String::new()).into(); let task = cx.spawn({ let multibuffer = multibuffer.clone(); @@ -40,8 +41,8 @@ impl Diff { let diff = build_buffer_diff( old_text.unwrap_or("".into()).into(), + base_text_exists, &buffer, - Some(language_registry.clone()), cx, ) .await?; @@ -88,16 +89,7 @@ impl Diff { let language = buffer.read(cx).language().cloned(); let language_registry = buffer.read(cx).language_registry(); let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx); - diff.language_changed(language.clone(), language_registry.clone(), cx); - let secondary_diff = cx.new(|cx| { - // For the secondary diff buffer we skip assigning the language as we do not really need to perform any syntax highlighting on - // it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff - // view multibuffers. - BufferDiff::new_unchanged(&buffer_text_snapshot, cx) - }); - diff.set_secondary_diff(secondary_diff); - diff + BufferDiff::new_unchanged(&buffer_text_snapshot, language, language_registry, cx) }); let multibuffer = cx.new(|cx| { @@ -233,28 +225,20 @@ impl PendingDiff { let base_text = self.base_text.clone(); self.update_diff = cx.spawn(async move |diff, cx| { let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); - let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned()); + let base_text_snapshot = buffer_diff.read_with(cx, |diff, cx| diff.base_text(cx)); let update = buffer_diff .update(cx, |diff, cx| { diff.update_diff( text_snapshot.clone(), + &base_text_snapshot, Some(base_text.clone()), - None, - language, cx, ) }) .await; - let (task1, task2) = buffer_diff.update(cx, |diff, cx| { - let task1 = diff.set_snapshot(update.clone(), &text_snapshot, cx); - let task2 = diff - .secondary_diff() - .unwrap() - .update(cx, |diff, cx| diff.set_snapshot(update, &text_snapshot, cx)); - (task1, task2) + buffer_diff.update(cx, |diff, cx| { + diff.set_snapshot(update.clone(), cx); }); - task1.await; - task2.await; diff.update(cx, |diff, cx| { if let Diff::Pending(diff) = diff { diff.update_visible_ranges(cx); @@ -272,7 +256,6 @@ impl PendingDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); let new_buffer = self.new_buffer.read(cx); - let language_registry = new_buffer.language_registry(); let path = new_buffer .file() @@ -299,7 +282,7 @@ impl PendingDiff { let buffer = buffer.clone(); async move |_this, cx| { buffer.update(cx, |buffer, _| buffer.parsing_idle()).await; - build_buffer_diff(base_text, &buffer, language_registry, cx).await + build_buffer_diff(base_text, true, &buffer, cx).await } }); @@ -397,39 +380,18 @@ pub struct FinalizedDiff { async fn build_buffer_diff( old_text: Arc, + base_text_exists: bool, buffer: &Entity, - language_registry: Option>, cx: &mut AsyncApp, ) -> Result> { let language = cx.update(|cx| buffer.read(cx).language().cloned()); - let text_snapshot = cx.update(|cx| buffer.read(cx).text_snapshot()); + let language_registry = cx.update(|cx| buffer.read(cx).language_registry()); let buffer = cx.update(|cx| buffer.read(cx).snapshot()); + let base_text = base_text_exists.then(|| old_text); - let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx)); - - let update = secondary_diff - .update(cx, |secondary_diff, cx| { - secondary_diff.update_diff( - text_snapshot.clone(), - Some(old_text), - Some(false), - language.clone(), - cx, - ) - }) - .await; - - secondary_diff - .update(cx, |secondary_diff, cx| { - secondary_diff.set_snapshot(update.clone(), &buffer, cx) - }) - .await; - - let diff = cx.new(|cx| BufferDiff::new(&buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new(&buffer, language, language_registry, cx)); diff.update(cx, |diff, cx| { - diff.language_changed(language, language_registry, cx); - diff.set_secondary_diff(secondary_diff); - diff.set_snapshot(update.clone(), &buffer, cx) + diff.set_base_text(base_text, buffer.text, cx) }) .await; Ok(diff) diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 99cc0a2d79..f8b15f621e 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -159,11 +159,8 @@ impl ActionLog { let text_snapshot = buffer.read(cx).text_snapshot(); let language = buffer.read(cx).language().cloned(); let language_registry = buffer.read(cx).language_registry(); - let diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - diff.language_changed(language, language_registry, cx); - diff - }); + let diff = + cx.new(|cx| BufferDiff::new(&text_snapshot, language, language_registry, cx)); let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); let diff_base; let unreviewed_edits; @@ -465,29 +462,15 @@ impl ActionLog { new_diff_base: Rope, cx: &mut AsyncApp, ) -> Result<()> { - let (diff, language) = this.read_with(cx, |this, cx| { + let diff = this.read_with(cx, |this, _cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; - anyhow::Ok(( - tracked_buffer.diff.clone(), - buffer.read(cx).language().cloned(), - )) + anyhow::Ok(tracked_buffer.diff.clone()) })??; - let update = diff - .update(cx, |diff, cx| { - diff.update_diff( - buffer_snapshot.clone(), - Some(new_base_text), - Some(true), - language, - cx, - ) - }) - .await; diff.update(cx, |diff, cx| { - diff.set_snapshot(update.clone(), &buffer_snapshot, cx) + diff.set_base_text(Some(new_base_text), buffer_snapshot.clone(), cx) }) .await; let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx)); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 9a820db7b2..a4ae164c51 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -102,6 +102,7 @@ impl AgentDiffPane { ); diff_display_editor .set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx); + diff_display_editor.set_render_diff_hunks_as_unstaged(cx); diff_display_editor.update_editors(cx, |editor, _cx| { editor.register_addon(AgentDiffAddon); }); @@ -1572,6 +1573,7 @@ impl AgentDiff { diff_hunk_controls(&thread, workspace.clone()), cx, ); + editor.set_render_diff_hunks_as_unstaged(true, cx); editor.set_expand_all_diff_hunks(cx); editor.register_addon(EditorAgentDiffAddon); }); diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index 48747f9cb3..68627d1ec8 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -470,6 +470,7 @@ fn create_editor_diff( editor.set_show_code_actions(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_expand_all_diff_hunks(cx); + editor.set_render_diff_hunks_as_unstaged(true, cx); editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); editor }) diff --git a/crates/buffer_diff/Cargo.toml b/crates/buffer_diff/Cargo.toml index d574b0dd3c..4bcdd3422e 100644 --- a/crates/buffer_diff/Cargo.toml +++ b/crates/buffer_diff/Cargo.toml @@ -16,7 +16,6 @@ test-support = ["settings"] [dependencies] clock.workspace = true -futures.workspace = true imara-diff.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index cc89f8c314..c300ace11a 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1,14 +1,12 @@ -use futures::channel::oneshot; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task}; use imara_diff::{Algorithm, Sink, intern::InternedInput, sources::lines_with_terminator}; use language::{ - Capability, Diff, DiffOptions, Language, LanguageName, LanguageRegistry, + Capability, DiffOptions, Language, LanguageName, LanguageRegistry, language_settings::LanguageSettings, word_diff_ranges, }; use rope::Rope; use std::{ cmp::Ordering, - future::Future, iter, ops::{Range, RangeInclusive}, sync::Arc, @@ -17,26 +15,33 @@ use sum_tree::SumTree; use text::{ Anchor, Bias, BufferId, Edit, OffsetRangeExt, Patch, Point, ToOffset as _, ToPoint as _, }; -use util::ResultExt; +use util::{ResultExt, debug_panic}; pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { pub buffer_id: BufferId, - inner: BufferDiffInner>, + base_text_buffer: Entity, + diff_snapshot: Option, secondary_diff: Option>, + buffer_snapshot: text::BufferSnapshot, } #[derive(Clone)] pub struct BufferDiffSnapshot { - inner: BufferDiffInner, + hunks: SumTree, + pending_hunks: SumTree, + base_text: language::BufferSnapshot, + base_text_exists: bool, + buffer_snapshot: text::BufferSnapshot, secondary_diff: Option>, } impl std::fmt::Debug for BufferDiffSnapshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") - .field("inner", &self.inner) + .field("hunks", &self.hunks) + .field("remote_id", &self.base_text.remote_id()) .field("secondary_diff", &self.secondary_diff) .finish() } @@ -44,24 +49,20 @@ impl std::fmt::Debug for BufferDiffSnapshot { #[derive(Clone)] pub struct BufferDiffUpdate { - inner: BufferDiffInner>, - buffer_snapshot: text::BufferSnapshot, - base_text_edits: Option, - base_text_changed: bool, -} - -#[derive(Clone)] -struct BufferDiffInner { hunks: SumTree, - pending_hunks: SumTree, - base_text: BaseText, + base_text: language::BufferSnapshot, base_text_exists: bool, buffer_snapshot: text::BufferSnapshot, } -impl BufferDiffInner { - fn buffer_version(&self) -> &clock::Global { - self.buffer_snapshot.version() +impl BufferDiffUpdate { + pub fn set_base_text_snapshot( + &mut self, + base_text: language::BufferSnapshot, + base_text_exists: bool, + ) { + self.base_text = base_text; + self.base_text_exists = base_text_exists; } } @@ -228,15 +229,6 @@ impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for usize { } } -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) - .field("remote_id", &self.base_text.remote_id()) - .finish() - } -} - impl BufferDiffSnapshot { #[cfg(test)] fn new_sync( @@ -249,26 +241,28 @@ impl BufferDiffSnapshot { } pub fn buffer_id(&self) -> BufferId { - self.inner.buffer_snapshot.remote_id() + self.buffer_snapshot.remote_id() + } + + pub fn buffer_snapshot(&self) -> &text::BufferSnapshot { + &self.buffer_snapshot } pub fn is_empty(&self) -> bool { - self.inner.hunks.is_empty() + self.hunks.is_empty() } pub fn changed_row_counts(&self) -> (u32, u32) { - let summary = self.inner.hunks.summary(); + let summary = self.hunks.summary(); (summary.added_rows, summary.removed_rows) } pub fn base_text_string(&self) -> Option { - self.inner - .base_text_exists - .then(|| self.inner.base_text.text()) + self.base_text_exists.then(|| self.base_text.text()) } pub fn base_text_exists(&self) -> bool { - self.inner.base_text_exists + self.base_text_exists } pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> { @@ -276,11 +270,11 @@ impl BufferDiffSnapshot { } pub fn buffer_version(&self) -> &clock::Global { - self.inner.buffer_version() + self.buffer_snapshot.version() } fn original_buffer_snapshot(&self) -> &text::BufferSnapshot { - &self.inner.buffer_snapshot + &self.buffer_snapshot } #[ztracing::instrument(skip_all)] @@ -289,9 +283,15 @@ impl BufferDiffSnapshot { 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) + let unstaged_counterpart = self.secondary_diff.as_deref(); + let range = range.to_offset(buffer); + let filter = move |summary: &DiffHunkSummary| { + let summary_range = summary.buffer_range.to_offset(buffer); + let before_start = summary_range.end < range.start; + let after_end = summary_range.start > range.end; + !before_start && !after_end + }; + self.hunks_intersecting_range_impl(filter, buffer, unstaged_counterpart) } pub fn hunks_intersecting_range_rev<'a>( @@ -304,7 +304,7 @@ impl BufferDiffSnapshot { let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); !before_start && !after_end }; - self.inner.hunks_intersecting_range_rev_impl(filter, buffer) + self.hunks_intersecting_range_rev_impl(filter, buffer) } pub fn hunks_intersecting_base_text_range<'a>( @@ -312,14 +312,13 @@ impl BufferDiffSnapshot { range: Range, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator { - let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); + let unstaged_counterpart = self.secondary_diff.as_deref(); let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end < range.start; let after_end = summary.diff_base_byte_range.start > range.end; !before_start && !after_end }; - self.inner - .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart) + self.hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart) } pub fn hunks_intersecting_base_text_range_rev<'a>( @@ -332,8 +331,7 @@ impl BufferDiffSnapshot { let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt(); !before_start && !after_end }; - self.inner - .hunks_intersecting_range_rev_impl(filter, main_buffer) + self.hunks_intersecting_range_rev_impl(filter, main_buffer) } pub fn hunks<'a>( @@ -374,7 +372,7 @@ impl BufferDiffSnapshot { } pub fn base_text(&self) -> &language::BufferSnapshot { - &self.inner.base_text + &self.base_text } /// If this function returns `true`, the base texts are equal. If this @@ -382,11 +380,11 @@ impl BufferDiffSnapshot { /// result is used to avoid recalculating diffs in situations where we know /// nothing has changed. pub fn base_texts_definitely_eq(&self, other: &Self) -> bool { - if self.inner.base_text_exists != other.inner.base_text_exists { + if self.base_text_exists != other.base_text_exists { return false; } - let left = &self.inner.base_text; - let right = &other.inner.base_text; + let left = &self.base_text; + let right = &other.base_text; let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty()); let (new_id, new_version, new_empty) = (right.remote_id(), right.version(), right.is_empty()); @@ -438,7 +436,7 @@ impl BufferDiffSnapshot { range: RangeInclusive, buffer: &'a text::BufferSnapshot, ) -> Patch { - if !self.inner.base_text_exists { + if !self.base_text_exists { return Patch::new(vec![Edit { old: Point::zero()..buffer.max_point(), new: Point::zero()..Point::zero(), @@ -447,7 +445,7 @@ impl BufferDiffSnapshot { let mut edits_since_diff = Patch::new( buffer - .edits_since::(&self.inner.buffer_snapshot.version) + .edits_since::(&self.buffer_snapshot.version) .collect::>(), ); edits_since_diff.invert(); @@ -460,7 +458,7 @@ impl BufferDiffSnapshot { let original_snapshot = self.original_buffer_snapshot(); let base_text = self.base_text(); - let mut cursor = self.inner.hunks.cursor(original_snapshot); + let mut cursor = self.hunks.cursor(original_snapshot); self.hunk_before_buffer_anchor( original_snapshot.anchor_before(start_point), &mut cursor, @@ -519,8 +517,7 @@ impl BufferDiffSnapshot { inverted_edits_since.invert(); inverted_edits_since.compose( - self.inner - .hunks + self.hunks .iter() .map(|hunk| { let old_start = hunk.buffer_range.start.to_point(original_snapshot); @@ -536,16 +533,14 @@ impl BufferDiffSnapshot { new: new_start..new_end, } }) - .chain( - if !self.inner.base_text_exists && self.inner.hunks.is_empty() { - Some(Edit { - old: Point::zero()..original_snapshot.max_point(), - new: Point::zero()..Point::zero(), - }) - } else { - None - }, - ), + .chain(if !self.base_text_exists && self.hunks.is_empty() { + Some(Edit { + old: Point::zero()..original_snapshot.max_point(), + new: Point::zero()..Point::zero(), + }) + } else { + None + }), ) } @@ -558,7 +553,7 @@ impl BufferDiffSnapshot { range: RangeInclusive, buffer: &'a text::BufferSnapshot, ) -> Patch { - if !self.inner.base_text_exists { + if !self.base_text_exists { return Patch::new(vec![Edit { old: Point::zero()..Point::zero(), new: Point::zero()..buffer.max_point(), @@ -566,11 +561,11 @@ impl BufferDiffSnapshot { } let edits_since_diff = buffer - .edits_since::(&self.inner.buffer_snapshot.version) + .edits_since::(&self.buffer_snapshot.version) .collect::>(); let mut hunk_patch = Vec::new(); - let mut cursor = self.inner.hunks.cursor(self.original_buffer_snapshot()); + let mut cursor = self.hunks.cursor(self.original_buffer_snapshot()); let hunk_before = self .hunk_before_base_text_offset(range.start().to_offset(self.base_text()), &mut cursor); @@ -635,7 +630,7 @@ impl BufferDiffSnapshot { let original_snapshot = self.original_buffer_snapshot(); let mut hunk_edits: Vec> = Vec::new(); - for hunk in self.inner.hunks.iter() { + for hunk in self.hunks.iter() { let old_start = self .base_text() .offset_to_point(hunk.diff_base_byte_range.start); @@ -649,7 +644,7 @@ impl BufferDiffSnapshot { new: new_start..new_end, }); } - if !self.inner.base_text_exists && hunk_edits.is_empty() { + if !self.base_text_exists && hunk_edits.is_empty() { hunk_edits.push(Edit { old: Point::zero()..Point::zero(), new: Point::zero()..original_snapshot.max_point(), @@ -709,8 +704,7 @@ impl BufferDiffSnapshot { } } -impl BufferDiffInner> { - /// Returns the new index text and new pending hunks. +impl BufferDiffSnapshot { fn stage_or_unstage_hunks_impl( &mut self, unstaged_diff: &Self, @@ -718,14 +712,13 @@ impl BufferDiffInner> { hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, - cx: &mut Context, ) -> Option { let head_text = self .base_text_exists - .then(|| self.base_text.read(cx).as_rope().clone()); + .then(|| self.base_text.as_rope().clone()); let index_text = unstaged_diff .base_text_exists - .then(|| unstaged_diff.base_text.read(cx).as_rope().clone()); + .then(|| unstaged_diff.base_text.as_rope().clone()); // If the file doesn't exist in either HEAD or the index, then the // entire file must be either created or deleted in the index. @@ -932,23 +925,7 @@ impl BufferDiffInner> { } } -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); - let filter = move |summary: &DiffHunkSummary| { - let summary_range = summary.buffer_range.to_offset(buffer); - let before_start = summary_range.end < range.start; - let after_end = summary_range.start > range.end; - !before_start && !after_end - }; - self.hunks_intersecting_range_impl(filter, buffer, secondary) - } - +impl BufferDiffSnapshot { fn hunks_intersecting_range_impl<'a>( &'a self, filter: impl 'a + Fn(&DiffHunkSummary) -> bool, @@ -1473,6 +1450,7 @@ fn compare_hunks( changed_range, base_text_changed_range, extended_range, + base_text_changed: false, } } @@ -1489,61 +1467,91 @@ pub struct DiffChanged { pub changed_range: Option>, pub base_text_changed_range: Option>, pub extended_range: Option>, + pub base_text_changed: bool, } #[derive(Clone, Debug)] pub enum BufferDiffEvent { BaseTextChanged, DiffChanged(DiffChanged), - LanguageChanged, HunksStagedOrUnstaged(Option), } -struct SetSnapshotResult { - change: DiffChanged, - base_text_changed: bool, -} - impl EventEmitter for BufferDiff {} impl BufferDiff { - pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self { + pub fn new( + buffer: &text::BufferSnapshot, + language: Option>, + language_registry: Option>, + cx: &mut App, + ) -> Self { let base_text = cx.new(|cx| { - let mut buffer = language::Buffer::local("", cx); - buffer.set_capability(Capability::ReadOnly, cx); - buffer + let mut base_buffer = language::Buffer::local("", cx); + base_buffer.set_capability(Capability::ReadOnly, cx); + if let Some(language_registry) = language_registry { + base_buffer.set_language_registry(language_registry); + } + base_buffer.set_language_async(language, cx); + base_buffer }); BufferDiff { buffer_id: buffer.remote_id(), - inner: BufferDiffInner { - base_text, - hunks: SumTree::new(buffer), - pending_hunks: SumTree::new(buffer), - base_text_exists: false, - buffer_snapshot: buffer.clone(), - }, + base_text_buffer: base_text, + diff_snapshot: None, + buffer_snapshot: buffer.clone(), secondary_diff: None, } } - pub fn new_unchanged(buffer: &text::BufferSnapshot, cx: &mut Context) -> Self { + pub fn new_with_base_text_buffer( + buffer: &text::BufferSnapshot, + base_text_buffer: Entity, + _cx: &mut App, + ) -> Self { + BufferDiff { + buffer_id: buffer.remote_id(), + base_text_buffer, + diff_snapshot: None, + buffer_snapshot: buffer.clone(), + secondary_diff: None, + } + } + + pub fn new_unchanged( + buffer: &text::BufferSnapshot, + language: Option>, + language_registry: Option>, + cx: &mut Context, + ) -> Self { let base_text = buffer.text(); let base_text = cx.new(|cx| { - let mut buffer = language::Buffer::local(base_text, cx); - buffer.set_capability(Capability::ReadOnly, cx); - buffer + let mut base_buffer = language::Buffer::local(base_text, cx); + base_buffer.set_capability(Capability::ReadOnly, cx); + if let Some(language_registry) = language_registry { + base_buffer.set_language_registry(language_registry); + } + base_buffer.set_language_async(language, cx); + base_buffer }); + let base_text_snapshot = base_text.read(cx).snapshot(); + + let diff_snapshot = BufferDiffSnapshot { + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text: base_text_snapshot, + base_text_exists: true, + buffer_snapshot: buffer.clone(), + secondary_diff: None, + }; + BufferDiff { buffer_id: buffer.remote_id(), - inner: BufferDiffInner { - base_text, - hunks: SumTree::new(buffer), - pending_hunks: SumTree::new(buffer), - base_text_exists: true, - buffer_snapshot: buffer.clone(), - }, + base_text_buffer: base_text, + diff_snapshot: Some(diff_snapshot), + buffer_snapshot: buffer.clone(), secondary_diff: None, } } @@ -1554,17 +1562,23 @@ impl BufferDiff { buffer: &text::BufferSnapshot, cx: &mut Context, ) -> Self { - let mut this = BufferDiff::new(&buffer, cx); + let mut this = BufferDiff::new(buffer, None, None, cx); let mut base_text = base_text.to_owned(); text::LineEnding::normalize(&mut base_text); - let inner = cx.foreground_executor().block_on(this.update_diff( + let base_text_buffer = cx.new(|cx| { + let mut buffer = language::Buffer::local(base_text, cx); + buffer.set_capability(Capability::ReadOnly, cx); + buffer + }); + let base_text = base_text_buffer.read(cx).snapshot(); + this.base_text_buffer = base_text_buffer; + let update = cx.foreground_executor().block_on(this.update_diff( buffer.clone(), - Some(Arc::from(base_text)), - Some(false), - None, + &base_text, + Some(Arc::from(base_text.text())), cx, )); - this.set_snapshot(inner, &buffer, cx).detach(); + this.set_snapshot(update, cx); this } @@ -1577,8 +1591,11 @@ impl BufferDiff { } pub fn clear_pending_hunks(&mut self, cx: &mut Context) { + let Some(diff_snapshot) = &mut self.diff_snapshot else { + return; + }; if self.secondary_diff.is_some() { - self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary { + diff_snapshot.pending_hunks = SumTree::from_summary(DiffHunkSummary { buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id), diff_base_byte_range: 0..0, added_rows: 0, @@ -1590,6 +1607,7 @@ impl BufferDiff { changed_range: changed_range.clone(), base_text_changed_range: base_text_range, extended_range: changed_range, + base_text_changed: false, })); } } @@ -1602,19 +1620,18 @@ impl BufferDiff { file_exists: bool, cx: &mut Context, ) -> Option { - let new_index_text = self - .secondary_diff - .as_ref()? - .update(cx, |secondary_diff, cx| { - self.inner.stage_or_unstage_hunks_impl( - &secondary_diff.inner, - stage, - hunks, - buffer, - file_exists, - cx, - ) - }); + let secondary_diff = self.secondary_diff.clone()?; + let diff_snapshot = self.diff_snapshot.as_mut()?; + let unstaged_diff_snapshot = secondary_diff.read_with(cx, |secondary_diff, _cx| { + secondary_diff.diff_snapshot.clone() + })?; + let new_index_text = diff_snapshot.stage_or_unstage_hunks_impl( + &unstaged_diff_snapshot, + stage, + hunks, + buffer, + file_exists, + ); cx.emit(BufferDiffEvent::HunksStagedOrUnstaged( new_index_text.clone(), @@ -1627,6 +1644,7 @@ impl BufferDiff { changed_range: changed_range.clone(), base_text_changed_range, extended_range: changed_range, + base_text_changed: false, })); } new_index_text @@ -1643,12 +1661,23 @@ impl BufferDiff { .snapshot(cx) .hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer) .collect::>(); + let Some(diff_snapshot) = &mut self.diff_snapshot else { + return; + }; let Some(secondary) = self.secondary_diff.clone() else { return; }; - let secondary = secondary.read(cx).inner.clone(); - self.inner - .stage_or_unstage_hunks_impl(&secondary, stage, &hunks, buffer, file_exists, cx); + let secondary = secondary.read(cx); + let Some(secondary_snapshot) = &secondary.diff_snapshot else { + return; + }; + diff_snapshot.stage_or_unstage_hunks_impl( + &secondary_snapshot, + stage, + &hunks, + buffer, + file_exists, + ); if let Some((first, last)) = hunks.first().zip(hunks.last()) { let changed_range = Some(first.buffer_range.start..last.buffer_range.end); let base_text_changed_range = @@ -1657,6 +1686,7 @@ impl BufferDiff { changed_range: changed_range.clone(), base_text_changed_range, extended_range: changed_range, + base_text_changed: false, })); } } @@ -1664,172 +1694,139 @@ impl BufferDiff { pub fn update_diff( &self, buffer: text::BufferSnapshot, + base_text_snapshot: &language::BufferSnapshot, base_text: Option>, - base_text_change: Option, - language: Option>, cx: &App, ) -> Task { let base_text = base_text.map(|t| text::LineEnding::normalize_arc(t)); - let prev_base_text = self.base_text(cx).as_rope().clone(); - let base_text_changed = base_text_change.is_some(); - let compute_base_text_edits = base_text_change == Some(true); + debug_assert_eq!( + base_text.as_deref().unwrap_or_default(), + &base_text_snapshot.text() + ); + debug_assert_eq!( + base_text_snapshot.remote_id(), + self.base_text_buffer.read(cx).remote_id() + ); + + let language = base_text_snapshot.language(); let diff_options = build_diff_options( - language.as_ref().map(|l| l.name()), - language.as_ref().map(|l| l.default_scope()), + language.map(|l| l.name()), + language.map(|l| l.default_scope()), cx, ); let buffer_snapshot = buffer.clone(); - - let base_text_diff_task = if base_text_changed && compute_base_text_edits { - base_text - .as_ref() - .map(|new_text| self.inner.base_text.read(cx).diff(new_text.clone(), cx)) - } else { - None - }; - - let hunk_task = cx.background_executor().spawn({ - let buffer_snapshot = buffer_snapshot.clone(); - async move { - let base_text_rope = if let Some(base_text) = &base_text { - if base_text_changed { - Rope::from(base_text.as_ref()) - } else { - prev_base_text - } - } else { - Rope::new() - }; - let base_text_exists = base_text.is_some(); - let hunks = compute_hunks( - base_text - .clone() - .map(|base_text| (base_text, base_text_rope.clone())), - &buffer, - diff_options, - ); - let base_text = base_text.unwrap_or_default(); - BufferDiffInner { - base_text, - hunks, - base_text_exists, - pending_hunks: SumTree::new(&buffer), - buffer_snapshot, - } + let base_text_snapshot = base_text_snapshot.clone(); + let base_text_exists = base_text.is_some(); + let unchanged_hunks = self.diff_snapshot.as_ref().and_then(|diff_snapshot| { + if diff_snapshot.base_text_exists == base_text_exists + && diff_snapshot.base_text.version() == base_text_snapshot.version() + && diff_snapshot.buffer_snapshot.version() == buffer_snapshot.version() + { + Some(diff_snapshot.hunks.clone()) + } else { + None } }); cx.background_executor().spawn(async move { - let (inner, base_text_edits) = match base_text_diff_task { - Some(diff_task) => { - let (inner, diff) = futures::join!(hunk_task, diff_task); - (inner, Some(diff)) - } - None => (hunk_task.await, None), + let hunks = if let Some(unchanged_hunks) = unchanged_hunks { + unchanged_hunks + } else if let Some(base_text) = base_text { + compute_hunks( + Some((base_text, base_text_snapshot.as_rope().clone())), + &buffer, + diff_options, + ) + } else { + compute_hunks(None, &buffer, diff_options) }; BufferDiffUpdate { - inner, + hunks, + base_text: base_text_snapshot, + base_text_exists, buffer_snapshot, - base_text_edits, - base_text_changed, } }) } - #[ztracing::instrument(skip_all)] - pub fn language_changed( - &mut self, - language: Option>, - language_registry: Option>, - cx: &mut Context, - ) { - let fut = self.inner.base_text.update(cx, |base_text, cx| { - if let Some(language_registry) = language_registry { - base_text.set_language_registry(language_registry); - } - base_text.set_language_async(language, cx); - base_text.parsing_idle() - }); - cx.spawn(async move |this, cx| { - fut.await; - this.update(cx, |_, cx| { - cx.emit(BufferDiffEvent::LanguageChanged); - }) - .ok(); - }) - .detach(); - } - - fn set_snapshot_with_secondary_inner( + pub fn set_snapshot_with_secondary( &mut self, update: BufferDiffUpdate, - buffer: &text::BufferSnapshot, secondary_diff_change: Option>, clear_pending_hunks: bool, cx: &mut Context, - ) -> impl Future + use<> { + ) -> Option> { log::debug!("set snapshot with secondary {secondary_diff_change:?}"); - let old_snapshot = self.snapshot(cx); - let new_state = update.inner; - let base_text_changed = update.base_text_changed; - - let state = &mut self.inner; - state.base_text_exists = new_state.base_text_exists; - let should_compare_hunks = update.base_text_edits.is_some() || !base_text_changed; - let parsing_idle = if let Some(diff) = update.base_text_edits { - state.base_text.update(cx, |base_text, cx| { - base_text.set_sync_parse_timeout(None); - base_text.set_capability(Capability::ReadWrite, cx); - base_text.apply_diff(diff, cx); - base_text.set_capability(Capability::ReadOnly, cx); - Some(base_text.parsing_idle()) - }) - } else if update.base_text_changed { - state.base_text.update(cx, |base_text, cx| { - base_text.set_sync_parse_timeout(None); - base_text.set_capability(Capability::ReadWrite, cx); - base_text.set_text(new_state.base_text.clone(), cx); - base_text.set_capability(Capability::ReadOnly, cx); - Some(base_text.parsing_idle()) - }) - } else { - None + let BufferDiffUpdate { + hunks: new_hunks, + base_text: new_base_text, + base_text_exists: new_base_text_exists, + buffer_snapshot: new_buffer_snapshot, + } = update; + let buffer = &new_buffer_snapshot; + let old_snapshot = self + .diff_snapshot + .clone() + .unwrap_or_else(|| BufferDiffSnapshot { + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text: new_base_text.clone(), + base_text_exists: false, + buffer_snapshot: new_buffer_snapshot.clone(), + secondary_diff: None, + }); + let mut new_snapshot = BufferDiffSnapshot { + hunks: new_hunks.clone(), + base_text: new_base_text.clone(), + base_text_exists: new_base_text_exists, + buffer_snapshot: new_buffer_snapshot.clone(), + pending_hunks: old_snapshot.pending_hunks.clone(), + secondary_diff: None, }; - let old_buffer_snapshot = &old_snapshot.inner.buffer_snapshot; - let old_base_snapshot = &old_snapshot.inner.base_text; - let new_base_snapshot = state.base_text.read(cx).snapshot(); + let old_base_text_exists = old_snapshot.base_text_exists; + let old_buffer_snapshot = &old_snapshot.buffer_snapshot; + let old_base_text = &old_snapshot.base_text; + let base_text_changed = old_base_text_exists != new_base_text_exists + || (new_base_text_exists + && (old_base_text.remote_id() != new_base_text.remote_id() + || new_base_text + .version() + .changed_since(old_base_text.version()))); let DiffChanged { mut changed_range, mut base_text_changed_range, mut extended_range, - } = match (state.base_text_exists, new_state.base_text_exists) { - (false, false) => DiffChanged::default(), - (true, true) if should_compare_hunks => compare_hunks( - &new_state.hunks, - &old_snapshot.inner.hunks, + base_text_changed: _, + } = match (old_base_text_exists, new_base_text_exists) { + (false, false) if self.diff_snapshot.is_some() => DiffChanged::default(), + (true, true) => compare_hunks( + &new_hunks, + &old_snapshot.hunks, old_buffer_snapshot, buffer, - old_base_snapshot, - &new_base_snapshot, + old_base_text, + &new_base_text, ), _ => { let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id); - let full_base_range = 0..new_state.base_text.len(); + let full_base_range = 0..new_base_text.len(); DiffChanged { changed_range: Some(full_range.clone()), base_text_changed_range: Some(full_base_range), extended_range: Some(full_range), + base_text_changed: false, } } }; - state.hunks = new_state.hunks; - state.buffer_snapshot = update.buffer_snapshot; if base_text_changed || clear_pending_hunks { - if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last()) + if let Some((first, last)) = old_snapshot + .pending_hunks + .first() + .zip(old_snapshot.pending_hunks.last()) { let pending_range = first.buffer_range.start..last.buffer_range.end; if let Some(range) = &mut changed_range { @@ -1855,7 +1852,7 @@ impl BufferDiff { extended_range = Some(pending_range); } } - state.pending_hunks = SumTree::new(buffer); + new_snapshot.pending_hunks = SumTree::new(buffer); } if let Some(secondary_changed_range) = secondary_diff_change @@ -1884,140 +1881,164 @@ impl BufferDiff { } } - async move { - if let Some(parsing_idle) = parsing_idle { - parsing_idle.await; - } - SetSnapshotResult { - change: DiffChanged { - changed_range, - base_text_changed_range, - extended_range, - }, - base_text_changed, - } + self.diff_snapshot = Some(new_snapshot); + self.buffer_snapshot = new_buffer_snapshot; + + let result = DiffChanged { + changed_range, + base_text_changed_range, + extended_range, + base_text_changed, + }; + if result.base_text_changed { + cx.emit(BufferDiffEvent::BaseTextChanged); } + let changed_range = result.changed_range.clone(); + cx.emit(BufferDiffEvent::DiffChanged(result)); + changed_range } pub fn set_snapshot( &mut self, new_state: BufferDiffUpdate, - buffer: &text::BufferSnapshot, cx: &mut Context, - ) -> Task>> { - self.set_snapshot_with_secondary(new_state, buffer, None, false, cx) - } - - pub fn set_snapshot_with_secondary( - &mut self, - update: BufferDiffUpdate, - buffer: &text::BufferSnapshot, - secondary_diff_change: Option>, - clear_pending_hunks: bool, - cx: &mut Context, - ) -> Task>> { - let fut = self.set_snapshot_with_secondary_inner( - update, - buffer, - secondary_diff_change, - clear_pending_hunks, - cx, - ); - - cx.spawn(async move |this, cx| { - let result = fut.await; - this.update(cx, |_, cx| { - if result.base_text_changed { - cx.emit(BufferDiffEvent::BaseTextChanged); - } - cx.emit(BufferDiffEvent::DiffChanged(result.change.clone())); - }) - .ok(); - result.change.changed_range - }) + ) -> Option> { + self.set_snapshot_with_secondary(new_state, None, false, cx) } pub fn base_text(&self, cx: &App) -> language::BufferSnapshot { - self.inner.base_text.read(cx).snapshot() + self.base_text_buffer.read(cx).snapshot() } pub fn base_text_exists(&self) -> bool { - self.inner.base_text_exists + self.diff_snapshot + .as_ref() + .is_some_and(|diff_snapshot| diff_snapshot.base_text_exists) } pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot { - BufferDiffSnapshot { - inner: BufferDiffInner { - hunks: self.inner.hunks.clone(), - pending_hunks: self.inner.pending_hunks.clone(), - base_text: self.inner.base_text.read(cx).snapshot(), - base_text_exists: self.inner.base_text_exists, - buffer_snapshot: self.inner.buffer_snapshot.clone(), - }, - secondary_diff: self.secondary_diff.as_ref().map(|diff| { - debug_assert!(diff.read(cx).secondary_diff.is_none()); - Arc::new(diff.read(cx).snapshot(cx)) - }), - } + let mut snapshot = self.diff_snapshot.clone().unwrap_or_else(|| { + let base_text = self.base_text_buffer.read(cx).snapshot(); + BufferDiffSnapshot { + hunks: SumTree::new(&self.buffer_snapshot), + pending_hunks: SumTree::new(&self.buffer_snapshot), + base_text, + base_text_exists: false, + buffer_snapshot: self.buffer_snapshot.clone(), + secondary_diff: None, + } + }); + snapshot.secondary_diff = self.secondary_diff.as_ref().map(|diff| { + debug_assert!(diff.read(cx).secondary_diff.is_none()); + Arc::new(diff.read(cx).snapshot(cx)) + }); + snapshot } /// Used in cases where the change set isn't derived from git. + /// + /// Dropping the returned task cancels the update, leaving the diff + /// unchanged. Calls must not overlap; to re-run this when the buffer or + /// base text changes, store the task somewhere that the next call will + /// overwrite, so that the previous call is cancelled. pub fn set_base_text( &mut self, base_text: Option>, - language: Option>, buffer: text::BufferSnapshot, cx: &mut Context, - ) -> oneshot::Receiver<()> { - let (tx, rx) = oneshot::channel(); - let complete_on_drop = util::defer(|| { - tx.send(()).ok(); - }); + ) -> Task<()> { cx.spawn(async move |this, cx| { + let base_text_exists = base_text.is_some(); + let base_text = base_text.unwrap_or_default(); + let Some(base_text_diff) = this + .update(cx, |this, cx| { + this.base_text_buffer.update(cx, |base_text_buffer, cx| { + base_text_buffer.diff(base_text.clone(), cx) + }) + }) + .log_err() + else { + return; + }; + let base_text_diff = base_text_diff.await; + let Some(edited_base_text) = this + .update(cx, |this, cx| { + if this.base_text_buffer.read(cx).version() != base_text_diff.base_version { + log::warn!("dropping concurrent diff update"); + debug_panic!("incorrect concurrent call to set_base_text"); + return None; + } + let edited_base_text = + this.base_text_buffer.update(cx, |base_text_buffer, cx| { + base_text_buffer.set_line_ending(base_text_diff.line_ending, cx); + assert!(base_text_buffer.version() == base_text_diff.base_version); + base_text_buffer.snapshot_with_edits(base_text_diff.edits, cx) + }); + Some(edited_base_text) + }) + .log_err() + .flatten() + else { + return; + }; + let edited_base_text = edited_base_text.await; + let base_text_snapshot = edited_base_text.snapshot().clone(); let Some(state) = this .update(cx, |this, cx| { - this.update_diff(buffer.clone(), base_text, Some(false), language, cx) + this.update_diff( + buffer.clone(), + &base_text_snapshot, + base_text_exists.then(|| base_text.clone()), + cx, + ) }) .log_err() else { return; }; let state = state.await; - if let Some(task) = this - .update(cx, |this, cx| this.set_snapshot(state, &buffer, cx)) - .log_err() - { - task.await; - } - drop(complete_on_drop) + this.update(cx, |this, cx| { + if &this.base_text_buffer.read(cx).version() != edited_base_text.base_version() { + log::warn!("dropping concurrent diff update"); + debug_panic!("incorrect concurrent call to set_base_text"); + return; + } + + this.base_text_buffer.update(cx, |base_text_buffer, cx| { + base_text_buffer.fast_forward(edited_base_text, cx) + }); + this.set_snapshot(state, cx); + }) + .log_err(); }) - .detach(); - rx } - pub fn base_text_string(&self, cx: &App) -> Option { - self.inner - .base_text_exists - .then(|| self.inner.base_text.read(cx).text()) + pub fn base_text_string(&self, _cx: &App) -> Option { + self.diff_snapshot.as_ref().and_then(|diff_snapshot| { + if diff_snapshot.base_text_exists { + Some(diff_snapshot.base_text.text()) + } else { + None + } + }) } #[cfg(any(test, feature = "test-support"))] pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context) { - let language = self.base_text(cx).language().cloned(); - let base_text = self.base_text_string(cx).map(|s| s.as_str().into()); - let fut = self.update_diff(buffer.clone(), base_text, None, language, cx); + let base_text = self.base_text(cx); + let fut = self.update_diff( + buffer.clone(), + &base_text, + self.base_text_exists().then(|| Arc::from(base_text.text())), + cx, + ); let fg_executor = cx.foreground_executor().clone(); let snapshot = fg_executor.block_on(fut); - let fut = self.set_snapshot_with_secondary_inner(snapshot, buffer, None, false, cx); - let result = fg_executor.block_on(fut); - if result.base_text_changed { - cx.emit(BufferDiffEvent::BaseTextChanged); - } - cx.emit(BufferDiffEvent::DiffChanged(result.change)); + let _changed_range = self.set_snapshot(snapshot, cx); } pub fn base_text_buffer(&self) -> &Entity { - &self.inner.base_text + &self.base_text_buffer } } @@ -2216,7 +2237,7 @@ mod tests { ], ); - diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx)); + diff = cx.update(|cx| BufferDiff::new(&buffer, None, None, cx).snapshot(cx)); assert_hunks::<&str, _>( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), @@ -2846,15 +2867,16 @@ mod tests { let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1); - let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx)); + let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, None, None, cx).snapshot(cx)); let diff_1 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_1.inner.hunks, - &empty_diff.inner.hunks, + &diff_1.hunks, + &empty_diff.hunks, &buffer, &buffer, &diff_1.base_text(), @@ -2887,9 +2909,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_2.inner.hunks, - &diff_1.inner.hunks, + &diff_2.hunks, + &diff_1.hunks, &buffer, &buffer, diff_2.base_text(), @@ -2925,9 +2948,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_3.inner.hunks, - &diff_2.inner.hunks, + &diff_3.hunks, + &diff_2.hunks, &buffer, &buffer, diff_3.base_text(), @@ -2959,9 +2983,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_4.inner.hunks, - &diff_3.inner.hunks, + &diff_4.hunks, + &diff_3.hunks, &buffer, &buffer, diff_4.base_text(), @@ -2994,9 +3019,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_5.inner.hunks, - &diff_4.inner.hunks, + &diff_5.hunks, + &diff_4.hunks, &buffer, &buffer, diff_5.base_text(), @@ -3029,9 +3055,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_6.inner.hunks, - &diff_5.inner.hunks, + &diff_6.hunks, + &diff_5.hunks, &buffer, &buffer, diff_6.base_text(), @@ -3063,9 +3090,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_7.inner.hunks, - &diff_6.inner.hunks, + &diff_7.hunks, + &diff_6.hunks, &buffer, &buffer, diff_7.base_text(), @@ -3096,9 +3124,10 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( - &diff_8.inner.hunks, - &diff_7.inner.hunks, + &diff_8.hunks, + &diff_7.hunks, &buffer, &buffer, diff_8.base_text(), @@ -3322,19 +3351,18 @@ mod tests { ); buffer.text_snapshot() }); + let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx)); let update = diff .update(cx, |diff, cx| { diff.update_diff( snapshot.clone(), - Some(base_text.as_str().into()), - None, - None, + &base_text_snapshot, + Some(Arc::from(base_text_snapshot.text())), cx, ) }) .await; - diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx)) - .await; + diff.update(cx, |diff, cx| diff.set_snapshot(update, cx)); cx.run_until_parked(); drop(subscription); let events = rx.into_iter().collect::>(); @@ -3344,6 +3372,7 @@ mod tests { changed_range: _, base_text_changed_range, extended_range: _, + base_text_changed: _, }), ] => { // TODO(cole) this seems like it should pass but currently fails (see compare_hunks) @@ -3398,9 +3427,10 @@ mod tests { changed_range, base_text_changed_range: _, extended_range, + base_text_changed: _, } = compare_hunks( - &diff_b.inner.hunks, - &diff_a.inner.hunks, + &diff_b.hunks, + &diff_a.hunks, &old_buffer, &buffer, &diff_a.base_text(), @@ -3461,9 +3491,10 @@ mod tests { changed_range, base_text_changed_range: _, extended_range, + base_text_changed: _, } = compare_hunks( - &diff_2b.inner.hunks, - &diff_2a.inner.hunks, + &diff_2b.hunks, + &diff_2a.hunks, &old_buffer_2, &buffer_2, &diff_2a.base_text(), @@ -3531,6 +3562,7 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( &new_hunks_1, &old_hunks_1, @@ -3590,6 +3622,7 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( &new_hunks_2, &old_hunks_2, @@ -3658,6 +3691,7 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( &new_hunks_3, &old_hunks_3, @@ -3735,6 +3769,7 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, + base_text_changed: _, } = compare_hunks( &new_hunks_4, &old_hunks_4, @@ -3990,17 +4025,11 @@ mod tests { ); let buffer_snapshot = buffer.snapshot(); - let diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx)); + let diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, None, None, cx)); diff.update(cx, |diff, cx| { - diff.set_base_text( - Some(Arc::from(base_text_crlf)), - None, - buffer_snapshot.clone(), - cx, - ) + diff.set_base_text(Some(Arc::from(base_text_crlf)), buffer_snapshot.clone(), cx) }) - .await - .ok(); + .await; cx.run_until_parked(); let snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx)); diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index 7fc56a3c86..dac33f9855 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -2886,6 +2886,107 @@ async fn test_git_diff_base_change( }); } +#[gpui::test(iterations = 10)] +async fn test_git_diff_index_matches_head( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + let committed_text = " + one + two + three + " + .unindent(); + let file_contents = " + one + TWO + three + " + .unindent(); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + ".git": {}, + "a.txt": file_contents, + }), + ) + .await; + client_a + .fs() + .set_head_and_index_for_repo(Path::new("/dir/.git"), &[("a.txt", committed_text.clone())]); + + let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| { + call.share_project(project_local.clone(), cx) + }) + .await + .unwrap(); + let project_remote = client_b.join_remote_project(project_id, cx_b).await; + + // Open the uncommitted diff on the guest, without opening it on the host + // first, so that the host loads the diff bases in response to the guest's + // request. + let remote_buffer = project_remote + .update(cx_b, |p, cx| { + p.open_buffer((worktree_id, rel_path("a.txt")), cx) + }) + .await + .unwrap(); + let remote_uncommitted_diff = project_remote + .update(cx_b, |p, cx| { + p.open_uncommitted_diff(remote_buffer.clone(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + + // The guest's index and head texts share one allocation, which is only + // possible if the host detected that the index matches the head and sent + // `Mode::IndexMatchesHead`. + let buffer_id = remote_buffer.read_with(cx_b, |buffer, _| buffer.remote_id()); + project_remote.read_with(cx_b, |project, cx| { + assert!( + project + .git_store() + .read(cx) + .index_matches_head_for_buffer(buffer_id, cx), + "the host should send IndexMatchesHead when the index is clean" + ); + }); + + remote_uncommitted_diff.read_with(cx_b, |diff, cx| { + let buffer = remote_buffer.read(cx); + assert_eq!( + diff.base_text_string(cx).as_deref(), + Some(committed_text.as_str()) + ); + assert_hunks( + diff.snapshot(cx).hunks_in_row_range(0..3, buffer), + buffer, + &diff.base_text_string(cx).unwrap(), + &[( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk), + )], + ); + }); +} + #[gpui::test(iterations = 10)] async fn test_git_branch_name( executor: BackgroundExecutor, diff --git a/crates/edit_prediction_ui/src/rate_prediction_modal.rs b/crates/edit_prediction_ui/src/rate_prediction_modal.rs index 782fc93f99..deae41c21e 100644 --- a/crates/edit_prediction_ui/src/rate_prediction_modal.rs +++ b/crates/edit_prediction_ui/src/rate_prediction_modal.rs @@ -5,7 +5,7 @@ use editor::{Editor, Inlay, MultiBuffer}; use feature_flags::{FeatureFlag, PresenceFlag, register_feature_flag}; use gpui::{ App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable, - Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*, + Length, StyleRefinement, Task, TextStyleRefinement, Window, actions, prelude::*, }; use language::{ Bias, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, Point, ToOffset, ToPoint, @@ -71,6 +71,8 @@ struct ActivePrediction { expected_editor: Entity, _expected_buffer_subscription: gpui::Subscription, formatted_inputs: Entity, + _predicted_diff_task: Task<()>, + expected_diff_task: Task<()>, } #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -308,29 +310,14 @@ impl RatePredictionsModal { new_buffer_snapshot: BufferSnapshot, old_buffer_snapshot: BufferSnapshot, cx: &mut App, - ) { - let language = new_buffer_snapshot.language().cloned(); + ) -> Task<()> { diff.update(cx, |diff, cx| { - let update = diff.update_diff( - new_buffer_snapshot.text.clone(), + diff.set_base_text( Some(old_buffer_snapshot.text().into()), - Some(true), - language, + new_buffer_snapshot.text, cx, - ); - cx.spawn(async move |diff, cx| { - let update = update.await; - if let Some(task) = diff - .update(cx, |diff, cx| { - diff.set_snapshot(update, &new_buffer_snapshot.text, cx) - }) - .ok() - { - task.await; - } - }) - .detach(); - }); + ) + }) } fn insert_editable_region_markers( @@ -469,10 +456,17 @@ impl RatePredictionsModal { Point::new(visible_range.start.row.saturating_sub(5), 0) ..Point::new(visible_range.end.row.saturating_add(5), 0) .min(predicted_buffer_snapshot.max_point()); - self.diff_editor.update(cx, |editor, cx| { + let predicted_diff_task = self.diff_editor.update(cx, |editor, cx| { let predicted_buffer_id = predicted_buffer_snapshot.remote_id(); - let diff = cx.new(|cx| BufferDiff::new(&predicted_buffer_snapshot.text, cx)); - Self::update_buffer_diff( + let diff = cx.new(|cx| { + BufferDiff::new( + &predicted_buffer_snapshot.text, + predicted_buffer_snapshot.language().cloned(), + predicted_buffer.read(cx).language_registry(), + cx, + ) + }); + let predicted_diff_task = Self::update_buffer_diff( &diff, predicted_buffer_snapshot.clone(), prediction.snapshot.clone(), @@ -490,6 +484,7 @@ impl RatePredictionsModal { ); multibuffer.add_diff(diff, cx); }); + predicted_diff_task }); if let Some(editable_range) = editable_range.as_ref() { @@ -625,8 +620,15 @@ impl RatePredictionsModal { ..range.end.to_point(&expected_buffer_snapshot) }) .unwrap_or(visible_range); - let expected_diff = cx.new(|cx| BufferDiff::new(&expected_buffer_snapshot.text, cx)); - Self::update_buffer_diff( + let expected_diff = cx.new(|cx| { + BufferDiff::new( + &expected_buffer_snapshot.text, + expected_buffer_snapshot.language().cloned(), + expected_buffer.read(cx).language_registry(), + cx, + ) + }); + let expected_diff_task = Self::update_buffer_diff( &expected_diff, expected_buffer_snapshot.clone(), prediction.snapshot.clone(), @@ -676,16 +678,19 @@ impl RatePredictionsModal { let expected_buffer_subscription = cx.subscribe(&expected_buffer, { let expected_diff = expected_diff.clone(); let original_snapshot = prediction.snapshot.clone(); - move |_this, buffer, event, cx| match event { + move |this, buffer, event, cx| match event { language::BufferEvent::Edited { .. } | language::BufferEvent::LanguageChanged(_) | language::BufferEvent::Reparsed => { - Self::update_buffer_diff( + let task = Self::update_buffer_diff( &expected_diff, buffer.read(cx).snapshot(), original_snapshot.clone(), cx, ); + if let Some(active_prediction) = this.active_prediction.as_mut() { + active_prediction.expected_diff_task = task; + } } _ => {} } @@ -716,6 +721,8 @@ impl RatePredictionsModal { expected_buffer, expected_editor, _expected_buffer_subscription: expected_buffer_subscription, + _predicted_diff_task: predicted_diff_task, + expected_diff_task, formatted_inputs: cx.new(|cx| { Markdown::new( formatted_inputs.into(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c7a259495..1dcb218080 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1122,6 +1122,10 @@ pub struct Editor { load_diff_task: Option>>, /// Whether we are temporarily displaying a diff other than git's temporary_diff_override: bool, + /// Whether to render all diff hunks with the "unstaged" appearance, + /// regardless of whether they have a secondary hunk. Used by views whose + /// diffs aren't related to the git index (e.g. agent diffs). + render_diff_hunks_as_unstaged: bool, selection_mark_mode: bool, toggle_fold_multiple_buffers: Task<()>, _scroll_cursor_center_top_bottom_task: Task<()>, @@ -2348,6 +2352,7 @@ impl Editor { text_style_refinement: None, load_diff_task: load_uncommitted_diff, temporary_diff_override: false, + render_diff_hunks_as_unstaged: false, minimap: None, change_list: ChangeList::new(), mode, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 49eb44ead5..48a074b25c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -37636,95 +37636,6 @@ async fn test_restore_and_next(cx: &mut TestAppContext) { ); } -#[gpui::test] -async fn test_restore_hunk_with_stale_base_text(cx: &mut TestAppContext) { - // Regression test: prepare_restore_change must read base_text from the same - // snapshot the hunk came from, not from the live BufferDiff entity. The live - // entity's base_text may have already been updated asynchronously (e.g. - // because git HEAD changed) while the MultiBufferSnapshot still holds the - // old hunk byte ranges — using both together causes Rope::slice to panic - // when the old range exceeds the new base text length. - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - - let long_base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n"; - cx.set_state("ˇONE\ntwo\nTHREE\nfour\nFIVE\nsix\nseven\neight\nnine\nten\n"); - cx.set_head_text(long_base_text); - - let buffer_id = cx.update_buffer(|buffer, _| buffer.remote_id()); - - // Verify we have hunks from the initial diff. - let has_hunks = cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let hunks = snapshot - .buffer_snapshot() - .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len()); - hunks.count() > 0 - }); - assert!(has_hunks, "should have diff hunks before restoring"); - - // Now trigger a git HEAD change to a much shorter base text. - // After this, the live BufferDiff entity's base_text buffer will be - // updated synchronously (inside set_snapshot_with_secondary_inner), - // but DiffChanged is deferred until parsing_idle completes. - // We step the executor tick-by-tick to find the window where the - // live base_text is already short but the MultiBuffer snapshot is - // still stale (old hunks + old base_text). - let short_base_text = "short\n"; - let fs = cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); - let path = cx.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); - fs.set_head_for_repo( - &Path::new(path!("/root")).join(".git"), - &[(path.as_unix_str(), short_base_text.to_string())], - "newcommit", - ); - - // Step the executor tick-by-tick. At each step, check whether the - // race condition exists: live BufferDiff has short base text but - // the MultiBuffer snapshot still has old (long) hunks. - let mut found_race = false; - for _ in 0..200 { - cx.executor().tick(); - - let race_exists = cx.update_editor(|editor, _window, cx| { - let multi_buffer = editor.buffer().read(cx); - let diff_entity = match multi_buffer.diff_for(buffer_id) { - Some(d) => d, - None => return false, - }; - let live_base_len = diff_entity.read(cx).base_text(cx).len(); - let snapshot = multi_buffer.snapshot(cx); - let snapshot_base_len = snapshot - .diff_for_buffer_id(buffer_id) - .map(|d| d.base_text().len()); - // Race: live base text is shorter than what the snapshot knows. - live_base_len < long_base_text.len() && snapshot_base_len == Some(long_base_text.len()) - }); - - if race_exists { - found_race = true; - // The race window is open: the live entity has new (short) base - // text but the MultiBuffer snapshot still has old hunks with byte - // ranges computed against the old long base text. Attempt restore. - // Without the fix, this panics with "cannot summarize past end of - // rope". With the fix, it reads base_text from the stale snapshot - // (consistent with the stale hunks) and succeeds. - cx.update_editor(|editor, window, cx| { - editor.select_all(&SelectAll, window, cx); - editor.git_restore(&Default::default(), window, cx); - }); - break; - } - } - - assert!( - found_race, - "failed to observe the race condition between \ - live BufferDiff base_text and stale MultiBuffer snapshot; \ - the test may need adjustment if the async diff pipeline changed" - ); -} - #[gpui::test] async fn test_align_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8994021ea5..e3d634b4a0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5135,6 +5135,7 @@ impl EditorElement { } fn paint_gutter_diff_hunks( + &self, layout: &mut EditorLayout, split_side: Option, window: &mut Window, @@ -5210,7 +5211,7 @@ impl EditorElement { .editor_background .blend(background_color); - if !Self::diff_hunk_hollow(status, cx) { + if !self.diff_hunk_hollow(status, cx) { window.paint_quad(quad( hunk_bounds, corner_radii, @@ -5388,7 +5389,7 @@ impl EditorElement { ) }); if show_git_gutter { - Self::paint_gutter_diff_hunks(layout, self.split_side, window, cx) + self.paint_gutter_diff_hunks(layout, self.split_side, window, cx) } let highlight_width = 0.275 * layout.position_map.line_height; @@ -6501,8 +6502,9 @@ impl EditorElement { ) } - fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool { - let unstaged = status.has_secondary_hunk(); + fn diff_hunk_hollow(&self, status: DiffHunkStatus, cx: &mut App) -> bool { + let unstaged = + self.editor.read(cx).render_diff_hunks_as_unstaged || status.has_secondary_hunk(); let unstaged_hollow = matches!( ProjectSettings::get_global(cx).git.hunk_style, GitHunkStyleSetting::UnstagedHollow @@ -8194,7 +8196,7 @@ impl Element for EditorElement { type_id: None, }; - let background = if Self::diff_hunk_hollow(diff_status, cx) { + let background = if self.diff_hunk_hollow(diff_status, cx) { hollow_highlight } else { filled_highlight diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 16b1bc4dae..dbe4f073e5 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -175,6 +175,18 @@ impl Editor { cx.notify(); } + /// Make all diff hunks render with the "unstaged" appearance, regardless + /// of whether they have a secondary hunk. Intended for views whose diffs + /// aren't related to the git index (e.g. agent diffs). + pub fn set_render_diff_hunks_as_unstaged( + &mut self, + render_as_unstaged: bool, + cx: &mut Context, + ) { + self.render_diff_hunks_as_unstaged = render_as_unstaged; + cx.notify(); + } + pub fn git_blame_inline_enabled(&self) -> bool { self.git_blame_inline_enabled } @@ -230,6 +242,7 @@ impl Editor { pub fn end_temporary_diff_override(&mut self, cx: &mut Context) { self.temporary_diff_override = false; + self.render_diff_hunks_as_unstaged = false; self.set_render_diff_hunk_controls(Arc::new(render_diff_hunk_controls), cx); self.buffer.update(cx, |buffer, cx| { buffer.set_all_diff_hunks_collapsed(cx); diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index d6380c04cf..2c3bd5f8db 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -471,6 +471,12 @@ impl SplittableEditor { }); } + pub fn set_render_diff_hunks_as_unstaged(&self, cx: &mut Context) { + self.update_editors(cx, |editor, cx| { + editor.set_render_diff_hunks_as_unstaged(true, cx); + }); + } + fn focused_side(&self) -> SplitSide { if let Some(lhs) = &self.lhs && lhs.was_last_focused @@ -607,9 +613,11 @@ impl SplittableEditor { }); let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone(); + let render_diff_hunks_as_unstaged = self.rhs_editor.read(cx).render_diff_hunks_as_unstaged; let lhs_editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx); + editor.set_render_diff_hunks_as_unstaged(render_diff_hunks_as_unstaged, cx); editor.set_number_deleted_lines(true, cx); editor.set_delegate_expand_excerpts(true); editor.set_delegate_stage_and_restore(true); @@ -2612,7 +2620,7 @@ mod tests { .collect::>(); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path(path, buffer.clone(), ranges, 0, diff.clone(), cx); }); cx.run_until_parked(); @@ -2669,7 +2677,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -2798,7 +2806,7 @@ mod tests { let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx); editor.update(cx, |editor, cx| { - let path1 = PathKey::for_buffer(&buffer1, cx); + let path1 = PathKey::sorted(0); editor.update_excerpts_for_path( path1, buffer1.clone(), @@ -2807,7 +2815,7 @@ mod tests { diff1.clone(), cx, ); - let path2 = PathKey::for_buffer(&buffer2, cx); + let path2 = PathKey::sorted(1); editor.update_excerpts_for_path( path2, buffer2.clone(), @@ -2956,7 +2964,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3083,7 +3091,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3202,7 +3210,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3332,7 +3340,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3429,7 +3437,7 @@ mod tests { editor.update(cx, |editor, cx| { let end = Point::new(0, text.len() as u32); - let path1 = PathKey::for_buffer(&buffer1, cx); + let path1 = PathKey::sorted(0); editor.update_excerpts_for_path( path1, buffer1.clone(), @@ -3438,7 +3446,7 @@ mod tests { diff1.clone(), cx, ); - let path2 = PathKey::for_buffer(&buffer2, cx); + let path2 = PathKey::sorted(1); editor.update_excerpts_for_path( path2, buffer2.clone(), @@ -3506,7 +3514,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3569,7 +3577,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3630,7 +3638,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3743,10 +3751,10 @@ mod tests { .unindent(); let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx)); - let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx)); + let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), None, None, cx)); editor.update(cx, |editor, cx| { - let path1 = PathKey::for_buffer(&buffer1, cx); + let path1 = PathKey::sorted(0); editor.update_excerpts_for_path( path1, buffer1.clone(), @@ -3756,7 +3764,7 @@ mod tests { cx, ); - let path2 = PathKey::for_buffer(&buffer2, cx); + let path2 = PathKey::sorted(1); editor.update_excerpts_for_path( path2, buffer2.clone(), @@ -3854,7 +3862,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -3930,7 +3938,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4017,7 +4025,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4131,7 +4139,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4215,7 +4223,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4299,7 +4307,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4391,7 +4399,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4519,7 +4527,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4666,7 +4674,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -4888,7 +4896,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -5227,18 +5235,16 @@ mod tests { let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id()); editor.update(cx, |editor, cx| { - let path1 = PathKey::for_buffer(&buffer1, cx); editor.update_excerpts_for_path( - path1, + PathKey::sorted(0), buffer1.clone(), vec![Point::new(0, 0)..buffer1.read(cx).max_point()], 0, diff1.clone(), cx, ); - let path2 = PathKey::for_buffer(&buffer2, cx); editor.update_excerpts_for_path( - path2, + PathKey::sorted(1), buffer2.clone(), vec![Point::new(0, 0)..buffer2.read(cx).max_point()], 1, @@ -5392,7 +5398,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -5553,7 +5559,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -5712,7 +5718,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -5843,7 +5849,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -5904,7 +5910,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -5987,7 +5993,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), @@ -6096,8 +6102,8 @@ mod tests { let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, ¤t_text_a, &mut cx); let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_text_b, &mut cx); - let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx)); - let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx)); + let path_a = PathKey::sorted(0); + let path_b = PathKey::sorted(1); editor.update(cx, |editor, cx| { editor.update_excerpts_for_path( @@ -6238,7 +6244,7 @@ mod tests { let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx); editor.update(cx, |editor, cx| { - let path = PathKey::for_buffer(&buffer, cx); + let path = PathKey::sorted(0); editor.update_excerpts_for_path( path, buffer.clone(), diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 353deb47ed..9fff74d398 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -368,7 +368,14 @@ impl CommitView { let buffer_diff = if is_binary { cx.update(|_, cx| { let snapshot = buffer.read(cx).snapshot(); - cx.new(|cx| BufferDiff::new_unchanged(&snapshot, cx)) + cx.new(|cx| { + BufferDiff::new_unchanged( + &snapshot, + snapshot.language().cloned(), + Some(language_registry.clone()), + cx, + ) + }) })? } else { build_buffer_diff(old_text, &buffer, &language_registry, cx).await? @@ -996,23 +1003,15 @@ async fn build_buffer_diff( let language = cx.update(|_, cx| buffer.read(cx).language().cloned())?; let buffer = cx.update(|_, cx| buffer.read(cx).snapshot())?; - let diff = cx.new(|cx| BufferDiff::new(&buffer.text, cx)); - - let update = diff - .update(cx, |diff, cx| { - diff.update_diff( - buffer.text.clone(), - old_text.map(|old_text| Arc::from(old_text.as_str())), - Some(true), - language.clone(), - cx, - ) - }) - .await; + let diff = + cx.new(|cx| BufferDiff::new(&buffer.text, language, Some(language_registry.clone()), cx)); diff.update(cx, |diff, cx| { - diff.language_changed(language, Some(language_registry.clone()), cx); - diff.set_snapshot(update, &buffer.text, cx) + diff.set_base_text( + old_text.map(|old_text| Arc::from(old_text.as_str())), + buffer.text.clone(), + cx, + ) }) .await; diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index ac9e4175b6..477f18be54 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -8,7 +8,7 @@ use gpui::{ AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Font, IntoElement, Render, Task, WeakEntity, Window, }; -use language::{Buffer, HighlightedText, LanguageRegistry}; +use language::{Buffer, HighlightedText}; use project::{Project, ProjectPath}; use settings::Settings; use std::{ @@ -53,9 +53,8 @@ impl FileDiffView { let new_buffer = project .update(cx, |project, cx| project.open_local_buffer(&new_path, cx)) .await?; - let languages = project.update(cx, |project, _| project.languages().clone()); - let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?; + let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?; workspace.update_in(cx, |workspace, window, cx| { let workspace_entity = cx.entity(); @@ -108,6 +107,7 @@ impl FileDiffView { editor.start_temporary_diff_override(); }); splittable.disable_diff_hunk_controls(cx); + splittable.set_render_diff_hunks_as_unstaged(cx); splittable }); @@ -154,13 +154,11 @@ impl FileDiffView { diff.update(cx, |diff, cx| { diff.set_base_text( Some(old_snapshot.text().as_str().into()), - old_snapshot.language().cloned(), new_snapshot.text.clone(), cx, ) }) - .await - .ok(); + .await; log::trace!("finish recalculating"); } Ok(()) @@ -170,36 +168,30 @@ impl FileDiffView { } #[ztracing::instrument(skip_all)] -async fn build_buffer_diff( +pub(crate) async fn build_buffer_diff( old_buffer: &Entity, new_buffer: &Entity, - language_registry: Arc, cx: &mut AsyncApp, ) -> Result> { let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot()); let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot()); + let language_registry = new_buffer.read_with(cx, |buffer, _| buffer.language_registry()); - let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx)); - - let update = diff - .update(cx, |diff, cx| { - diff.update_diff( - new_buffer_snapshot.text.clone(), - Some(old_buffer_snapshot.text().into()), - Some(true), - new_buffer_snapshot.language().cloned(), - cx, - ) - }) - .await; + let diff = cx.new(|cx| { + BufferDiff::new( + &new_buffer_snapshot.text, + new_buffer_snapshot.language().cloned(), + language_registry, + cx, + ) + }); diff.update(cx, |diff, cx| { - diff.language_changed( - new_buffer_snapshot.language().cloned(), - Some(language_registry), + diff.set_base_text( + Some(old_buffer_snapshot.text().into()), + new_buffer_snapshot.text.clone(), cx, - ); - diff.set_snapshot(update, &new_buffer_snapshot.text, cx) + ) }) .await; diff --git a/crates/git_ui/src/multi_diff_view.rs b/crates/git_ui/src/multi_diff_view.rs index a3b55fabe9..f8097e68f5 100644 --- a/crates/git_ui/src/multi_diff_view.rs +++ b/crates/git_ui/src/multi_diff_view.rs @@ -1,3 +1,4 @@ +use crate::file_diff_view::build_buffer_diff; use anyhow::Result; use buffer_diff::BufferDiff; use editor::{Editor, EditorEvent, MultiBuffer, multibuffer_context_lines}; @@ -140,36 +141,6 @@ fn common_prefix(paths: &[PathBuf]) -> Option { Some(prefix) } -async fn build_buffer_diff( - old_buffer: &Entity, - new_buffer: &Entity, - cx: &mut AsyncApp, -) -> Result> { - let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot()); - let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx)); - - let update = diff - .update(cx, |diff, cx| { - diff.update_diff( - new_buffer_snapshot.text.clone(), - Some(old_buffer_snapshot.text().into()), - Some(true), - new_buffer_snapshot.language().cloned(), - cx, - ) - }) - .await; - - diff.update(cx, |diff, cx| { - diff.set_snapshot(update, &new_buffer_snapshot.text, cx) - }) - .await; - - Ok(diff) -} - impl MultiDiffView { pub fn open( diff_pairs: Vec<[String; 2]>, @@ -228,6 +199,7 @@ impl MultiDiffView { editor.start_temporary_diff_override(); editor.disable_diagnostics(cx); editor.set_expand_all_diff_hunks(cx); + editor.set_render_diff_hunks_as_unstaged(true, cx); editor.set_render_diff_hunk_controls( Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), cx, diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 5627c40a4c..cd929c22e6 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -836,7 +836,6 @@ impl ProjectDiff { ); } buffer_diff::BufferDiffEvent::BaseTextChanged - | buffer_diff::BufferDiffEvent::LanguageChanged | buffer_diff::BufferDiffEvent::HunksStagedOrUnstaged(_) => {} } }); diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index aae52ffe43..5312ae6d08 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -11,7 +11,7 @@ use gpui::{ AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, Render, Task, Window, }; -use language::{self, Buffer, OffsetRangeExt, Point}; +use language::{self, Buffer, Capability, OffsetRangeExt, Point}; use project::{Project, ProjectPath}; use settings::Settings; use std::{ @@ -110,16 +110,22 @@ impl TextDiffView { } let workspace = workspace.weak_handle(); - let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx)); let clipboard_buffer = build_clipboard_buffer( clipboard_text, &source_buffer, expanded_selection_range.clone(), cx, ); + let diff_buffer = cx.new(|cx| { + BufferDiff::new_with_base_text_buffer( + &source_buffer_snapshot.text, + clipboard_buffer.clone(), + cx, + ) + }); let task = window.spawn(cx, async move |cx| { - update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?; + update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await; workspace.update_in(cx, |workspace, window, cx| { let project = workspace.project().clone(); @@ -179,6 +185,7 @@ impl TextDiffView { cx, ); splittable.disable_diff_hunk_controls(cx); + splittable.set_render_diff_hunks_as_unstaged(cx); splittable.rhs_editor().update(cx, |editor, _cx| { editor.start_temporary_diff_override(); }); @@ -240,7 +247,7 @@ impl TextDiffView { } log::trace!("start recalculating"); - update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?; + update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await; log::trace!("finish recalculating"); } Ok(()) @@ -259,12 +266,17 @@ fn build_clipboard_buffer( cx.new(|cx| { let mut buffer = language::Buffer::local(source_buffer_snapshot.text(), cx); let language = source_buffer.read(cx).language().cloned(); + if let Some(language_registry) = source_buffer.read(cx).language_registry() { + buffer.set_language_registry(language_registry); + } buffer.set_language(language, cx); let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start); let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end); buffer.edit([(range_start..range_end, text)], None, cx); + buffer.set_capability(Capability::ReadOnly, cx); + buffer }) } @@ -274,32 +286,23 @@ async fn update_diff_buffer( source_buffer: &Entity, clipboard_buffer: &Entity, cx: &mut AsyncApp, -) -> Result<()> { +) { let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot()); - let language = source_buffer_snapshot.language().cloned(); - let language_registry = source_buffer.read_with(cx, |buffer, _| buffer.language_registry()); - let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot()); - let base_text = base_buffer_snapshot.text(); + let base_text = Arc::::from(base_buffer_snapshot.text()); let update = diff .update(cx, |diff, cx| { diff.update_diff( source_buffer_snapshot.text.clone(), - Some(Arc::from(base_text.as_str())), - Some(true), - language.clone(), + &base_buffer_snapshot, + Some(base_text.clone()), cx, ) }) .await; - diff.update(cx, |diff, cx| { - diff.language_changed(language, language_registry, cx); - diff.set_snapshot(update, &source_buffer_snapshot.text, cx) - }) - .await; - Ok(()) + diff.update(cx, |diff, cx| diff.set_snapshot(update, cx)); } impl EventEmitter for TextDiffView {} diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3f02305a4a..57612012c8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3330,6 +3330,76 @@ impl Buffer { pub fn set_group_interval(&mut self, group_interval: Duration) { self.text.set_group_interval(group_interval); } + + // TODO: see if ep can use this instead of Buffer::branch + pub fn snapshot_with_edits( + &mut self, + edits: I, + cx: &mut Context, + ) -> Task + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + let mut snapshot = self.snapshot(); + let text = snapshot.text.clone(); + let mut syntax = snapshot.syntax.clone(); + let language = self.language().cloned(); + let registry = self.language_registry(); + let new_text = self.text.snapshot_with_edits(edits); + cx.background_spawn(async move { + if let Some(language) = language.clone() { + syntax.reparse(&text, registry.clone(), language); + } + + syntax.interpolate(&new_text.snapshot); + + if let Some(language) = language { + syntax.reparse(&new_text.snapshot, registry, language); + } + + snapshot.text = new_text.snapshot.clone(); + snapshot.syntax = syntax; + + EditedBufferSnapshot { + text: new_text, + snapshot, + } + }) + } + + pub fn fast_forward(&mut self, edited: EditedBufferSnapshot, cx: &mut Context) { + let base_version = edited.text.base_version.clone(); + let did_edit = edited.text.did_edit; + self.text.fast_forward(edited.text); + if edited.snapshot.language == self.language { + self.reparse = None; + self.did_finish_parsing(edited.snapshot.syntax, None, cx); + if did_edit { + cx.emit(BufferEvent::Edited { + source: BufferEditSource::User, + }); + } + } else { + self.did_edit(&base_version, false, BufferEditSource::User, cx); + } + } +} + +pub struct EditedBufferSnapshot { + text: text::EditedBufferSnapshot, + snapshot: BufferSnapshot, +} + +impl EditedBufferSnapshot { + pub fn snapshot(&self) -> &BufferSnapshot { + &self.snapshot + } + + pub fn base_version(&self) -> &clock::Global { + &self.text.base_version + } } #[doc(hidden)] diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 1641c460eb..dd0c24fb5f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -615,6 +615,7 @@ impl DiffState { changed_range, base_text_changed_range: _, extended_range, + base_text_changed: _, }) => { let use_extended = this.snapshot.borrow().use_extended_diff_range; let range = if use_extended { @@ -622,13 +623,10 @@ impl DiffState { } else { changed_range.clone() }; - if let Some(range) = range { - this.buffer_diff_changed(diff, range, cx) - } + this.buffer_diff_changed(diff, range, cx); cx.emit(Event::BufferDiffChanged); } - BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx), - _ => {} + BufferDiffEvent::BaseTextChanged | BufferDiffEvent::HunksStagedOrUnstaged(_) => {} }), diff, main_buffer: None, @@ -652,6 +650,7 @@ impl DiffState { changed_range: _, base_text_changed_range, extended_range: _, + base_text_changed: _, }) => { this.inverted_buffer_diff_changed( diff, @@ -661,10 +660,8 @@ impl DiffState { ); cx.emit(Event::BufferDiffChanged); } - BufferDiffEvent::LanguageChanged => { - this.inverted_buffer_diff_language_changed(diff, main_buffer, cx) - } - _ => {} + BufferDiffEvent::BaseTextChanged + | BufferDiffEvent::HunksStagedOrUnstaged(_) => {} } } }), @@ -1973,38 +1970,10 @@ impl MultiBuffer { }); } - fn buffer_diff_language_changed(&mut self, diff: Entity, cx: &mut Context) { - let diff = diff.read(cx); - let buffer_id = diff.buffer_id; - let diff = DiffStateSnapshot { - buffer_id, - diff: diff.snapshot(cx), - main_buffer: None, - }; - self.snapshot.get_mut().diffs.insert_or_replace(diff, ()); - } - - fn inverted_buffer_diff_language_changed( - &mut self, - diff: Entity, - main_buffer: Entity, - cx: &mut Context, - ) { - let base_text_buffer_id = diff.read(cx).base_text_buffer().read(cx).remote_id(); - let main_buffer_snapshot = main_buffer.read(cx).snapshot(); - let diff = diff.read(cx); - let diff = DiffStateSnapshot { - buffer_id: base_text_buffer_id, - diff: diff.snapshot(cx), - main_buffer: Some(main_buffer_snapshot), - }; - self.snapshot.get_mut().diffs.insert_or_replace(diff, ()); - } - fn buffer_diff_changed( &mut self, diff: Entity, - range: Range, + range: Option>, cx: &mut Context, ) { let Some(buffer) = self.buffer(diff.read(cx).buffer_id) else { @@ -2029,6 +1998,9 @@ impl MultiBuffer { snapshot.diffs.insert_or_replace(new_diff, ()); let buffer = buffer.read(cx); + let Some(range) = range else { + return; + }; let diff_change_range = range.to_offset(buffer); let excerpt_edits = snapshot.excerpt_edits_for_diff_change(&path, diff_change_range); @@ -2239,7 +2211,7 @@ impl MultiBuffer { self.buffer_diff_changed( diff.clone(), - text::Anchor::min_max_range_for_buffer(buffer_id), + Some(text::Anchor::min_max_range_for_buffer(buffer_id)), cx, ); self.diffs.insert(buffer_id, DiffState::new(diff, cx)); diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 3e71deb8f8..8e52eac1cb 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -1,12 +1,13 @@ use super::*; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; -use gpui::{App, TestAppContext}; +use gpui::{App, Entity, TestAppContext}; use indoc::indoc; use language::{Buffer, Rope}; use parking_lot::RwLock; use rand::prelude::*; use settings::SettingsStore; use std::env; +use std::sync::Arc; use std::time::{Duration, Instant}; use util::RandomCharIter; use util::rel_path::rel_path; @@ -4783,21 +4784,19 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { ); }); cx.run_until_parked(); + let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); let update = diff .update(cx, |diff, cx| { diff.update_diff( - buffer.read(cx).text_snapshot(), - Some(base_text.into()), - None, - None, + buffer_snapshot, + &base_text_snapshot, + Some(Arc::from(base_text)), cx, ) }) .await; - diff.update(cx, |diff, cx| { - diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx) - }) - .await; + diff.update(cx, |diff, cx| diff.set_snapshot(update, cx)); cx.run_until_parked(); assert_new_snapshot( @@ -4821,21 +4820,19 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { buffer.set_text("ZERO\nONE\nTWO\n", cx); }); cx.run_until_parked(); + let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); let update = diff .update(cx, |diff, cx| { diff.update_diff( - buffer.read(cx).text_snapshot(), - Some(base_text.into()), - None, - None, + buffer_snapshot, + &base_text_snapshot, + Some(Arc::from(base_text)), cx, ) }) .await; - diff.update(cx, |diff, cx| { - diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx) - }) - .await; + diff.update(cx, |diff, cx| diff.set_snapshot(update, cx)); cx.run_until_parked(); assert_new_snapshot( @@ -4858,13 +4855,11 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { diff.update(cx, |diff, cx| { diff.set_base_text( Some("new base\n".into()), - None, buffer.read(cx).text_snapshot(), cx, ) }) - .await - .unwrap(); + .await; cx.run_until_parked(); assert_new_snapshot( @@ -4915,21 +4910,10 @@ async fn test_inverted_diff_base_text_change(cx: &mut TestAppContext) { ), ); - let update = diff - .update(cx, |diff, cx| { - diff.update_diff( - buffer.read(cx).text_snapshot(), - Some("ddd\n".into()), - Some(true), - None, - cx, - ) - }) - .await; diff.update(cx, |diff, cx| { - diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx) + diff.set_base_text(Some("ddd\n".into()), buffer.read(cx).text_snapshot(), cx) }) - .detach(); + .await; let _hunks: Vec<_> = multibuffer .read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)) @@ -4961,22 +4945,19 @@ async fn test_inverted_diff_secondary_version_mismatch(cx: &mut TestAppContext) buffer.edit([(0..0, "ZERO\n")], None, cx); }); + let base_text_snapshot = unstaged_diff.read_with(cx, |diff, cx| diff.base_text(cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); let update = unstaged_diff .update(cx, |diff, cx| { diff.update_diff( - buffer.read(cx).text_snapshot(), - Some(index_text.into()), - None, - None, + buffer_snapshot, + &base_text_snapshot, + Some(Arc::from(index_text)), cx, ) }) .await; - unstaged_diff - .update(cx, |diff, cx| { - diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx) - }) - .await; + unstaged_diff.update(cx, |diff, cx| diff.set_snapshot(update, cx)); let base_text_buffer = uncommitted_diff.read_with(cx, |diff, _| diff.base_text_buffer().clone()); diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 70266e2138..6ec98f522e 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -51,7 +51,7 @@ use gpui::{ Subscription, Task, TaskExt, WeakEntity, }; use language::{ - Buffer, BufferEvent, Language, LanguageRegistry, + Buffer, BufferEvent, Capability, Language, LanguageRegistry, proto::{deserialize_version, serialize_version}, }; use parking_lot::Mutex; @@ -118,6 +118,7 @@ struct SharedDiffs { struct BufferGitState { unstaged_diff: Option>, + staged_diff: Option<(WeakEntity, Entity)>, uncommitted_diff: Option>, oid_diffs: HashMap, WeakEntity>, conflict_set: Option>, @@ -142,6 +143,9 @@ struct BufferGitState { head_text: Option>, index_text: Option>, oid_texts: HashMap>, + head_text_buffer: WeakEntity, + index_text_buffer: WeakEntity, + index_text_buffer_language_enabled: bool, head_changed: bool, index_changed: bool, language_changed: bool, @@ -161,6 +165,7 @@ enum DiffBasesChange { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum DiffKind { Unstaged, + Staged, Uncommitted, SinceOid(Option), } @@ -931,6 +936,53 @@ impl GitStore { cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) } + pub fn open_staged_diff( + &mut self, + buffer: Entity, + cx: &mut Context, + ) -> Task>> { + let buffer_id = buffer.read(cx).remote_id(); + + if let Some(diff_state) = self.diffs.get(&buffer_id) + && let Some(staged_diff) = diff_state.read(cx).staged_diff() + { + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) + { + return cx.background_executor().spawn(async move { + task.await; + Ok(staged_diff) + }); + } + return Task::ready(Ok(staged_diff)); + } + + let Some((repo, repo_path)) = + self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) + else { + return Task::ready(Err(anyhow!("failed to find git repository for buffer"))); + }; + + let task = self + .loading_diffs + .entry((buffer_id, DiffKind::Staged)) + .or_insert_with(|| { + let changes = repo.update(cx, |repo, cx| { + repo.load_committed_text(buffer_id, repo_path, cx) + }); + + cx.spawn(async move |this, cx| { + Self::open_diff_internal(this, DiffKind::Staged, changes.await, buffer, cx) + .await + .map_err(Arc::new) + }) + .shared() + }) + .clone(); + + cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) + } + pub fn open_diff_since( &mut self, oid: Option, @@ -968,29 +1020,28 @@ impl GitStore { buffer.update(cx, |buffer, _| buffer.language_registry()); let content: Option> = match oid { None => None, - Some(oid) => Some( - repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx)) - .await? - .into(), - ), + Some(oid) => Some({ + let mut content = repo + .update(cx, |repo, cx| repo.load_blob_content(oid, cx)) + .await?; + text::LineEnding::normalize(&mut content); + content.into() + }), }; - let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx)); + let buffer_diff = cx.new(|cx| { + BufferDiff::new( + &buffer_snapshot, + buffer_snapshot.language().cloned(), + language_registry, + cx, + ) + }); buffer_diff .update(cx, |buffer_diff, cx| { - buffer_diff.language_changed( - buffer_snapshot.language().cloned(), - language_registry, - cx, - ); - buffer_diff.set_base_text( - content.clone(), - buffer_snapshot.language().cloned(), - buffer_snapshot.text, - cx, - ) + buffer_diff.set_base_text(content.clone(), buffer_snapshot.text, cx) }) - .await?; + .await; let unstaged_diff = this .update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))? .await?; @@ -1008,7 +1059,7 @@ impl GitStore { let diff_state = this .diffs .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); + .or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx))); diff_state.update(cx, |state, _| { if let Some(oid) = oid { @@ -1122,25 +1173,99 @@ impl GitStore { let diff_state = this .diffs .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); + .or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx))); - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); + let existing_unstaged_diff = diff_state.read(cx).unstaged_diff(); - cx.subscribe(&diff, Self::on_buffer_diff_event).detach(); + let mut staged_index_text_buffer = None; + let diff = if kind == DiffKind::Unstaged + && let Some(existing_unstaged_diff) = existing_unstaged_diff.clone() + { + existing_unstaged_diff + } else { + let diff = match kind { + DiffKind::Unstaged => { + let base_text_buffer = diff_state.update(cx, |diff_state, cx| { + diff_state.get_or_create_index_text_buffer(cx) + }); + cx.new(|cx| { + BufferDiff::new_with_base_text_buffer( + &text_snapshot, + base_text_buffer, + cx, + ) + }) + } + DiffKind::Staged => { + let (index_text_buffer, base_text_buffer) = + diff_state.update(cx, |diff_state, cx| { + ( + diff_state.get_or_create_index_text_buffer(cx), + diff_state.get_or_create_head_text_buffer(cx), + ) + }); + index_text_buffer.update(cx, |index_text_buffer, cx| { + if let Some(language_registry) = language_registry.clone() { + index_text_buffer.set_language_registry(language_registry); + } + index_text_buffer.set_language_async(language.clone(), cx); + }); + let index_text_snapshot = index_text_buffer.read(cx).text_snapshot(); + staged_index_text_buffer = Some(index_text_buffer); + cx.new(|cx| { + BufferDiff::new_with_base_text_buffer( + &index_text_snapshot, + base_text_buffer, + cx, + ) + }) + } + DiffKind::Uncommitted => { + let base_text_buffer = diff_state.update(cx, |diff_state, cx| { + diff_state.get_or_create_head_text_buffer(cx) + }); + cx.new(|cx| { + BufferDiff::new_with_base_text_buffer( + &text_snapshot, + base_text_buffer, + cx, + ) + }) + } + DiffKind::SinceOid(_) => { + unreachable!("open_diff_internal is not used for OID diffs") + } + }; + cx.subscribe(&diff, Self::on_buffer_diff_event).detach(); + diff + }; diff_state.update(cx, |diff_state, cx| { - diff_state.language_changed = true; diff_state.language = language; diff_state.language_registry = language_registry; match kind { DiffKind::Unstaged => { - diff_state.unstaged_diff.get_or_insert(diff.downgrade()); + diff_state.unstaged_diff = Some(diff.downgrade()); + } + DiffKind::Staged => { + diff_state.index_text_buffer_language_enabled = true; + let index_text_buffer = staged_index_text_buffer + .take() + .context("index text buffer was not created for staged diff")?; + diff_state.staged_diff = Some((diff.downgrade(), index_text_buffer)); } DiffKind::Uncommitted => { - let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() { + let unstaged_diff = if let Some(diff) = existing_unstaged_diff { diff } else { - let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); + let base_text_buffer = diff_state.get_or_create_index_text_buffer(cx); + let unstaged_diff = cx.new(|cx| { + BufferDiff::new_with_base_text_buffer( + &text_snapshot, + base_text_buffer, + cx, + ) + }); diff_state.unstaged_diff = Some(unstaged_diff.downgrade()); unstaged_diff }; @@ -1172,6 +1297,11 @@ impl GitStore { diff_state.read(cx).unstaged_diff.as_ref()?.upgrade() } + pub fn get_staged_diff(&self, buffer_id: BufferId, cx: &App) -> Option> { + let diff_state = self.diffs.get(&buffer_id)?; + diff_state.read(cx).staged_diff() + } + pub fn get_uncommitted_diff( &self, buffer_id: BufferId, @@ -1191,6 +1321,17 @@ impl GitStore { diff_state.read(cx).oid_diff(oid) } + /// Whether this buffer's index text is known to match its committed text + /// without comparing contents, i.e. whether the texts share one allocation. + /// In a downstream project, this can only be true if the upstream sent + /// `Mode::IndexMatchesHead`. + #[cfg(any(test, feature = "test-support"))] + pub fn index_matches_head_for_buffer(&self, buffer_id: BufferId, cx: &App) -> bool { + self.diffs + .get(&buffer_id) + .is_some_and(|diff_state| diff_state.read(cx).index_matches_head()) + } + pub fn open_conflict_set( &mut self, buffer: Entity, @@ -1225,7 +1366,7 @@ impl GitStore { let buffer_git_state = self .diffs .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); + .or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx))); let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx)); self._subscriptions @@ -3561,45 +3702,30 @@ impl GitStore { .or_default(); shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone()); }); - Ok(diff.read_with(&cx, |diff, cx| { + this.read_with(&cx, |this, cx| { use proto::open_uncommitted_diff_response::Mode; - let unstaged_diff = diff.secondary_diff(); - let index_snapshot = unstaged_diff.and_then(|diff| { - let diff = diff.read(cx); - diff.base_text_exists().then(|| diff.base_text(cx)) - }); + let diff_state = this.diffs.get(&buffer_id).context("missing diff state")?; + let diff_state = diff_state.read(cx); + let index_matches_head = diff_state.index_matches_head(); + let index_text = diff_state.index_text.clone(); + let head_text = diff_state.head_text.clone(); - let mode; - let staged_text; - let committed_text; - if diff.base_text_exists() { - let committed_snapshot = diff.base_text(cx); - committed_text = Some(committed_snapshot.text()); - if let Some(index_text) = index_snapshot { - if index_text.remote_id() == committed_snapshot.remote_id() { - mode = Mode::IndexMatchesHead; - staged_text = None; - } else { - mode = Mode::IndexAndHead; - staged_text = Some(index_text.text()); - } - } else { - mode = Mode::IndexAndHead; - staged_text = None; + let response = if index_matches_head { + proto::OpenUncommittedDiffResponse { + committed_text: head_text.map(|head| head.to_string()), + staged_text: None, + mode: Mode::IndexMatchesHead.into(), } } else { - mode = Mode::IndexAndHead; - committed_text = None; - staged_text = index_snapshot.as_ref().map(|buffer| buffer.text()); - } - - proto::OpenUncommittedDiffResponse { - committed_text, - staged_text, - mode: mode.into(), - } - })) + proto::OpenUncommittedDiffResponse { + committed_text: head_text.map(|head| head.to_string()), + staged_text: index_text.map(|index| index.to_string()), + mode: Mode::IndexAndHead.into(), + } + }; + anyhow::Ok(response) + }) } async fn handle_update_diff_bases( @@ -3794,9 +3920,10 @@ impl GitStore { } impl BufferGitState { - fn new(_git_store: WeakEntity) -> Self { + fn new(_git_store: WeakEntity, _cx: &mut Context) -> Self { Self { unstaged_diff: Default::default(), + staged_diff: Default::default(), uncommitted_diff: Default::default(), oid_diffs: Default::default(), recalculate_diff_task: Default::default(), @@ -3808,6 +3935,9 @@ impl BufferGitState { head_text: Default::default(), index_text: Default::default(), oid_texts: Default::default(), + head_text_buffer: WeakEntity::new_invalid(), + index_text_buffer: WeakEntity::new_invalid(), + index_text_buffer_language_enabled: Default::default(), head_changed: Default::default(), index_changed: Default::default(), language_changed: Default::default(), @@ -3817,6 +3947,34 @@ impl BufferGitState { } } + fn get_or_create_head_text_buffer(&mut self, cx: &mut Context) -> Entity { + if let Some(buffer) = self.head_text_buffer.upgrade() { + return buffer; + } + let head_text = self.head_text.clone(); + let buffer = cx.new(|cx| { + let mut buffer = Buffer::local(head_text.as_deref().unwrap_or(""), cx); + buffer.set_capability(Capability::ReadOnly, cx); + buffer + }); + self.head_text_buffer = buffer.downgrade(); + buffer + } + + fn get_or_create_index_text_buffer(&mut self, cx: &mut Context) -> Entity { + if let Some(buffer) = self.index_text_buffer.upgrade() { + return buffer; + } + let index_text = self.index_text.clone(); + let buffer = cx.new(|cx| { + let mut buffer = Buffer::local(index_text.as_deref().unwrap_or(""), cx); + buffer.set_capability(Capability::ReadOnly, cx); + buffer + }); + self.index_text_buffer = buffer.downgrade(); + buffer + } + #[ztracing::instrument(skip_all)] fn buffer_language_changed(&mut self, buffer: Entity, cx: &mut Context) { self.language = buffer.read(cx).language().cloned(); @@ -3876,6 +4034,10 @@ impl BufferGitState { self.unstaged_diff.as_ref().and_then(|set| set.upgrade()) } + fn staged_diff(&self) -> Option> { + self.staged_diff.as_ref().and_then(|(set, _)| set.upgrade()) + } + fn uncommitted_diff(&self) -> Option> { self.uncommitted_diff.as_ref().and_then(|set| set.upgrade()) } @@ -3884,6 +4046,19 @@ impl BufferGitState { self.oid_diffs.get(&oid).and_then(|weak| weak.upgrade()) } + /// Whether the index text is known to match the committed text, without + /// comparing their contents. Always true when both texts were set by a + /// single `DiffBasesChange::SetBoth`, which shares one allocation between + /// them. May be false even when the contents are equal, if the texts were + /// loaded separately. + fn index_matches_head(&self) -> bool { + match (self.index_text.as_ref(), self.head_text.as_ref()) { + (Some(index), Some(head)) => Arc::ptr_eq(index, head), + (None, None) => true, + _ => false, + } + } + fn handle_base_texts_updated( &mut self, buffer: text::BufferSnapshot, @@ -3981,25 +4156,35 @@ impl BufferGitState { let language = self.language.clone(); let language_registry = self.language_registry.clone(); let unstaged_diff = self.unstaged_diff(); + let staged_diff = self.staged_diff(); let uncommitted_diff = self.uncommitted_diff(); let head = self.head_text.clone(); let index = self.index_text.clone(); + let head_text_buffer = self.head_text_buffer.upgrade(); + let index_text_buffer = self.index_text_buffer.upgrade(); + let index_text_buffer_language_enabled = self.index_text_buffer_language_enabled; let index_changed = self.index_changed; let head_changed = self.head_changed; let language_changed = self.language_changed; let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write; - let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) { - (Some(index), Some(head)) => Arc::ptr_eq(index, head), - (None, None) => true, - _ => false, - }; + let index_matches_head = self.index_matches_head(); - let oid_diffs: Vec<(Option, Entity, Option>)> = self + let oid_diffs: Vec<( + Option, + Entity, + Entity, + Option>, + )> = self .oid_diffs .iter() .filter_map(|(oid, weak)| { - let base_text = oid.and_then(|oid| self.oid_texts.get(&oid).cloned()); - weak.upgrade().map(|diff| (*oid, diff, base_text)) + let diff = weak.upgrade()?; + let base_text_buffer = diff.read(cx).base_text_buffer().clone(); + let base_text = match oid { + Some(oid) => Some(self.oid_texts.get(oid)?.clone()), + None => None, + }; + Some((*oid, diff, base_text_buffer, base_text)) }) .collect(); @@ -4012,21 +4197,86 @@ impl BufferGitState { } alive }); + if self + .staged_diff + .as_ref() + .is_some_and(|(weak, _)| !weak.is_upgradable()) + { + self.staged_diff = None; + } self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| { log::debug!( "start recalculating diffs for buffer {}", buffer.remote_id() ); + if index_text_buffer_language_enabled + && let Some(index_text_buffer) = &index_text_buffer + { + index_text_buffer.update(cx, |index_text_buffer, cx| { + if let Some(language_registry) = language_registry.clone() { + index_text_buffer.set_language_registry(language_registry); + } + index_text_buffer.set_language_async(language.clone(), cx); + }); + } + if let Some(head_text_buffer) = &head_text_buffer { + head_text_buffer.update(cx, |head_text_buffer, cx| { + if let Some(language_registry) = language_registry.clone() { + head_text_buffer.set_language_registry(language_registry); + } + head_text_buffer.set_language_async(language.clone(), cx); + }); + } + + for (_, _, base_text_buffer, _) in &oid_diffs { + base_text_buffer.update(cx, |base_text_buffer, cx| { + if let Some(language_registry) = language_registry.clone() { + base_text_buffer.set_language_registry(language_registry); + } + base_text_buffer.set_language_async(language.clone(), cx); + }); + } + + let mut edited_index_text = None; + + let index_text_snapshot = if let Some(index_text_buffer) = &index_text_buffer + && (unstaged_diff.is_some() || staged_diff.is_some()) + { + let index_text_snapshot = if index_changed || language_changed { + let new_index_text = index.clone().unwrap_or_default(); + let index_text_diff = index_text_buffer + .update(cx, |index_text_buffer, cx| { + index_text_buffer.diff(new_index_text.clone(), cx) + }) + .await; + let edited = index_text_buffer + .update(cx, |index_text_buffer, cx| { + index_text_buffer.snapshot_with_edits(index_text_diff.edits, cx) + }) + .await; + let snapshot = edited.snapshot().clone(); + edited_index_text = Some(edited); + snapshot + } else { + index_text_buffer.read_with(cx, |buffer, _| buffer.snapshot()) + }; + Some(index_text_snapshot) + } else { + None + }; + let mut new_unstaged_diff = None; - if let Some(unstaged_diff) = &unstaged_diff { + + if let (Some(unstaged_diff), Some(index_text_snapshot)) = + (unstaged_diff.as_ref(), index_text_snapshot.as_ref()) + { new_unstaged_diff = Some( cx.update(|cx| { unstaged_diff.read(cx).update_diff( buffer.clone(), - index, - index_changed.then_some(false), - language.clone(), + index_text_snapshot, + index.clone(), cx, ) }) @@ -4038,23 +4288,72 @@ impl BufferGitState { // for a bit yield_now().await; + let mut edited_head_text = None; + let mut new_staged_diff = None; let mut new_uncommitted_diff = None; - if let Some(uncommitted_diff) = &uncommitted_diff { - new_uncommitted_diff = if index_matches_head { - new_unstaged_diff.clone() + if let Some(head_text_buffer) = &head_text_buffer + && (staged_diff.is_some() || uncommitted_diff.is_some()) + { + let head_base_text_exists = head.is_some(); + let head_text_snapshot = if head_changed || language_changed { + let new_head_text = head.clone().unwrap_or_default(); + let head_text_diff = head_text_buffer + .update(cx, |head_text_buffer, cx| { + head_text_buffer.diff(new_head_text.clone(), cx) + }) + .await; + let edited = head_text_buffer + .update(cx, |base_text_buffer, cx| { + base_text_buffer.snapshot_with_edits(head_text_diff.edits, cx) + }) + .await; + let snapshot = edited.snapshot().clone(); + edited_head_text = Some(edited); + snapshot } else { - Some( + head_text_buffer.read_with(cx, |buffer, _| buffer.snapshot()) + }; + if let (Some(staged_diff), Some(index_base_text_snapshot)) = + (staged_diff.as_ref(), index_text_snapshot.as_ref()) + { + new_staged_diff = Some( cx.update(|cx| { - uncommitted_diff.read(cx).update_diff( - buffer.clone(), - head, - head_changed.then_some(true), - language.clone(), + staged_diff.read(cx).update_diff( + index_base_text_snapshot.text.clone(), + &head_text_snapshot, + head.clone(), cx, ) }) .await, - ) + ); + } + + if let Some(uncommitted_diff) = &uncommitted_diff { + new_uncommitted_diff = if index_matches_head { + new_unstaged_diff.clone().map(|mut update| { + update.set_base_text_snapshot( + head_text_snapshot.clone(), + head_base_text_exists, + ); + update + }) + } else { + None + }; + if new_uncommitted_diff.is_none() { + new_uncommitted_diff = Some( + cx.update(|cx| { + uncommitted_diff.read(cx).update_diff( + buffer.clone(), + &head_text_snapshot, + head.clone(), + cx, + ) + }) + .await, + ); + } } } @@ -4087,64 +4386,84 @@ impl BufferGitState { return Ok(()); } - let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) = - unstaged_diff.as_ref().zip(new_unstaged_diff.clone()) - { - let task = unstaged_diff.update(cx, |diff, cx| { - // For git index buffer we skip assigning the language as we do not really need to perform any syntax highlighting on - // it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff - // view multibuffers. - diff.set_snapshot(new_unstaged_diff, &buffer, cx) - }); - Some(task.await) - } else { - None - }; + this.update(cx, |_, cx| { + if let (Some(staged_diff), Some(new_staged_diff)) = + (staged_diff.as_ref(), new_staged_diff.clone()) + { + staged_diff.update(cx, |diff, cx| { + if let Some(edited_base_text) = edited_index_text.take() + && let Some(index_text_buffer) = &index_text_buffer + { + index_text_buffer.update(cx, |index_text_buffer, cx| { + index_text_buffer.fast_forward(edited_base_text, cx) + }); + } + if let Some(edited_head_text) = edited_head_text.take() + && let Some(head_text_buffer) = &head_text_buffer + { + head_text_buffer.update(cx, |head_text_buffer, cx| { + head_text_buffer.fast_forward(edited_head_text, cx) + }); + } + diff.set_snapshot(new_staged_diff, cx) + }); + } - yield_now().await; + let unstaged_changed_range = if let (Some(unstaged_diff), Some(new_unstaged_diff)) = + (unstaged_diff.as_ref(), new_unstaged_diff.clone()) + { + Some(unstaged_diff.update(cx, |diff, cx| { + if let Some(edited_index_text) = edited_index_text.take() + && let Some(index_text_buffer) = &index_text_buffer + { + index_text_buffer.update(cx, |index_text_buffer, cx| { + index_text_buffer.fast_forward(edited_index_text, cx) + }); + } + diff.set_snapshot(new_unstaged_diff, cx) + })) + } else { + None + }; - if let Some((uncommitted_diff, new_uncommitted_diff)) = - uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone()) - { - uncommitted_diff - .update(cx, |diff, cx| { - if language_changed { - diff.language_changed(language.clone(), language_registry.clone(), cx); + if let (Some(uncommitted_diff), Some(new_uncommitted_diff)) = + (uncommitted_diff.as_ref(), new_uncommitted_diff.clone()) + { + uncommitted_diff.update(cx, |diff, cx| { + if let Some(edited_base_text) = edited_head_text.take() + && let Some(head_text_buffer) = &head_text_buffer + { + head_text_buffer.update(cx, |head_text_buffer, cx| { + head_text_buffer.fast_forward(edited_base_text, cx) + }); } diff.set_snapshot_with_secondary( new_uncommitted_diff, - &buffer, unstaged_changed_range.flatten(), true, cx, ) - }) - .await; - } + }); + } + })?; yield_now().await; - for (oid, oid_diff, base_text) in oid_diffs { + for (oid, oid_diff, base_text_buffer, base_text) in oid_diffs { + let base_text_snapshot = + base_text_buffer.read_with(cx, |buffer, _| buffer.snapshot()); let new_oid_diff = cx .update(|cx| { oid_diff.read(cx).update_diff( buffer.clone(), - base_text, - None, - language.clone(), + &base_text_snapshot, + base_text.clone(), cx, ) }) .await; - oid_diff - .update(cx, |diff, cx| { - if language_changed { - diff.language_changed(language.clone(), language_registry.clone(), cx); - } - diff.set_snapshot(new_oid_diff, &buffer, cx) - }) - .await; + oid_diff.update(cx, |diff, cx| diff.set_snapshot(new_oid_diff, cx)); log::debug!( "finished recalculating oid diff for buffer {} oid {:?}", @@ -4805,6 +5124,10 @@ impl Repository { .unstaged_diff .as_ref() .is_some_and(|diff| diff.is_upgradable()); + let has_staged_diff = diff_state + .staged_diff + .as_ref() + .is_some_and(|(diff, _)| diff.is_upgradable()); let has_uncommitted_diff = diff_state .uncommitted_diff .as_ref() @@ -4814,8 +5137,10 @@ impl Repository { buffer, repo_path, is_symlink, - has_unstaged_diff.then(|| diff_state.index_text.clone()), - has_uncommitted_diff.then(|| diff_state.head_text.clone()), + (has_unstaged_diff || has_staged_diff) + .then(|| diff_state.index_text.clone()), + (has_staged_diff || has_uncommitted_diff) + .then(|| diff_state.head_text.clone()), )) }) }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d03b341a8a..061f8265ab 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3219,6 +3219,19 @@ impl Project { .update(cx, |git_store, cx| git_store.open_unstaged_diff(buffer, cx)) } + #[ztracing::instrument(skip_all)] + pub fn open_staged_diff( + &mut self, + buffer: Entity, + cx: &mut Context, + ) -> Task>> { + if self.is_disconnected(cx) { + return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); + } + self.git_store + .update(cx, |git_store, cx| git_store.open_staged_diff(buffer, cx)) + } + #[ztracing::instrument(skip_all)] pub fn open_uncommitted_diff( &mut self, diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index af276a0aa4..674a2ace91 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -9198,6 +9198,412 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_reopening_unstaged_diff_after_drop(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let staged_contents = r#" + fn main() { + println!("hello world"); + } + "# + .unindent(); + let file_contents = r#" + // print goodbye + fn main() { + println!("goodbye world"); + } + "# + .unindent(); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/src/main.rs", cx) + }) + .await + .unwrap(); + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + + let unstaged_diff = project + .update(cx, |project, cx| { + project.open_unstaged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + + // Drop the diff while the buffer (and its git state) stays alive. + drop(unstaged_diff); + cx.run_until_parked(); + project.read_with(cx, |project, cx| { + assert!( + project + .git_store() + .read(cx) + .get_unstaged_diff(buffer_id, cx) + .is_none(), + "unstaged diff should have been released" + ); + }); + + // Reopen the diff. The new entity must be registered in the git store, + // and its hunks must be recalculated. + let unstaged_diff = project + .update(cx, |project, cx| { + project.open_unstaged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + + project.read_with(cx, |project, cx| { + let registered = project + .git_store() + .read(cx) + .get_unstaged_diff(buffer_id, cx); + assert_eq!( + registered.as_ref(), + Some(&unstaged_diff), + "reopened unstaged diff should be registered in the git store" + ); + }); + + unstaged_diff.update(cx, |unstaged_diff, cx| { + let snapshot = buffer.read(cx).snapshot(); + assert_hunks( + unstaged_diff.snapshot(cx).hunks(&snapshot), + &snapshot, + &unstaged_diff.base_text_string(cx).unwrap(), + &[ + (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()), + ( + 2..3, + " println!(\"hello world\");\n", + " println!(\"goodbye world\");\n", + DiffHunkStatus::modified_none(), + ), + ], + ); + }); +} + +#[gpui::test] +async fn test_staged_diff_for_buffer(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let committed_contents = r#" + fn main() { + println!("hello world"); + } + "# + .unindent(); + let staged_contents = r#" + // print goodbye + fn main() { + println!("goodbye world"); + } + "# + .unindent(); + let file_contents = r#" + // print goodbye + fn main() { + println!("working copy only"); + } + "# + .unindent(); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + fs.set_head_for_repo( + Path::new("/dir/.git"), + &[("src/main.rs", committed_contents)], + "deadbeef", + ); + fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let language = rust_lang(); + language_registry.add(language.clone()); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/src/main.rs", cx) + }) + .await + .unwrap(); + let unstaged_diff = project + .update(cx, |project, cx| { + project.open_unstaged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + unstaged_diff.read_with(cx, |diff, cx| { + assert_eq!(diff.base_text(cx).language().cloned(), None); + }); + + let staged_diff = project + .update(cx, |project, cx| { + project.open_staged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + + cx.run_until_parked(); + unstaged_diff.read_with(cx, |diff, cx| { + assert_eq!( + diff.base_text(cx).language().cloned(), + Some(language.clone()) + ); + }); + staged_diff.update(cx, |staged_diff, cx| { + let snapshot = staged_diff.snapshot(cx); + let buffer_snapshot = snapshot.buffer_snapshot(); + assert_hunks( + snapshot.hunks(buffer_snapshot), + buffer_snapshot, + &staged_diff.base_text_string(cx).unwrap(), + &[ + (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()), + ( + 2..3, + " println!(\"hello world\");\n", + " println!(\"goodbye world\");\n", + DiffHunkStatus::modified_none(), + ), + ], + ); + }); + + let staged_contents = r#" + // print goodbye + fn main() { + } + "# + .unindent(); + fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]); + + cx.run_until_parked(); + staged_diff.update(cx, |staged_diff, cx| { + let snapshot = staged_diff.snapshot(cx); + let buffer_snapshot = snapshot.buffer_snapshot(); + assert_hunks( + snapshot.hunks(buffer_snapshot), + buffer_snapshot, + &staged_diff.base_text_string(cx).unwrap(), + &[ + (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()), + ( + 2..2, + " println!(\"hello world\");\n", + "", + DiffHunkStatus::deleted_none(), + ), + ], + ); + }); +} + +#[gpui::test] +async fn test_base_text_buffers_released_when_diffs_dropped(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let committed_contents = "one\ntwo\nthree\n"; + let staged_contents = "one\nTWO\nthree\n"; + let file_contents = "one\nTWO\nTHREE\n"; + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + fs.set_head_for_repo( + Path::new("/dir/.git"), + &[("src/main.rs", committed_contents.to_owned())], + "deadbeef", + ); + fs.set_index_for_repo( + Path::new("/dir/.git"), + &[("src/main.rs", staged_contents.to_owned())], + ); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/src/main.rs", cx) + }) + .await + .unwrap(); + + let uncommitted_diff = project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + let unstaged_diff = project + .update(cx, |project, cx| { + project.open_unstaged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + + let weak_head_text_buffer = + uncommitted_diff.read_with(cx, |diff, _| diff.base_text_buffer().downgrade()); + let weak_index_text_buffer = + unstaged_diff.read_with(cx, |diff, _| diff.base_text_buffer().downgrade()); + + drop(uncommitted_diff); + cx.run_until_parked(); + cx.update(|_| {}); + weak_head_text_buffer.assert_released(); + assert!( + weak_index_text_buffer.upgrade().is_some(), + "index text buffer should stay alive while the unstaged diff is open" + ); + + drop(unstaged_diff); + cx.run_until_parked(); + cx.update(|_| {}); + weak_index_text_buffer.assert_released(); + + let uncommitted_diff = project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + uncommitted_diff.update(cx, |uncommitted_diff, cx| { + assert_eq!( + uncommitted_diff.base_text_string(cx).as_deref(), + Some(committed_contents), + ); + let snapshot = buffer.read(cx).snapshot(); + assert_hunks( + uncommitted_diff.snapshot(cx).hunks(&snapshot), + &snapshot, + &uncommitted_diff.base_text_string(cx).unwrap(), + &[( + 1..3, + "two\nthree\n", + "TWO\nTHREE\n", + DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk), + )], + ); + }); +} + +#[gpui::test] +async fn test_staged_diff_without_unstaged_diff(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let committed_contents = "one\ntwo\nthree\n"; + let staged_contents = "one\nTWO\nthree\n"; + let file_contents = "one\nTWO\nthree\n"; + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + fs.set_head_for_repo( + Path::new("/dir/.git"), + &[("src/main.rs", committed_contents.to_owned())], + "deadbeef", + ); + fs.set_index_for_repo( + Path::new("/dir/.git"), + &[("src/main.rs", staged_contents.to_owned())], + ); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/src/main.rs", cx) + }) + .await + .unwrap(); + + let staged_diff = project + .update(cx, |project, cx| { + project.open_staged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + + staged_diff.update(cx, |staged_diff, cx| { + let snapshot = staged_diff.snapshot(cx); + let buffer_snapshot = snapshot.buffer_snapshot(); + assert_hunks( + snapshot.hunks(buffer_snapshot), + buffer_snapshot, + &staged_diff.base_text_string(cx).unwrap(), + &[(1..2, "two\n", "TWO\n", DiffHunkStatus::modified_none())], + ); + }); + + fs.set_index_for_repo( + Path::new("/dir/.git"), + &[("src/main.rs", "one\nTWO\nTHREE\n".to_owned())], + ); + cx.run_until_parked(); + + staged_diff.update(cx, |staged_diff, cx| { + let snapshot = staged_diff.snapshot(cx); + let buffer_snapshot = snapshot.buffer_snapshot(); + assert_hunks( + snapshot.hunks(buffer_snapshot), + buffer_snapshot, + &staged_diff.base_text_string(cx).unwrap(), + &[( + 1..3, + "two\nthree\n", + "TWO\nTHREE\n", + DiffHunkStatus::modified_none(), + )], + ); + }); +} + #[gpui::test] async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -9521,6 +9927,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { changed_range: Some(changed_range), base_text_changed_range: _, extended_range: _, + base_text_changed: _, }) = event { let changed_range = changed_range.to_point(&snapshot); @@ -9565,10 +9972,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { changed_range: Some(changed_range), base_text_changed_range: _, extended_range: _, + base_text_changed: _, }) = event { let changed_range = changed_range.to_point(&snapshot); - assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0)); + assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0)); } else { panic!("Unexpected event {event:?}"); } @@ -9624,6 +10032,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { changed_range: Some(changed_range), base_text_changed_range: _, extended_range: _, + base_text_changed: _, }) = event { let changed_range = changed_range.to_point(&snapshot); @@ -9667,6 +10076,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { changed_range: Some(changed_range), base_text_changed_range: _, extended_range: _, + base_text_changed: _, }) = event { let changed_range = changed_range.to_point(&snapshot); @@ -9776,13 +10186,32 @@ async fn test_uncommitted_diff_opened_before_unstaged_diff(cx: &mut gpui::TestAp let unstaged_diff_task = project.update(cx, |project, cx| { project.open_unstaged_diff(buffer.clone(), cx) }); - let (uncommitted_diff, _unstaged_diff) = + let (uncommitted_diff, unstaged_diff) = futures::future::join(uncommitted_diff_task, unstaged_diff_task).await; let uncommitted_diff = uncommitted_diff.unwrap(); - let _unstaged_diff = _unstaged_diff.unwrap(); + let unstaged_diff = unstaged_diff.unwrap(); cx.run_until_parked(); + uncommitted_diff.read_with(cx, |diff, _| { + assert_eq!( + diff.secondary_diff(), + Some(unstaged_diff.clone()), + "the unstaged diff returned to callers should be the uncommitted diff's secondary" + ); + }); + project.read_with(cx, |project, cx| { + let buffer_id = buffer.read(cx).remote_id(); + assert_eq!( + project + .git_store() + .read(cx) + .get_unstaged_diff(buffer_id, cx), + Some(unstaged_diff.clone()), + "the unstaged diff returned to callers should be the registered one" + ); + }); + uncommitted_diff.read_with(cx, |diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 66f93e781f..b83875dbdb 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -894,163 +894,11 @@ impl Buffer { edits: impl ExactSizeIterator, T)>, timestamp: clock::Lamport, ) -> EditOperation { - let mut edits_patch = Patch::default(); - let mut edit_op = EditOperation { - timestamp, - version: self.version(), - ranges: Vec::with_capacity(edits.len()), - new_text: Vec::with_capacity(edits.len()), - }; - let mut new_insertions = Vec::new(); - let mut insertion_offset: u32 = 0; - let mut insertion_slices = Vec::new(); - - let mut edits = edits - .map(|(range, new_text)| (range.to_offset(&*self), new_text)) - .peekable(); - - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self.fragments.cursor::(&None); - let mut new_fragments = - FragmentBuilder::new(old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right)); - new_ropes.append(new_fragments.summary().text); - - let mut fragment_start = old_fragments.start().visible; - for (range, new_text) in edits { - let new_text = LineEnding::normalize_arc(new_text.into()); - let fragment_end = old_fragments.end().visible; - - // If the current fragment ends before this range, then jump ahead to the first fragment - // that extends past the start of this range, reusing any intervening fragments. - if fragment_end < range.start { - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().visible { - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = (fragment_end - fragment_start) as u32; - suffix.insertion_offset += - (fragment_start - old_fragments.start().visible) as u32; - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let slice = old_fragments.slice(&range.start, Bias::Right); - new_ropes.append(slice.summary().text); - new_fragments.append(slice, &None); - fragment_start = old_fragments.start().visible; - } - - let full_range_start = FullOffset(range.start + old_fragments.start().deleted); - - // Preserve any portion of the current fragment that precedes this range. - if fragment_start < range.start { - let mut prefix = old_fragments.item().unwrap().clone(); - prefix.len = (range.start - fragment_start) as u32; - prefix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32; - prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); - new_insertions.push(InsertionFragment::insert_new(&prefix)); - new_ropes.push_fragment(&prefix, prefix.visible); - new_fragments.push(prefix, &None); - fragment_start = range.start; - } - - // Insert the new text before any existing fragments within the range. - if !new_text.is_empty() { - let new_start = new_fragments.summary().text.visible; - - let next_fragment_id = old_fragments - .item() - .map_or(Locator::max_ref(), |old_fragment| &old_fragment.id); - Self::push_fragments_for_insertion( - new_text.as_ref(), - timestamp, - &mut insertion_offset, - &mut new_fragments, - &mut new_insertions, - &mut insertion_slices, - &mut new_ropes, - next_fragment_id, - timestamp, - ); - edits_patch.push(Edit { - old: fragment_start..fragment_start, - new: new_start..new_start + new_text.len(), - }); - } - - // Advance through every fragment that intersects this range, marking the intersecting - // portions as deleted. - while fragment_start < range.end { - let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end().visible; - let mut intersection = fragment.clone(); - let intersection_end = cmp::min(range.end, fragment_end); - if fragment.visible { - intersection.len = (intersection_end - fragment_start) as u32; - intersection.insertion_offset += - (fragment_start - old_fragments.start().visible) as u32; - intersection.id = - Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.push(timestamp); - intersection.visible = false; - } - if intersection.len > 0 { - if fragment.visible && !intersection.visible { - let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: fragment_start..intersection_end, - new: new_start..new_start, - }); - insertion_slices - .push(InsertionSlice::from_fragment(timestamp, &intersection)); - } - new_insertions.push(InsertionFragment::insert_new(&intersection)); - new_ropes.push_fragment(&intersection, fragment.visible); - new_fragments.push(intersection, &None); - fragment_start = intersection_end; - } - if fragment_end <= range.end { - old_fragments.next(); - } - } - - let full_range_end = FullOffset(range.end + old_fragments.start().deleted); - edit_op.ranges.push(full_range_start..full_range_end); - edit_op.new_text.push(new_text); - } - - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().visible { - let fragment_end = old_fragments.end().visible; - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = (fragment_end - fragment_start) as u32; - suffix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32; - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - let (visible_text, deleted_text) = new_ropes.finish(); - drop(old_fragments); - - self.snapshot.fragments = new_fragments.to_sum_tree(&None); - self.snapshot.insertions.edit(new_insertions, ()); - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; + let edits: Vec<_> = edits + .map(|(range, new_text)| (range.to_offset(&*self), new_text.into())) + .collect(); + let (edit_op, edits_patch) = self.snapshot.apply_edit_internal(edits, timestamp); self.subscriptions.publish_mut(&edits_patch); - self.snapshot.insertion_slices.extend(insertion_slices); edit_op } @@ -1213,7 +1061,7 @@ impl Buffer { let next_fragment_id = old_fragments .item() .map_or(Locator::max_ref(), |old_fragment| &old_fragment.id); - Self::push_fragments_for_insertion( + push_fragments_for_insertion( new_text, timestamp, &mut insertion_offset, @@ -1301,49 +1149,6 @@ impl Buffer { self.subscriptions.publish_mut(&edits_patch) } - fn push_fragments_for_insertion( - new_text: &str, - timestamp: clock::Lamport, - insertion_offset: &mut u32, - new_fragments: &mut FragmentBuilder, - new_insertions: &mut Vec>, - insertion_slices: &mut Vec, - new_ropes: &mut RopeBuilder, - next_fragment_id: &Locator, - edit_timestamp: clock::Lamport, - ) { - let mut text_offset = 0; - while text_offset < new_text.len() { - let target_end = new_text.len().min(text_offset + MAX_INSERTION_LEN); - let chunk_end = if target_end == new_text.len() { - target_end - } else { - new_text.floor_char_boundary(target_end) - }; - if chunk_end == text_offset { - break; - } - let chunk_len = chunk_end - text_offset; - - let fragment = Fragment { - id: Locator::between(&new_fragments.summary().max_id, next_fragment_id), - timestamp, - insertion_offset: *insertion_offset, - len: chunk_len as u32, - deletions: Default::default(), - max_undos: Default::default(), - visible: true, - }; - insertion_slices.push(InsertionSlice::from_fragment(edit_timestamp, &fragment)); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_fragments.push(fragment, &None); - - *insertion_offset += chunk_len as u32; - text_offset = chunk_end; - } - new_ropes.push_str(new_text); - } - fn fragment_ids_for_edits<'a>( &'a self, edit_ids: impl Iterator, @@ -1832,6 +1637,51 @@ impl Buffer { pub fn set_group_interval(&mut self, group_interval: Duration) { self.history.group_interval = group_interval; } + + pub fn snapshot_with_edits(&mut self, edits: I) -> EditedBufferSnapshot + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + let mut snapshot = self.snapshot.clone(); + let base_version = self.version(); + let edits: Vec<_> = edits + .into_iter() + .map(|(range, new_text)| (range.to_offset(&snapshot), new_text.into())) + .collect(); + if edits.is_empty() { + return EditedBufferSnapshot { + base_version, + snapshot, + did_edit: false, + }; + } + let timestamp = self.lamport_clock.tick(); + snapshot.apply_edit_internal(edits, timestamp); + snapshot.version.observe(timestamp); + EditedBufferSnapshot { + base_version, + snapshot, + did_edit: true, + } + } + + pub fn fast_forward(&mut self, edited: EditedBufferSnapshot) { + if self.version.changed_since(&edited.base_version) { + panic!("buffer cannot be fast-forwarded") + } + self.snapshot = edited.snapshot.clone(); + for timestamp in edited.snapshot.version.iter() { + self.lamport_clock.observe(timestamp); + } + } +} + +pub struct EditedBufferSnapshot { + pub base_version: clock::Global, + pub snapshot: BufferSnapshot, + pub did_edit: bool, } #[cfg(any(test, feature = "test-support"))] @@ -2011,6 +1861,49 @@ impl Buffer { } } +fn push_fragments_for_insertion( + new_text: &str, + timestamp: clock::Lamport, + insertion_offset: &mut u32, + new_fragments: &mut FragmentBuilder, + new_insertions: &mut Vec>, + insertion_slices: &mut Vec, + new_ropes: &mut RopeBuilder, + next_fragment_id: &Locator, + edit_timestamp: clock::Lamport, +) { + let mut text_offset = 0; + while text_offset < new_text.len() { + let target_end = new_text.len().min(text_offset + MAX_INSERTION_LEN); + let chunk_end = if target_end == new_text.len() { + target_end + } else { + new_text.floor_char_boundary(target_end) + }; + if chunk_end == text_offset { + break; + } + let chunk_len = chunk_end - text_offset; + + let fragment = Fragment { + id: Locator::between(&new_fragments.summary().max_id, next_fragment_id), + timestamp, + insertion_offset: *insertion_offset, + len: chunk_len as u32, + deletions: Default::default(), + max_undos: Default::default(), + visible: true, + }; + insertion_slices.push(InsertionSlice::from_fragment(edit_timestamp, &fragment)); + new_insertions.push(InsertionFragment::insert_new(&fragment)); + new_fragments.push(fragment, &None); + + *insertion_offset += chunk_len as u32; + text_offset = chunk_end; + } + new_ropes.push_str(new_text); +} + impl Deref for Buffer { type Target = BufferSnapshot; @@ -2020,6 +1913,162 @@ impl Deref for Buffer { } impl BufferSnapshot { + fn apply_edit_internal( + &mut self, + edits: Vec<(Range, Arc)>, + timestamp: clock::Lamport, + ) -> (EditOperation, Patch) { + let mut edits_patch = Patch::default(); + let mut edit_op = EditOperation { + timestamp, + version: self.version.clone(), + ranges: Vec::with_capacity(edits.len()), + new_text: Vec::with_capacity(edits.len()), + }; + let mut new_insertions = Vec::new(); + let mut insertion_offset: u32 = 0; + let mut insertion_slices = Vec::new(); + + let mut edits = edits.into_iter().peekable(); + + if edits.peek().is_none() { + return (edit_op, edits_patch); + } + + let mut new_ropes = + RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); + let mut old_fragments = self.fragments.cursor::(&None); + let mut new_fragments = + FragmentBuilder::new(old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right)); + new_ropes.append(new_fragments.summary().text); + + let mut fragment_start = old_fragments.start().visible; + for (range, new_text) in edits { + let new_text: Arc = LineEnding::normalize_arc(new_text); + let fragment_end = old_fragments.end().visible; + + if fragment_end < range.start { + if fragment_start > old_fragments.start().visible { + if fragment_end > fragment_start { + let mut suffix = old_fragments.item().unwrap().clone(); + suffix.len = (fragment_end - fragment_start) as u32; + suffix.insertion_offset += + (fragment_start - old_fragments.start().visible) as u32; + new_insertions.push(InsertionFragment::insert_new(&suffix)); + new_ropes.push_fragment(&suffix, suffix.visible); + new_fragments.push(suffix, &None); + } + old_fragments.next(); + } + + let slice = old_fragments.slice(&range.start, Bias::Right); + new_ropes.append(slice.summary().text); + new_fragments.append(slice, &None); + fragment_start = old_fragments.start().visible; + } + + let full_range_start = FullOffset(range.start + old_fragments.start().deleted); + + if fragment_start < range.start { + let mut prefix = old_fragments.item().unwrap().clone(); + prefix.len = (range.start - fragment_start) as u32; + prefix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32; + prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); + new_insertions.push(InsertionFragment::insert_new(&prefix)); + new_ropes.push_fragment(&prefix, prefix.visible); + new_fragments.push(prefix, &None); + fragment_start = range.start; + } + + if !new_text.is_empty() { + let new_start = new_fragments.summary().text.visible; + + let next_fragment_id = old_fragments + .item() + .map_or(Locator::max_ref(), |old_fragment| &old_fragment.id); + push_fragments_for_insertion( + new_text.as_ref(), + timestamp, + &mut insertion_offset, + &mut new_fragments, + &mut new_insertions, + &mut insertion_slices, + &mut new_ropes, + next_fragment_id, + timestamp, + ); + edits_patch.push(Edit { + old: fragment_start..fragment_start, + new: new_start..new_start + new_text.len(), + }); + } + + while fragment_start < range.end { + let fragment = old_fragments.item().unwrap(); + let fragment_end = old_fragments.end().visible; + let mut intersection = fragment.clone(); + let intersection_end = cmp::min(range.end, fragment_end); + if fragment.visible { + intersection.len = (intersection_end - fragment_start) as u32; + intersection.insertion_offset += + (fragment_start - old_fragments.start().visible) as u32; + intersection.id = + Locator::between(&new_fragments.summary().max_id, &intersection.id); + intersection.deletions.push(timestamp); + intersection.visible = false; + } + if intersection.len > 0 { + if fragment.visible && !intersection.visible { + let new_start = new_fragments.summary().text.visible; + edits_patch.push(Edit { + old: fragment_start..intersection_end, + new: new_start..new_start, + }); + insertion_slices + .push(InsertionSlice::from_fragment(timestamp, &intersection)); + } + new_insertions.push(InsertionFragment::insert_new(&intersection)); + new_ropes.push_fragment(&intersection, fragment.visible); + new_fragments.push(intersection, &None); + fragment_start = intersection_end; + } + if fragment_end <= range.end { + old_fragments.next(); + } + } + + let full_range_end = FullOffset(range.end + old_fragments.start().deleted); + edit_op.ranges.push(full_range_start..full_range_end); + edit_op.new_text.push(new_text); + } + + if fragment_start > old_fragments.start().visible { + let fragment_end = old_fragments.end().visible; + if fragment_end > fragment_start { + let mut suffix = old_fragments.item().unwrap().clone(); + suffix.len = (fragment_end - fragment_start) as u32; + suffix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32; + new_insertions.push(InsertionFragment::insert_new(&suffix)); + new_ropes.push_fragment(&suffix, suffix.visible); + new_fragments.push(suffix, &None); + } + old_fragments.next(); + } + + let suffix = old_fragments.suffix(); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); + let (visible_text, deleted_text) = new_ropes.finish(); + drop(old_fragments); + + self.fragments = new_fragments.to_sum_tree(&None); + self.insertions.edit(new_insertions, ()); + self.visible_text = visible_text; + self.deleted_text = deleted_text; + self.insertion_slices.extend(insertion_slices); + (edit_op, edits_patch) + } + pub fn as_rope(&self) -> &Rope { &self.visible_text }