diff --git a/Cargo.lock b/Cargo.lock index 9fb85c94da195f..201875263c8f09 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 a6d3b86db7c980..d297b5fa98f513 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 99cc0a2d79bfb1..f8b15f621e9d6b 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 9a820db7b20806..a4ae164c51ff48 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 48747f9cb33662..68627d1ec871e4 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 d574b0dd3cfdb0..4bcdd3422ec99e 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 cc89f8c314d0f3..c300ace11ae1d9 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_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, cx: &mut Context) -> Self { + 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 7fc56a3c86c6f5..dac33f9855b303 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 782fc93f9961c7..deae41c21ef366 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 9c7a2594950cc0..1dcb21808091b5 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 49eb44ead50366..48a074b25cc1b6 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 8994021ea51802..e3d634b4a0a459 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 16b1bc4daee2da..dbe4f073e50f23 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 d6380c04cf5926..2c3bd5f8dba18c 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 353deb47ed5c76..9fff74d3983897 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 ac9e4175b65469..477f18be545dbc 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 a3b55fabe9a74c..f8097e68f5c8d8 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 5627c40a4ca18f..cd929c22e670f0 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 aae52ffe438e97..5312ae6d08814d 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 3f02305a4ac2ee..57612012c84c84 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 1641c460eb2adf..dd0c24fb5f8705 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 3e71deb8f85ffc..8e52eac1cb17dc 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 70266e2138c431..6ec98f522ec9f6 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 oid_diffs: Vec<(Option, Entity, Option>)> = self + let index_matches_head = self.index_matches_head(); + + 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 d03b341a8ac56d..061f8265ab3ee1 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 af276a0aa40ead..674a2ace91087d 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 66f93e781f7878..b83875dbdb15e2 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 }