diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 78c8ba6920337cb7ede530b7f10cffdadaa1f3cc..23448b43a7cc72cdc3f561383b54101e0bb6acd9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -98,7 +98,9 @@ use language::{ }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; -use proposed_changes_editor::{ProposedChangesBuffer, ProposedChangesEditor}; +pub use proposed_changes_editor::{ + ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar, +}; use similar::{ChangeTag, TextDiff}; use task::{ResolvedTask, TaskTemplate, TaskVariables}; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 3979e558a423648e8f0e90f381db077c9a51f345..ec0c05d88382c0a898658716ff78875672f33156 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -6,10 +6,13 @@ use language::{Buffer, BufferEvent, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; use project::Project; use smol::stream::StreamExt; -use std::{ops::Range, time::Duration}; +use std::{any::TypeId, ops::Range, time::Duration}; use text::ToOffset; use ui::prelude::*; -use workspace::Item; +use workspace::{ + searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, + ToolbarItemView, +}; pub struct ProposedChangesEditor { editor: View, @@ -23,6 +26,10 @@ pub struct ProposedChangesBuffer { pub ranges: Vec>, } +pub struct ProposedChangesEditorToolbar { + current_editor: Option>, +} + impl ProposedChangesEditor { pub fn new( buffers: Vec>, @@ -96,6 +103,17 @@ impl ProposedChangesEditor { self.recalculate_diffs_tx.unbounded_send(buffer).ok(); } } + + fn apply_all_changes(&self, cx: &mut ViewContext) { + let buffers = self.editor.read(cx).buffer.read(cx).all_buffers(); + for branch_buffer in buffers { + if let Some(base_buffer) = branch_buffer.read(cx).diff_base_buffer() { + base_buffer.update(cx, |base_buffer, cx| { + base_buffer.merge(&branch_buffer, None, cx) + }); + } + } + } } impl Render for ProposedChangesEditor { @@ -122,4 +140,66 @@ impl Item for ProposedChangesEditor { fn tab_content_text(&self, _cx: &WindowContext) -> Option { Some("Proposed changes".into()) } + + fn as_searchable(&self, _: &View) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } +} + +impl ProposedChangesEditorToolbar { + pub fn new() -> Self { + Self { + current_editor: None, + } + } + + fn get_toolbar_item_location(&self) -> ToolbarItemLocation { + if self.current_editor.is_some() { + ToolbarItemLocation::PrimaryRight + } else { + ToolbarItemLocation::Hidden + } + } +} + +impl Render for ProposedChangesEditorToolbar { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + let editor = self.current_editor.clone(); + Button::new("apply-changes", "Apply All").on_click(move |_, cx| { + if let Some(editor) = &editor { + editor.update(cx, |editor, cx| { + editor.apply_all_changes(cx); + }); + } + }) + } +} + +impl EventEmitter for ProposedChangesEditorToolbar {} + +impl ToolbarItemView for ProposedChangesEditorToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn workspace::ItemHandle>, + _cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + self.current_editor = + active_pane_item.and_then(|item| item.downcast::()); + self.get_toolbar_item_location() + } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5735ee961651ab21436f7164cff67585cdc39fb1..7abc9b8dba146a1ce18590830ded98ffcc3d15e1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -87,7 +87,11 @@ pub type BufferRow = u32; #[derive(Clone)] enum BufferDiffBase { Git(Rope), - PastBufferVersion(Model, BufferSnapshot), + PastBufferVersion { + buffer: Model, + rope: Rope, + operations_to_ignore: Vec, + }, } /// An in-memory representation of a source code file, including its text, @@ -795,19 +799,15 @@ impl Buffer { let this = cx.handle(); cx.new_model(|cx| { let mut branch = Self { - diff_base: Some(BufferDiffBase::PastBufferVersion( - this.clone(), - self.snapshot(), - )), + diff_base: Some(BufferDiffBase::PastBufferVersion { + buffer: this.clone(), + rope: self.as_rope().clone(), + operations_to_ignore: Vec::new(), + }), language: self.language.clone(), has_conflict: self.has_conflict, has_unsaved_edits: Cell::new(self.has_unsaved_edits.get_mut().clone()), - _subscriptions: vec![cx.subscribe(&this, |branch: &mut Self, _, event, cx| { - if let BufferEvent::Operation { operation, .. } = event { - branch.apply_ops([operation.clone()], cx); - branch.diff_base_version += 1; - } - })], + _subscriptions: vec![cx.subscribe(&this, Self::on_base_buffer_event)], ..Self::build( self.text.branch(), None, @@ -823,18 +823,74 @@ impl Buffer { }) } - pub fn merge(&mut self, branch: &Model, cx: &mut ModelContext) { - let branch = branch.read(cx); - let edits = branch - .edits_since::(&self.version) - .map(|edit| { - ( - edit.old, - branch.text_for_range(edit.new).collect::(), + /// Applies all of the changes in `branch` buffer that intersect the given `range` + /// to this buffer. + pub fn merge( + &mut self, + branch: &Model, + range: Option>, + cx: &mut ModelContext, + ) { + let edits = branch.read_with(cx, |branch, _| { + branch + .edits_since_in_range::( + &self.version, + range.unwrap_or(Anchor::MIN..Anchor::MAX), ) - }) - .collect::>(); - self.edit(edits, None, cx); + .map(|edit| { + ( + edit.old, + branch.text_for_range(edit.new).collect::(), + ) + }) + .collect::>() + }); + let operation = self.edit(edits, None, cx); + + // Prevent this operation from being reapplied to the branch. + branch.update(cx, |branch, cx| { + if let Some(BufferDiffBase::PastBufferVersion { + operations_to_ignore, + .. + }) = &mut branch.diff_base + { + operations_to_ignore.extend(operation); + } + cx.emit(BufferEvent::Edited) + }); + } + + fn on_base_buffer_event( + &mut self, + _: Model, + event: &BufferEvent, + cx: &mut ModelContext, + ) { + if let BufferEvent::Operation { operation, .. } = event { + if let Some(BufferDiffBase::PastBufferVersion { + operations_to_ignore, + .. + }) = &mut self.diff_base + { + let mut is_ignored = false; + if let Operation::Buffer(text::Operation::Edit(buffer_operation)) = &operation { + operations_to_ignore.retain(|operation_to_ignore| { + match buffer_operation.timestamp.cmp(&operation_to_ignore) { + Ordering::Less => true, + Ordering::Equal => { + is_ignored = true; + false + } + Ordering::Greater => false, + } + }); + } + if !is_ignored { + self.apply_ops([operation.clone()], cx); + self.diff_base_version += 1; + } + } + } } #[cfg(test)] @@ -1017,9 +1073,8 @@ impl Buffer { /// Returns the current diff base, see [Buffer::set_diff_base]. pub fn diff_base(&self) -> Option<&Rope> { match self.diff_base.as_ref()? { - BufferDiffBase::Git(rope) => Some(rope), - BufferDiffBase::PastBufferVersion(_, buffer_snapshot) => { - Some(buffer_snapshot.as_rope()) + BufferDiffBase::Git(rope) | BufferDiffBase::PastBufferVersion { rope, .. } => { + Some(rope) } } } @@ -1050,29 +1105,36 @@ impl Buffer { self.diff_base_version } + pub fn diff_base_buffer(&self) -> Option> { + match self.diff_base.as_ref()? { + BufferDiffBase::Git(_) => None, + BufferDiffBase::PastBufferVersion { buffer, .. } => Some(buffer.clone()), + } + } + /// Recomputes the diff. pub fn recalculate_diff(&mut self, cx: &mut ModelContext) -> Option> { - let diff_base_rope = match self.diff_base.as_mut()? { + let diff_base_rope = match self.diff_base.as_ref()? { BufferDiffBase::Git(rope) => rope.clone(), - BufferDiffBase::PastBufferVersion(base_buffer, base_buffer_snapshot) => { - let new_base_snapshot = base_buffer.read(cx).snapshot(); - *base_buffer_snapshot = new_base_snapshot; - base_buffer_snapshot.as_rope().clone() - } + BufferDiffBase::PastBufferVersion { buffer, .. } => buffer.read(cx).as_rope().clone(), }; - let snapshot = self.snapshot(); + let snapshot = self.snapshot(); let mut diff = self.git_diff.clone(); let diff = cx.background_executor().spawn(async move { diff.update(&diff_base_rope, &snapshot).await; - diff + (diff, diff_base_rope) }); Some(cx.spawn(|this, mut cx| async move { - let buffer_diff = diff.await; + let (buffer_diff, diff_base_rope) = diff.await; this.update(&mut cx, |this, cx| { this.git_diff = buffer_diff; this.non_text_state_update_count += 1; + if let Some(BufferDiffBase::PastBufferVersion { rope, .. }) = &mut this.diff_base { + *rope = diff_base_rope; + cx.emit(BufferEvent::DiffBaseChanged); + } cx.emit(BufferEvent::DiffUpdated); }) .ok(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 1335a94dd0313ff6585d290342da40b6d61da4af..49cc31067b93ae14ac4ed302c7d212de683e74e6 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2413,80 +2413,98 @@ fn test_branch_and_merge(cx: &mut TestAppContext) { }); // Edits to the branch are not applied to the base. - branch_buffer.update(cx, |buffer, cx| { - buffer.edit( - [(Point::new(1, 0)..Point::new(1, 0), "ONE_POINT_FIVE\n")], + branch_buffer.update(cx, |branch_buffer, cx| { + branch_buffer.edit( + [ + (Point::new(1, 0)..Point::new(1, 0), "1.5\n"), + (Point::new(2, 0)..Point::new(2, 5), "THREE"), + ], None, cx, ) }); branch_buffer.read_with(cx, |branch_buffer, cx| { assert_eq!(base_buffer.read(cx).text(), "one\ntwo\nthree\n"); - assert_eq!(branch_buffer.text(), "one\nONE_POINT_FIVE\ntwo\nthree\n"); + assert_eq!(branch_buffer.text(), "one\n1.5\ntwo\nTHREE\n"); }); + // The branch buffer maintains a diff with respect to its base buffer. + start_recalculating_diff(&branch_buffer, cx); + cx.run_until_parked(); + assert_diff_hunks( + &branch_buffer, + cx, + &[(1..2, "", "1.5\n"), (3..4, "three\n", "THREE\n")], + ); + // Edits to the base are applied to the branch. base_buffer.update(cx, |buffer, cx| { buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "ZERO\n")], None, cx) }); branch_buffer.read_with(cx, |branch_buffer, cx| { assert_eq!(base_buffer.read(cx).text(), "ZERO\none\ntwo\nthree\n"); - assert_eq!( - branch_buffer.text(), - "ZERO\none\nONE_POINT_FIVE\ntwo\nthree\n" - ); + assert_eq!(branch_buffer.text(), "ZERO\none\n1.5\ntwo\nTHREE\n"); }); - assert_diff_hunks(&branch_buffer, cx, &[(2..3, "", "ONE_POINT_FIVE\n")]); + // Until the git diff recalculation is complete, the git diff references + // the previous content of the base buffer, so that it stays in sync. + start_recalculating_diff(&branch_buffer, cx); + assert_diff_hunks( + &branch_buffer, + cx, + &[(2..3, "", "1.5\n"), (4..5, "three\n", "THREE\n")], + ); + cx.run_until_parked(); + assert_diff_hunks( + &branch_buffer, + cx, + &[(2..3, "", "1.5\n"), (4..5, "three\n", "THREE\n")], + ); // Edits to any replica of the base are applied to the branch. base_buffer_replica.update(cx, |buffer, cx| { - buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), "TWO_POINT_FIVE\n")], - None, - cx, - ) + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "2.5\n")], None, cx) }); branch_buffer.read_with(cx, |branch_buffer, cx| { - assert_eq!( - base_buffer.read(cx).text(), - "ZERO\none\ntwo\nTWO_POINT_FIVE\nthree\n" - ); - assert_eq!( - branch_buffer.text(), - "ZERO\none\nONE_POINT_FIVE\ntwo\nTWO_POINT_FIVE\nthree\n" - ); + assert_eq!(base_buffer.read(cx).text(), "ZERO\none\ntwo\n2.5\nthree\n"); + assert_eq!(branch_buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n"); }); // Merging the branch applies all of its changes to the base. base_buffer.update(cx, |base_buffer, cx| { - base_buffer.merge(&branch_buffer, cx); + base_buffer.merge(&branch_buffer, None, cx); + }); + + branch_buffer.update(cx, |branch_buffer, cx| { assert_eq!( - base_buffer.text(), - "ZERO\none\nONE_POINT_FIVE\ntwo\nTWO_POINT_FIVE\nthree\n" + base_buffer.read(cx).text(), + "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n" ); + assert_eq!(branch_buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n"); }); } +fn start_recalculating_diff(buffer: &Model, cx: &mut TestAppContext) { + buffer + .update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap()) + .detach(); +} + +#[track_caller] fn assert_diff_hunks( buffer: &Model, cx: &mut TestAppContext, expected_hunks: &[(Range, &str, &str)], ) { - buffer - .update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap()) - .detach(); - cx.executor().run_until_parked(); - - buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - assert_hunks( - snapshot.git_diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX), - &snapshot, - &buffer.diff_base().unwrap().to_string(), - expected_hunks, - ); - }); + let (snapshot, diff_base) = buffer.read_with(cx, |buffer, _| { + (buffer.snapshot(), buffer.diff_base().unwrap().to_string()) + }); + assert_hunks( + snapshot.git_diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX), + &snapshot, + &diff_base, + expected_hunks, + ); } #[gpui::test(iterations = 100)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c631c01f99a1a65ec73732e28ef9fc282df00a95..4dc378a755bdae0de70b1d4de6ef34bdb8d6bbeb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -14,6 +14,7 @@ use breadcrumbs::Breadcrumbs; use client::ZED_URL_SCHEME; use collections::VecDeque; use command_palette_hooks::CommandPaletteFilter; +use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use feature_flags::FeatureFlagAppExt; use gpui::{ @@ -582,6 +583,8 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewCo let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); + let proposed_change_bar = cx.new_view(|_| ProposedChangesEditorToolbar::new()); + toolbar.add_item(proposed_change_bar, cx); let quick_action_bar = cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx)); toolbar.add_item(quick_action_bar, cx);