From 743feb98bcae8e00c8399be03fb27dc2b925bcdb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 20 Sep 2024 15:28:50 -0700 Subject: [PATCH] Add the ability to propose changes to a set of buffers (#18170) This PR introduces functionality for creating *branches* of buffers that can be used to preview and edit change sets that haven't yet been applied to the buffers themselves. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers Co-authored-by: Marshall --- Cargo.lock | 1 - crates/assistant/src/context.rs | 9 +- crates/channel/src/channel_buffer.rs | 5 +- crates/clock/src/clock.rs | 83 ++++++---- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 78 ++++++++-- crates/editor/src/element.rs | 5 +- crates/editor/src/git.rs | 24 +-- crates/editor/src/hunk_diff.rs | 24 +-- crates/editor/src/proposed_changes_editor.rs | 125 +++++++++++++++ crates/editor/src/test.rs | 6 +- crates/git/src/diff.rs | 70 ++++----- crates/language/src/buffer.rs | 154 ++++++++++++++----- crates/language/src/buffer_tests.rs | 146 ++++++++++++++++-- crates/multi_buffer/Cargo.toml | 1 - crates/multi_buffer/src/multi_buffer.rs | 46 +++--- crates/project/src/project.rs | 7 +- crates/project/src/project_tests.rs | 2 +- crates/remote_server/src/headless_project.rs | 7 +- crates/text/src/text.rs | 14 ++ 20 files changed, 622 insertions(+), 186 deletions(-) create mode 100644 crates/editor/src/proposed_changes_editor.rs diff --git a/Cargo.lock b/Cargo.lock index dd07dfa1cf..c0f6751b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7055,7 +7055,6 @@ dependencies = [ "ctor", "env_logger", "futures 0.3.30", - "git", "gpui", "itertools 0.13.0", "language", diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 1cac47831f..4f1f885b33 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1006,9 +1006,12 @@ impl Context { cx: &mut ModelContext, ) { match event { - language::BufferEvent::Operation(operation) => cx.emit(ContextEvent::Operation( - ContextOperation::BufferOperation(operation.clone()), - )), + language::BufferEvent::Operation { + operation, + is_local: true, + } => cx.emit(ContextEvent::Operation(ContextOperation::BufferOperation( + operation.clone(), + ))), language::BufferEvent::Edited => { self.count_remaining_tokens(cx); self.reparse(cx); diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 755e7400e1..0a4a259648 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -175,7 +175,10 @@ impl ChannelBuffer { cx: &mut ModelContext, ) { match event { - language::BufferEvent::Operation(operation) => { + language::BufferEvent::Operation { + operation, + is_local: true, + } => { if *ZED_ALWAYS_ACTIVE { if let language::Operation::UpdateSelections { selections, .. } = operation { if selections.is_empty() { diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index f7d36ed4a8..2b45e4a8fa 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -9,6 +9,8 @@ use std::{ pub use system_clock::*; +pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX; + /// A unique identifier for each distributed node. pub type ReplicaId = u16; @@ -25,7 +27,10 @@ pub struct Lamport { /// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock). #[derive(Clone, Default, Hash, Eq, PartialEq)] -pub struct Global(SmallVec<[u32; 8]>); +pub struct Global { + values: SmallVec<[u32; 8]>, + local_branch_value: u32, +} impl Global { pub fn new() -> Self { @@ -33,41 +38,51 @@ impl Global { } pub fn get(&self, replica_id: ReplicaId) -> Seq { - self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq + if replica_id == LOCAL_BRANCH_REPLICA_ID { + self.local_branch_value + } else { + self.values.get(replica_id as usize).copied().unwrap_or(0) as Seq + } } pub fn observe(&mut self, timestamp: Lamport) { if timestamp.value > 0 { - let new_len = timestamp.replica_id as usize + 1; - if new_len > self.0.len() { - self.0.resize(new_len, 0); - } + if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID { + self.local_branch_value = cmp::max(self.local_branch_value, timestamp.value); + } else { + let new_len = timestamp.replica_id as usize + 1; + if new_len > self.values.len() { + self.values.resize(new_len, 0); + } - let entry = &mut self.0[timestamp.replica_id as usize]; - *entry = cmp::max(*entry, timestamp.value); + let entry = &mut self.values[timestamp.replica_id as usize]; + *entry = cmp::max(*entry, timestamp.value); + } } } pub fn join(&mut self, other: &Self) { - if other.0.len() > self.0.len() { - self.0.resize(other.0.len(), 0); + if other.values.len() > self.values.len() { + self.values.resize(other.values.len(), 0); } - for (left, right) in self.0.iter_mut().zip(&other.0) { + for (left, right) in self.values.iter_mut().zip(&other.values) { *left = cmp::max(*left, *right); } + + self.local_branch_value = cmp::max(self.local_branch_value, other.local_branch_value); } pub fn meet(&mut self, other: &Self) { - if other.0.len() > self.0.len() { - self.0.resize(other.0.len(), 0); + if other.values.len() > self.values.len() { + self.values.resize(other.values.len(), 0); } let mut new_len = 0; for (ix, (left, right)) in self - .0 + .values .iter_mut() - .zip(other.0.iter().chain(iter::repeat(&0))) + .zip(other.values.iter().chain(iter::repeat(&0))) .enumerate() { if *left == 0 { @@ -80,7 +95,8 @@ impl Global { new_len = ix + 1; } } - self.0.resize(new_len, 0); + self.values.resize(new_len, 0); + self.local_branch_value = cmp::min(self.local_branch_value, other.local_branch_value); } pub fn observed(&self, timestamp: Lamport) -> bool { @@ -88,34 +104,44 @@ impl Global { } pub fn observed_any(&self, other: &Self) -> bool { - self.0 + self.values .iter() - .zip(other.0.iter()) + .zip(other.values.iter()) .any(|(left, right)| *right > 0 && left >= right) + || (other.local_branch_value > 0 && self.local_branch_value >= other.local_branch_value) } pub fn observed_all(&self, other: &Self) -> bool { - let mut rhs = other.0.iter(); - self.0.iter().all(|left| match rhs.next() { + let mut rhs = other.values.iter(); + self.values.iter().all(|left| match rhs.next() { Some(right) => left >= right, None => true, }) && rhs.next().is_none() + && self.local_branch_value >= other.local_branch_value } pub fn changed_since(&self, other: &Self) -> bool { - self.0.len() > other.0.len() + self.values.len() > other.values.len() || self - .0 + .values .iter() - .zip(other.0.iter()) + .zip(other.values.iter()) .any(|(left, right)| left > right) + || self.local_branch_value > other.local_branch_value } pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().enumerate().map(|(replica_id, seq)| Lamport { - replica_id: replica_id as ReplicaId, - value: *seq, - }) + self.values + .iter() + .enumerate() + .map(|(replica_id, seq)| Lamport { + replica_id: replica_id as ReplicaId, + value: *seq, + }) + .chain((self.local_branch_value > 0).then_some(Lamport { + replica_id: LOCAL_BRANCH_REPLICA_ID, + value: self.local_branch_value, + })) } } @@ -192,6 +218,9 @@ impl fmt::Debug for Global { } write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?; } + if self.local_branch_value > 0 { + write!(f, ": {}", self.local_branch_value)?; + } write!(f, "}}") } } diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 93c83af195..2383c7f71a 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -273,6 +273,7 @@ gpui::actions!( NextScreen, OpenExcerpts, OpenExcerptsSplit, + OpenProposedChangesEditor, OpenFile, OpenPermalinkToLine, OpenUrl, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f4a9376d2..b1a3d95a0d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -35,6 +35,7 @@ mod lsp_ext; mod mouse_context_menu; pub mod movement; mod persistence; +mod proposed_changes_editor; mod rust_analyzer_ext; pub mod scroll; mod selections_collection; @@ -46,7 +47,7 @@ mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; -use ::git::diff::{DiffHunk, DiffHunkStatus}; +use ::git::diff::DiffHunkStatus; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; pub(crate) use actions::*; use aho_corasick::AhoCorasick; @@ -98,6 +99,7 @@ use language::{ }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; +use proposed_changes_editor::{ProposedChangesBuffer, ProposedChangesEditor}; use similar::{ChangeTag, TextDiff}; use task::{ResolvedTask, TaskTemplate, TaskVariables}; @@ -113,7 +115,9 @@ pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; -use multi_buffer::{ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16}; +use multi_buffer::{ + ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, +}; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::project_settings::{GitGutterSetting, ProjectSettings}; @@ -6152,7 +6156,7 @@ impl Editor { pub fn prepare_revert_change( revert_changes: &mut HashMap, Rope)>>, multi_buffer: &Model, - hunk: &DiffHunk, + hunk: &MultiBufferDiffHunk, cx: &AppContext, ) -> Option<()> { let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?; @@ -9338,7 +9342,7 @@ impl Editor { snapshot: &DisplaySnapshot, initial_point: Point, is_wrapped: bool, - hunks: impl Iterator>, + hunks: impl Iterator, cx: &mut ViewContext, ) -> bool { let display_point = initial_point.to_display_point(snapshot); @@ -11885,6 +11889,52 @@ impl Editor { self.searchable } + fn open_proposed_changes_editor( + &mut self, + _: &OpenProposedChangesEditor, + cx: &mut ViewContext, + ) { + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; + + let buffer = self.buffer.read(cx); + let mut new_selections_by_buffer = HashMap::default(); + for selection in self.selections.all::(cx) { + for (buffer, mut range, _) in + buffer.range_to_buffer_ranges(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + let mut range = range.to_point(buffer.read(cx)); + range.start.column = 0; + range.end.column = buffer.read(cx).line_len(range.end.row); + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } + } + + let proposed_changes_buffers = new_selections_by_buffer + .into_iter() + .map(|(buffer, ranges)| ProposedChangesBuffer { buffer, ranges }) + .collect::>(); + let proposed_changes_editor = cx.new_view(|cx| { + ProposedChangesEditor::new(proposed_changes_buffers, self.project.clone(), cx) + }); + + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(proposed_changes_editor), true, true, None, cx); + }); + }); + }); + } + fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext) { self.open_excerpts_common(true, cx) } @@ -12399,7 +12449,7 @@ impl Editor { fn hunks_for_selections( multi_buffer_snapshot: &MultiBufferSnapshot, selections: &[Selection], -) -> Vec> { +) -> Vec { let buffer_rows_for_selections = selections.iter().map(|selection| { let head = selection.head(); let tail = selection.tail(); @@ -12418,7 +12468,7 @@ fn hunks_for_selections( pub fn hunks_for_rows( rows: impl Iterator>, multi_buffer_snapshot: &MultiBufferSnapshot, -) -> Vec> { +) -> Vec { let mut hunks = Vec::new(); let mut processed_buffer_rows: HashMap>> = HashMap::default(); @@ -12430,14 +12480,14 @@ pub fn hunks_for_rows( // when the caret is just above or just below the deleted hunk. let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; let related_to_selection = if allow_adjacent { - hunk.associated_range.overlaps(&query_rows) - || hunk.associated_range.start == query_rows.end - || hunk.associated_range.end == query_rows.start + hunk.row_range.overlaps(&query_rows) + || hunk.row_range.start == query_rows.end + || hunk.row_range.end == query_rows.start } else { // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) - // `hunk.associated_range` is exclusive (e.g. [2..3] means 2nd row is selected) - hunk.associated_range.overlaps(&selected_multi_buffer_rows) - || selected_multi_buffer_rows.end == hunk.associated_range.start + // `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected) + hunk.row_range.overlaps(&selected_multi_buffer_rows) + || selected_multi_buffer_rows.end == hunk.row_range.start }; if related_to_selection { if !processed_buffer_rows @@ -13738,10 +13788,10 @@ impl RowRangeExt for Range { } } -fn hunk_status(hunk: &DiffHunk) -> DiffHunkStatus { +fn hunk_status(hunk: &MultiBufferDiffHunk) -> DiffHunkStatus { if hunk.diff_base_byte_range.is_empty() { DiffHunkStatus::Added - } else if hunk.associated_range.is_empty() { + } else if hunk.row_range.is_empty() { DiffHunkStatus::Removed } else { DiffHunkStatus::Modified diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 47107b9754..d4075431ff 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -346,6 +346,7 @@ impl EditorElement { register_action(view, cx, Editor::toggle_code_actions); register_action(view, cx, Editor::open_excerpts); register_action(view, cx, Editor::open_excerpts_in_split); + register_action(view, cx, Editor::open_proposed_changes_editor); register_action(view, cx, Editor::toggle_soft_wrap); register_action(view, cx, Editor::toggle_tab_bar); register_action(view, cx, Editor::toggle_line_numbers); @@ -3710,11 +3711,11 @@ impl EditorElement { ) .map(|hunk| { let start_display_row = - MultiBufferPoint::new(hunk.associated_range.start.0, 0) + MultiBufferPoint::new(hunk.row_range.start.0, 0) .to_display_point(&snapshot.display_snapshot) .row(); let mut end_display_row = - MultiBufferPoint::new(hunk.associated_range.end.0, 0) + MultiBufferPoint::new(hunk.row_range.end.0, 0) .to_display_point(&snapshot.display_snapshot) .row(); if end_display_row != start_display_row { diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 63b083faa8..79b78d5d14 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -2,9 +2,9 @@ pub mod blame; use std::ops::Range; -use git::diff::{DiffHunk, DiffHunkStatus}; +use git::diff::DiffHunkStatus; use language::Point; -use multi_buffer::{Anchor, MultiBufferRow}; +use multi_buffer::{Anchor, MultiBufferDiffHunk}; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -49,25 +49,25 @@ impl DisplayDiffHunk { } pub fn diff_hunk_to_display( - hunk: &DiffHunk, + hunk: &MultiBufferDiffHunk, snapshot: &DisplaySnapshot, ) -> DisplayDiffHunk { - let hunk_start_point = Point::new(hunk.associated_range.start.0, 0); - let hunk_start_point_sub = Point::new(hunk.associated_range.start.0.saturating_sub(1), 0); + let hunk_start_point = Point::new(hunk.row_range.start.0, 0); + let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0); let hunk_end_point_sub = Point::new( - hunk.associated_range + hunk.row_range .end .0 .saturating_sub(1) - .max(hunk.associated_range.start.0), + .max(hunk.row_range.start.0), 0, ); let status = hunk_status(hunk); let is_removal = status == DiffHunkStatus::Removed; - let folds_start = Point::new(hunk.associated_range.start.0.saturating_sub(2), 0); - let folds_end = Point::new(hunk.associated_range.end.0 + 2, 0); + let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0); + let folds_end = Point::new(hunk.row_range.end.0 + 2, 0); let folds_range = folds_start..folds_end; let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { @@ -87,7 +87,7 @@ pub fn diff_hunk_to_display( } else { let start = hunk_start_point.to_display_point(snapshot).row(); - let hunk_end_row = hunk.associated_range.end.max(hunk.associated_range.start); + let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start); let hunk_end_point = Point::new(hunk_end_row.0, 0); let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point); @@ -288,7 +288,7 @@ mod tests { assert_eq!( snapshot .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12)) - .map(|hunk| (hunk_status(&hunk), hunk.associated_range)) + .map(|hunk| (hunk_status(&hunk), hunk.row_range)) .collect::>(), &expected, ); @@ -296,7 +296,7 @@ mod tests { assert_eq!( snapshot .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12)) - .map(|hunk| (hunk_status(&hunk), hunk.associated_range)) + .map(|hunk| (hunk_status(&hunk), hunk.row_range)) .collect::>(), expected .iter() diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 361ea6246e..917d07ec4e 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -4,11 +4,12 @@ use std::{ }; use collections::{hash_map, HashMap, HashSet}; -use git::diff::{DiffHunk, DiffHunkStatus}; +use git::diff::DiffHunkStatus; use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View}; use language::Buffer; use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint, + Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, + MultiBufferSnapshot, ToPoint, }; use settings::SettingsStore; use text::{BufferId, Point}; @@ -190,9 +191,9 @@ impl Editor { .buffer_snapshot .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX) .filter(|hunk| { - let hunk_display_row_range = Point::new(hunk.associated_range.start.0, 0) + let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0) .to_display_point(&snapshot.display_snapshot) - ..Point::new(hunk.associated_range.end.0, 0) + ..Point::new(hunk.row_range.end.0, 0) .to_display_point(&snapshot.display_snapshot); let row_range_end = display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row()); @@ -203,7 +204,7 @@ impl Editor { fn toggle_hunks_expanded( &mut self, - hunks_to_toggle: Vec>, + hunks_to_toggle: Vec, cx: &mut ViewContext, ) { let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None); @@ -274,8 +275,8 @@ impl Editor { }); for remaining_hunk in hunks_to_toggle { let remaining_hunk_point_range = - Point::new(remaining_hunk.associated_range.start.0, 0) - ..Point::new(remaining_hunk.associated_range.end.0, 0); + Point::new(remaining_hunk.row_range.start.0, 0) + ..Point::new(remaining_hunk.row_range.end.0, 0); hunks_to_expand.push(HoveredHunk { status: hunk_status(&remaining_hunk), multi_buffer_range: remaining_hunk_point_range @@ -705,7 +706,7 @@ impl Editor { fn to_diff_hunk( hovered_hunk: &HoveredHunk, multi_buffer_snapshot: &MultiBufferSnapshot, -) -> Option> { +) -> Option { let buffer_id = hovered_hunk .multi_buffer_range .start @@ -716,9 +717,8 @@ fn to_diff_hunk( let point_range = hovered_hunk .multi_buffer_range .to_point(multi_buffer_snapshot); - Some(DiffHunk { - associated_range: MultiBufferRow(point_range.start.row) - ..MultiBufferRow(point_range.end.row), + Some(MultiBufferDiffHunk { + row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row), buffer_id, buffer_range, diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(), @@ -868,7 +868,7 @@ fn editor_with_deleted_text( fn buffer_diff_hunk( buffer_snapshot: &MultiBufferSnapshot, row_range: Range, -) -> Option> { +) -> Option { let mut hunks = buffer_snapshot.git_diff_hunks_in_range( MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row), ); diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs new file mode 100644 index 0000000000..3979e558a4 --- /dev/null +++ b/crates/editor/src/proposed_changes_editor.rs @@ -0,0 +1,125 @@ +use crate::{Editor, EditorEvent}; +use collections::HashSet; +use futures::{channel::mpsc, future::join_all}; +use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View}; +use language::{Buffer, BufferEvent, Capability}; +use multi_buffer::{ExcerptRange, MultiBuffer}; +use project::Project; +use smol::stream::StreamExt; +use std::{ops::Range, time::Duration}; +use text::ToOffset; +use ui::prelude::*; +use workspace::Item; + +pub struct ProposedChangesEditor { + editor: View, + _subscriptions: Vec, + _recalculate_diffs_task: Task>, + recalculate_diffs_tx: mpsc::UnboundedSender>, +} + +pub struct ProposedChangesBuffer { + pub buffer: Model, + pub ranges: Vec>, +} + +impl ProposedChangesEditor { + pub fn new( + buffers: Vec>, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + let mut subscriptions = Vec::new(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + + for buffer in buffers { + let branch_buffer = buffer.buffer.update(cx, |buffer, cx| buffer.branch(cx)); + subscriptions.push(cx.subscribe(&branch_buffer, Self::on_buffer_event)); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts( + branch_buffer, + buffer.ranges.into_iter().map(|range| ExcerptRange { + context: range, + primary: None, + }), + cx, + ); + }); + } + + let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded(); + + Self { + editor: cx + .new_view(|cx| Editor::for_multibuffer(multibuffer.clone(), project, true, cx)), + recalculate_diffs_tx, + _recalculate_diffs_task: cx.spawn(|_, mut cx| async move { + let mut buffers_to_diff = HashSet::default(); + while let Some(buffer) = recalculate_diffs_rx.next().await { + buffers_to_diff.insert(buffer); + + loop { + cx.background_executor() + .timer(Duration::from_millis(250)) + .await; + let mut had_further_changes = false; + while let Ok(next_buffer) = recalculate_diffs_rx.try_next() { + buffers_to_diff.insert(next_buffer?); + had_further_changes = true; + } + if !had_further_changes { + break; + } + } + + join_all(buffers_to_diff.drain().filter_map(|buffer| { + buffer + .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx)) + .ok()? + })) + .await; + } + None + }), + _subscriptions: subscriptions, + } + } + + fn on_buffer_event( + &mut self, + buffer: Model, + event: &BufferEvent, + _cx: &mut ViewContext, + ) { + if let BufferEvent::Edited = event { + self.recalculate_diffs_tx.unbounded_send(buffer).ok(); + } + } +} + +impl Render for ProposedChangesEditor { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + self.editor.clone() + } +} + +impl FocusableView for ProposedChangesEditor { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl EventEmitter for ProposedChangesEditor {} + +impl Item for ProposedChangesEditor { + type Event = EditorEvent; + + fn tab_icon(&self, _cx: &ui::WindowContext) -> Option { + Some(Icon::new(IconName::Pencil)) + } + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some("Proposed changes".into()) + } +} diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index fcbd3bd423..50214cd723 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -108,16 +108,16 @@ pub fn editor_hunks( .buffer_snapshot .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX) .map(|hunk| { - let display_range = Point::new(hunk.associated_range.start.0, 0) + let display_range = Point::new(hunk.row_range.start.0, 0) .to_display_point(snapshot) .row() - ..Point::new(hunk.associated_range.end.0, 0) + ..Point::new(hunk.row_range.end.0, 0) .to_display_point(snapshot) .row(); let (_, buffer, _) = editor .buffer() .read(cx) - .excerpt_containing(Point::new(hunk.associated_range.start.0, 0), cx) + .excerpt_containing(Point::new(hunk.row_range.start.0, 0), cx) .expect("no excerpt for expanded buffer's hunk start"); let diff_base = buffer .read(cx) diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 8cc7ee1863..1f7930ce14 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -1,7 +1,7 @@ use rope::Rope; use std::{iter, ops::Range}; use sum_tree::SumTree; -use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; pub use git2 as libgit; use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; @@ -13,29 +13,30 @@ pub enum DiffHunkStatus { Removed, } -/// A diff hunk, representing a range of consequent lines in a singleton buffer, associated with a generic range. +/// A diff hunk resolved to rows in the buffer. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DiffHunk { - /// E.g. a range in multibuffer, that has an excerpt added, singleton buffer for which has this diff hunk. - /// Consider a singleton buffer with 10 lines, all of them are modified — so a corresponding diff hunk would have a range 0..10. - /// And a multibuffer with the excerpt of lines 2-6 from the singleton buffer. - /// If the multibuffer is searched for diff hunks, the associated range would be multibuffer rows, corresponding to rows 2..6 from the singleton buffer. - /// But the hunk range would be 0..10, same for any other excerpts from the same singleton buffer. - pub associated_range: Range, - /// Singleton buffer ID this hunk belongs to. - pub buffer_id: BufferId, - /// A consequent range of lines in the singleton buffer, that were changed and produced this diff hunk. +pub struct DiffHunk { + /// The buffer range, expressed in terms of rows. + pub row_range: Range, + /// The range in the buffer to which this hunk corresponds. pub buffer_range: Range, - /// Original singleton buffer text before the change, that was instead of the `buffer_range`. + /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range, } -impl sum_tree::Item for DiffHunk { +/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. +#[derive(Debug, Clone)] +struct InternalDiffHunk { + buffer_range: Range, + diff_base_byte_range: Range, +} + +impl sum_tree::Item for InternalDiffHunk { type Summary = DiffHunkSummary; fn summary(&self) -> Self::Summary { DiffHunkSummary { - buffer_range: self.associated_range.clone(), + buffer_range: self.buffer_range.clone(), } } } @@ -64,7 +65,7 @@ impl sum_tree::Summary for DiffHunkSummary { #[derive(Debug, Clone)] pub struct BufferDiff { last_buffer_version: Option, - tree: SumTree>, + tree: SumTree, } impl BufferDiff { @@ -79,11 +80,12 @@ impl BufferDiff { self.tree.is_empty() } + #[cfg(any(test, feature = "test-support"))] pub fn hunks_in_row_range<'a>( &'a self, range: Range, buffer: &'a BufferSnapshot, - ) -> impl 'a + Iterator> { + ) -> impl 'a + Iterator { let start = buffer.anchor_before(Point::new(range.start, 0)); let end = buffer.anchor_after(Point::new(range.end, 0)); @@ -94,7 +96,7 @@ impl BufferDiff { &'a self, range: Range, buffer: &'a BufferSnapshot, - ) -> impl 'a + Iterator> { + ) -> impl 'a + Iterator { let mut cursor = self .tree .filter::<_, DiffHunkSummary>(buffer, move |summary| { @@ -109,11 +111,8 @@ impl BufferDiff { }) .flat_map(move |hunk| { [ - ( - &hunk.associated_range.start, - hunk.diff_base_byte_range.start, - ), - (&hunk.associated_range.end, hunk.diff_base_byte_range.end), + (&hunk.buffer_range.start, hunk.diff_base_byte_range.start), + (&hunk.buffer_range.end, hunk.diff_base_byte_range.end), ] .into_iter() }); @@ -129,10 +128,9 @@ impl BufferDiff { } Some(DiffHunk { - associated_range: start_point.row..end_point.row, + row_range: start_point.row..end_point.row, diff_base_byte_range: start_base..end_base, buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point), - buffer_id: buffer.remote_id(), }) }) } @@ -141,7 +139,7 @@ impl BufferDiff { &'a self, range: Range, buffer: &'a BufferSnapshot, - ) -> impl 'a + Iterator> { + ) -> impl 'a + Iterator { let mut cursor = self .tree .filter::<_, DiffHunkSummary>(buffer, move |summary| { @@ -154,7 +152,7 @@ impl BufferDiff { cursor.prev(buffer); let hunk = cursor.item()?; - let range = hunk.associated_range.to_point(buffer); + let range = hunk.buffer_range.to_point(buffer); let end_row = if range.end.column > 0 { range.end.row + 1 } else { @@ -162,10 +160,9 @@ impl BufferDiff { }; Some(DiffHunk { - associated_range: range.start.row..end_row, + row_range: range.start.row..end_row, diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), - buffer_id: hunk.buffer_id, }) }) } @@ -196,7 +193,7 @@ impl BufferDiff { } #[cfg(test)] - fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { + fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator { let start = text.anchor_before(Point::new(0, 0)); let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); self.hunks_intersecting_range(start..end, text) @@ -229,7 +226,7 @@ impl BufferDiff { hunk_index: usize, buffer: &text::BufferSnapshot, buffer_row_divergence: &mut i64, - ) -> DiffHunk { + ) -> InternalDiffHunk { let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap(); assert!(line_item_count > 0); @@ -284,11 +281,9 @@ impl BufferDiff { let start = Point::new(buffer_row_range.start, 0); let end = Point::new(buffer_row_range.end, 0); let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end); - DiffHunk { - associated_range: buffer_range.clone(), + InternalDiffHunk { buffer_range, diff_base_byte_range, - buffer_id: buffer.remote_id(), } } } @@ -302,17 +297,16 @@ pub fn assert_hunks( diff_base: &str, expected_hunks: &[(Range, &str, &str)], ) where - Iter: Iterator>, + Iter: Iterator, { let actual_hunks = diff_hunks .map(|hunk| { ( - hunk.associated_range.clone(), + hunk.row_range.clone(), &diff_base[hunk.diff_base_byte_range], buffer .text_for_range( - Point::new(hunk.associated_range.start, 0) - ..Point::new(hunk.associated_range.end, 0), + Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0), ) .collect::(), ) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index acb57273e3..5735ee9616 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,8 +21,8 @@ use async_watch as watch; pub use clock::ReplicaId; use futures::channel::oneshot; use gpui::{ - AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Pixels, Task, TaskLabel, - WindowContext, + AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext, + Pixels, Task, TaskLabel, WindowContext, }; use lsp::LanguageServerId; use parking_lot::Mutex; @@ -84,11 +84,17 @@ pub enum Capability { pub type BufferRow = u32; +#[derive(Clone)] +enum BufferDiffBase { + Git(Rope), + PastBufferVersion(Model, BufferSnapshot), +} + /// An in-memory representation of a source code file, including its text, /// syntax trees, git status, and diagnostics. pub struct Buffer { text: TextBuffer, - diff_base: Option, + diff_base: Option, git_diff: git::diff::BufferDiff, file: Option>, /// The mtime of the file when this buffer was last loaded from @@ -121,6 +127,7 @@ pub struct Buffer { /// Memoize calls to has_changes_since(saved_version). /// The contents of a cell are (self.version, has_changes) at the time of a last call. has_unsaved_edits: Cell<(clock::Global, bool)>, + _subscriptions: Vec, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -308,7 +315,10 @@ pub enum Operation { pub enum BufferEvent { /// The buffer was changed in a way that must be /// propagated to its other replicas. - Operation(Operation), + Operation { + operation: Operation, + is_local: bool, + }, /// The buffer was edited. Edited, /// The buffer's `dirty` bit changed. @@ -644,7 +654,7 @@ impl Buffer { id: self.remote_id().into(), file: self.file.as_ref().map(|f| f.to_proto(cx)), base_text: self.base_text().to_string(), - diff_base: self.diff_base.as_ref().map(|h| h.to_string()), + diff_base: self.diff_base().as_ref().map(|h| h.to_string()), line_ending: proto::serialize_line_ending(self.line_ending()) as i32, saved_version: proto::serialize_version(&self.saved_version), saved_mtime: self.saved_mtime.map(|time| time.into()), @@ -734,12 +744,10 @@ impl Buffer { was_dirty_before_starting_transaction: None, has_unsaved_edits: Cell::new((buffer.version(), false)), text: buffer, - diff_base: diff_base - .map(|mut raw_diff_base| { - LineEnding::normalize(&mut raw_diff_base); - raw_diff_base - }) - .map(Rope::from), + diff_base: diff_base.map(|mut raw_diff_base| { + LineEnding::normalize(&mut raw_diff_base); + BufferDiffBase::Git(Rope::from(raw_diff_base)) + }), diff_base_version: 0, git_diff, file, @@ -759,6 +767,7 @@ impl Buffer { completion_triggers_timestamp: Default::default(), deferred_ops: OperationQueue::new(), has_conflict: false, + _subscriptions: Vec::new(), } } @@ -782,6 +791,52 @@ impl Buffer { } } + pub fn branch(&mut self, cx: &mut ModelContext) -> Model { + let this = cx.handle(); + cx.new_model(|cx| { + let mut branch = Self { + diff_base: Some(BufferDiffBase::PastBufferVersion( + this.clone(), + self.snapshot(), + )), + 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; + } + })], + ..Self::build( + self.text.branch(), + None, + self.file.clone(), + self.capability(), + ) + }; + if let Some(language_registry) = self.language_registry() { + branch.set_language_registry(language_registry); + } + + branch + }) + } + + 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::(), + ) + }) + .collect::>(); + self.edit(edits, None, cx); + } + #[cfg(test)] pub(crate) fn as_text_snapshot(&self) -> &text::BufferSnapshot { &self.text @@ -961,20 +1016,23 @@ impl Buffer { /// Returns the current diff base, see [Buffer::set_diff_base]. pub fn diff_base(&self) -> Option<&Rope> { - self.diff_base.as_ref() + match self.diff_base.as_ref()? { + BufferDiffBase::Git(rope) => Some(rope), + BufferDiffBase::PastBufferVersion(_, buffer_snapshot) => { + Some(buffer_snapshot.as_rope()) + } + } } /// Sets the text that will be used to compute a Git diff /// against the buffer text. pub fn set_diff_base(&mut self, diff_base: Option, cx: &mut ModelContext) { - self.diff_base = diff_base - .map(|mut raw_diff_base| { - LineEnding::normalize(&mut raw_diff_base); - raw_diff_base - }) - .map(Rope::from); + self.diff_base = diff_base.map(|mut raw_diff_base| { + LineEnding::normalize(&mut raw_diff_base); + BufferDiffBase::Git(Rope::from(raw_diff_base)) + }); self.diff_base_version += 1; - if let Some(recalc_task) = self.git_diff_recalc(cx) { + if let Some(recalc_task) = self.recalculate_diff(cx) { cx.spawn(|buffer, mut cx| async move { recalc_task.await; buffer @@ -992,14 +1050,21 @@ impl Buffer { self.diff_base_version } - /// Recomputes the Git diff status. - pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) -> Option> { - let diff_base = self.diff_base.clone()?; + /// Recomputes the diff. + pub fn recalculate_diff(&mut self, cx: &mut ModelContext) -> Option> { + let diff_base_rope = match self.diff_base.as_mut()? { + 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() + } + }; let snapshot = self.snapshot(); let mut diff = self.git_diff.clone(); let diff = cx.background_executor().spawn(async move { - diff.update(&diff_base, &snapshot).await; + diff.update(&diff_base_rope, &snapshot).await; diff }); @@ -1169,7 +1234,7 @@ impl Buffer { lamport_timestamp, }; self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx); - self.send_operation(op, cx); + self.send_operation(op, true, cx); } fn request_autoindent(&mut self, cx: &mut ModelContext) { @@ -1743,6 +1808,7 @@ impl Buffer { lamport_timestamp, cursor_shape, }, + true, cx, ); self.non_text_state_update_count += 1; @@ -1889,7 +1955,7 @@ impl Buffer { } self.end_transaction(cx); - self.send_operation(Operation::Buffer(edit_operation), cx); + self.send_operation(Operation::Buffer(edit_operation), true, cx); Some(edit_id) } @@ -1991,6 +2057,9 @@ impl Buffer { } }) .collect::>(); + for operation in buffer_ops.iter() { + self.send_operation(Operation::Buffer(operation.clone()), false, cx); + } self.text.apply_ops(buffer_ops); self.deferred_ops.insert(deferred_ops); self.flush_deferred_ops(cx); @@ -2114,8 +2183,16 @@ impl Buffer { } } - fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext) { - cx.emit(BufferEvent::Operation(operation)); + fn send_operation( + &mut self, + operation: Operation, + is_local: bool, + cx: &mut ModelContext, + ) { + cx.emit(BufferEvent::Operation { + operation, + is_local, + }); } /// Removes the selections for a given peer. @@ -2130,7 +2207,7 @@ impl Buffer { let old_version = self.version.clone(); if let Some((transaction_id, operation)) = self.text.undo() { - self.send_operation(Operation::Buffer(operation), cx); + self.send_operation(Operation::Buffer(operation), true, cx); self.did_edit(&old_version, was_dirty, cx); Some(transaction_id) } else { @@ -2147,7 +2224,7 @@ impl Buffer { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); if let Some(operation) = self.text.undo_transaction(transaction_id) { - self.send_operation(Operation::Buffer(operation), cx); + self.send_operation(Operation::Buffer(operation), true, cx); self.did_edit(&old_version, was_dirty, cx); true } else { @@ -2167,7 +2244,7 @@ impl Buffer { let operations = self.text.undo_to_transaction(transaction_id); let undone = !operations.is_empty(); for operation in operations { - self.send_operation(Operation::Buffer(operation), cx); + self.send_operation(Operation::Buffer(operation), true, cx); } if undone { self.did_edit(&old_version, was_dirty, cx) @@ -2181,7 +2258,7 @@ impl Buffer { let old_version = self.version.clone(); if let Some((transaction_id, operation)) = self.text.redo() { - self.send_operation(Operation::Buffer(operation), cx); + self.send_operation(Operation::Buffer(operation), true, cx); self.did_edit(&old_version, was_dirty, cx); Some(transaction_id) } else { @@ -2201,7 +2278,7 @@ impl Buffer { let operations = self.text.redo_to_transaction(transaction_id); let redone = !operations.is_empty(); for operation in operations { - self.send_operation(Operation::Buffer(operation), cx); + self.send_operation(Operation::Buffer(operation), true, cx); } if redone { self.did_edit(&old_version, was_dirty, cx) @@ -2218,6 +2295,7 @@ impl Buffer { triggers, lamport_timestamp: self.completion_triggers_timestamp, }, + true, cx, ); cx.notify(); @@ -2297,7 +2375,7 @@ impl Buffer { let ops = self.text.randomly_undo_redo(rng); if !ops.is_empty() { for op in ops { - self.send_operation(Operation::Buffer(op), cx); + self.send_operation(Operation::Buffer(op), true, cx); self.did_edit(&old_version, was_dirty, cx); } } @@ -3638,12 +3716,12 @@ impl BufferSnapshot { !self.git_diff.is_empty() } - /// Returns all the Git diff hunks intersecting the given - /// row range. + /// Returns all the Git diff hunks intersecting the given row range. + #[cfg(any(test, feature = "test-support"))] pub fn git_diff_hunks_in_row_range( &self, range: Range, - ) -> impl '_ + Iterator> { + ) -> impl '_ + Iterator { self.git_diff.hunks_in_row_range(range, self) } @@ -3652,7 +3730,7 @@ impl BufferSnapshot { pub fn git_diff_hunks_intersecting_range( &self, range: Range, - ) -> impl '_ + Iterator> { + ) -> impl '_ + Iterator { self.git_diff.hunks_intersecting_range(range, self) } @@ -3661,7 +3739,7 @@ impl BufferSnapshot { pub fn git_diff_hunks_intersecting_range_rev( &self, range: Range, - ) -> impl '_ + Iterator> { + ) -> impl '_ + Iterator { self.git_diff.hunks_intersecting_range_rev(range, self) } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 23faa33316..1335a94dd0 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -6,6 +6,7 @@ use crate::Buffer; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; +use git::diff::assert_hunks; use gpui::{AppContext, BorrowAppContext, Model}; use gpui::{Context, TestAppContext}; use indoc::indoc; @@ -275,13 +276,19 @@ fn test_edit_events(cx: &mut gpui::AppContext) { |buffer, cx| { let buffer_1_events = buffer_1_events.clone(); cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { - BufferEvent::Operation(op) => buffer1_ops.lock().push(op), + BufferEvent::Operation { + operation, + is_local: true, + } => buffer1_ops.lock().push(operation), event => buffer_1_events.lock().push(event), }) .detach(); let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, _, event, _| { - buffer_2_events.lock().push(event.clone()) + cx.subscribe(&buffer2, move |_, _, event, _| match event.clone() { + BufferEvent::Operation { + is_local: false, .. + } => {} + event => buffer_2_events.lock().push(event), }) .detach(); @@ -2370,6 +2377,118 @@ async fn test_find_matching_indent(cx: &mut TestAppContext) { ); } +#[gpui::test] +fn test_branch_and_merge(cx: &mut TestAppContext) { + cx.update(|cx| init_settings(cx, |_| {})); + + let base_buffer = cx.new_model(|cx| Buffer::local("one\ntwo\nthree\n", cx)); + + // Create a remote replica of the base buffer. + let base_buffer_replica = cx.new_model(|cx| { + Buffer::from_proto( + 1, + Capability::ReadWrite, + base_buffer.read(cx).to_proto(cx), + None, + ) + .unwrap() + }); + base_buffer.update(cx, |_buffer, cx| { + cx.subscribe(&base_buffer_replica, |this, _, event, cx| { + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { + this.apply_ops([operation.clone()], cx); + } + }) + .detach(); + }); + + // Create a branch, which initially has the same state as the base buffer. + let branch_buffer = base_buffer.update(cx, |buffer, cx| buffer.branch(cx)); + branch_buffer.read_with(cx, |buffer, _| { + assert_eq!(buffer.text(), "one\ntwo\nthree\n"); + }); + + // 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")], + 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"); + }); + + // 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_diff_hunks(&branch_buffer, cx, &[(2..3, "", "ONE_POINT_FIVE\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, + ) + }); + 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" + ); + }); + + // Merging the branch applies all of its changes to the base. + base_buffer.update(cx, |base_buffer, cx| { + base_buffer.merge(&branch_buffer, cx); + assert_eq!( + base_buffer.text(), + "ZERO\none\nONE_POINT_FIVE\ntwo\nTWO_POINT_FIVE\nthree\n" + ); + }); +} + +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, + ); + }); +} + #[gpui::test(iterations = 100)] fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let min_peers = env::var("MIN_PEERS") @@ -2407,10 +2526,15 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); let network = network.clone(); cx.subscribe(&cx.handle(), move |buffer, _, event, _| { - if let BufferEvent::Operation(op) = event { - network - .lock() - .broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]); + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { + network.lock().broadcast( + buffer.replica_id(), + vec![proto::serialize_operation(operation)], + ); } }) .detach(); @@ -2533,10 +2657,14 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); let network = network.clone(); cx.subscribe(&cx.handle(), move |buffer, _, event, _| { - if let BufferEvent::Operation(op) = event { + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { network.lock().broadcast( buffer.replica_id(), - vec![proto::serialize_operation(op)], + vec![proto::serialize_operation(operation)], ); } }) diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index acd0c89f8e..444fe3c75c 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -27,7 +27,6 @@ collections.workspace = true ctor.workspace = true env_logger.workspace = true futures.workspace = true -git.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f6a61f562a..d406f9bfaf 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5,7 +5,6 @@ use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; -use git::diff::DiffHunk; use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext}; use itertools::Itertools; use language::{ @@ -110,6 +109,19 @@ pub enum Event { DiagnosticsUpdated, } +/// A diff hunk, representing a range of consequent lines in a multibuffer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MultiBufferDiffHunk { + /// The row range in the multibuffer where this diff hunk appears. + pub row_range: Range, + /// The buffer ID that this hunk belongs to. + pub buffer_id: BufferId, + /// The range of the underlying buffer that this hunk corresponds to. + pub buffer_range: Range, + /// The range within the buffer's diff base that this hunk corresponds to. + pub diff_base_byte_range: Range, +} + pub type MultiBufferPoint = Point; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, serde::Deserialize)] @@ -1711,7 +1723,7 @@ impl MultiBuffer { } // - language::BufferEvent::Operation(_) => return, + language::BufferEvent::Operation { .. } => return, }); } @@ -3561,7 +3573,7 @@ impl MultiBufferSnapshot { pub fn git_diff_hunks_in_range_rev( &self, row_range: Range, - ) -> impl Iterator> + '_ { + ) -> impl Iterator + '_ { let mut cursor = self.excerpts.cursor::(&()); cursor.seek(&Point::new(row_range.end.0, 0), Bias::Left, &()); @@ -3599,22 +3611,19 @@ impl MultiBufferSnapshot { .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end) .map(move |hunk| { let start = multibuffer_start.row - + hunk - .associated_range - .start - .saturating_sub(excerpt_start_point.row); + + hunk.row_range.start.saturating_sub(excerpt_start_point.row); let end = multibuffer_start.row + hunk - .associated_range + .row_range .end .min(excerpt_end_point.row + 1) .saturating_sub(excerpt_start_point.row); - DiffHunk { - associated_range: MultiBufferRow(start)..MultiBufferRow(end), + MultiBufferDiffHunk { + row_range: MultiBufferRow(start)..MultiBufferRow(end), diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), - buffer_id: hunk.buffer_id, + buffer_id: excerpt.buffer_id, } }); @@ -3628,7 +3637,7 @@ impl MultiBufferSnapshot { pub fn git_diff_hunks_in_range( &self, row_range: Range, - ) -> impl Iterator> + '_ { + ) -> impl Iterator + '_ { let mut cursor = self.excerpts.cursor::(&()); cursor.seek(&Point::new(row_range.start.0, 0), Bias::Left, &()); @@ -3673,23 +3682,20 @@ impl MultiBufferSnapshot { MultiBufferRow(0)..MultiBufferRow(1) } else { let start = multibuffer_start.row - + hunk - .associated_range - .start - .saturating_sub(excerpt_rows.start); + + hunk.row_range.start.saturating_sub(excerpt_rows.start); let end = multibuffer_start.row + hunk - .associated_range + .row_range .end .min(excerpt_rows.end + 1) .saturating_sub(excerpt_rows.start); MultiBufferRow(start)..MultiBufferRow(end) }; - DiffHunk { - associated_range: buffer_range, + MultiBufferDiffHunk { + row_range: buffer_range, diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), - buffer_id: hunk.buffer_id, + buffer_id: excerpt.buffer_id, } }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 435c143024..bd9c17ecb2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2182,7 +2182,10 @@ impl Project { let buffer_id = buffer.read(cx).remote_id(); match event { - BufferEvent::Operation(operation) => { + BufferEvent::Operation { + operation, + is_local: true, + } => { let operation = language::proto::serialize_operation(operation); if let Some(ssh) = &self.ssh_session { @@ -2267,7 +2270,7 @@ impl Project { .filter_map(|buffer| { let buffer = buffer.upgrade()?; buffer - .update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx)) + .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx)) .ok() .flatten() }) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 72a38ccba7..d0d67f0cda 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3288,7 +3288,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { cx.subscribe(&buffer1, { let events = events.clone(); move |_, _, event, _| match event { - BufferEvent::Operation(_) => {} + BufferEvent::Operation { .. } => {} _ => events.lock().push(event.clone()), } }) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 54f48e3626..9d5c26d6c7 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -146,12 +146,15 @@ impl HeadlessProject { cx: &mut ModelContext, ) { match event { - BufferEvent::Operation(op) => cx + BufferEvent::Operation { + operation, + is_local: true, + } => cx .background_executor() .spawn(self.session.request(proto::UpdateBuffer { project_id: SSH_PROJECT_ID, buffer_id: buffer.read(cx).remote_id().to_proto(), - operations: vec![serialize_operation(op)], + operations: vec![serialize_operation(operation)], })) .detach(), _ => {} diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8d2cd97aac..8bdc9fdb03 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -13,6 +13,7 @@ mod undo_map; pub use anchor::*; use anyhow::{anyhow, Context as _, Result}; pub use clock::ReplicaId; +use clock::LOCAL_BRANCH_REPLICA_ID; use collections::{HashMap, HashSet}; use locator::Locator; use operation_queue::OperationQueue; @@ -715,6 +716,19 @@ impl Buffer { self.snapshot.clone() } + pub fn branch(&self) -> Self { + Self { + snapshot: self.snapshot.clone(), + history: History::new(self.base_text().clone()), + deferred_ops: OperationQueue::new(), + deferred_replicas: HashSet::default(), + lamport_clock: clock::Lamport::new(LOCAL_BRANCH_REPLICA_ID), + subscriptions: Default::default(), + edit_id_resolvers: Default::default(), + wait_for_version_txs: Default::default(), + } + } + pub fn replica_id(&self) -> ReplicaId { self.lamport_clock.replica_id }