Cargo.lock 🔗
@@ -7055,7 +7055,6 @@ dependencies = [
"ctor",
"env_logger",
"futures 0.3.30",
- "git",
"gpui",
"itertools 0.13.0",
"language",
Max Brunsfeld , Marshall Bowers , and Marshall created
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 <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
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(-)
@@ -7055,7 +7055,6 @@ dependencies = [
"ctor",
"env_logger",
"futures 0.3.30",
- "git",
"gpui",
"itertools 0.13.0",
"language",
@@ -1006,9 +1006,12 @@ impl Context {
cx: &mut ModelContext<Self>,
) {
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);
@@ -175,7 +175,10 @@ impl ChannelBuffer {
cx: &mut ModelContext<Self>,
) {
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() {
@@ -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.values[timestamp.replica_id as usize];
+ *entry = cmp::max(*entry, timestamp.value);
}
-
- let entry = &mut self.0[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<Item = Lamport> + '_ {
- 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, "<branch>: {}", self.local_branch_value)?;
+ }
write!(f, "}}")
}
}
@@ -273,6 +273,7 @@ gpui::actions!(
NextScreen,
OpenExcerpts,
OpenExcerptsSplit,
+ OpenProposedChangesEditor,
OpenFile,
OpenPermalinkToLine,
OpenUrl,
@@ -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<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
multi_buffer: &Model<MultiBuffer>,
- hunk: &DiffHunk<MultiBufferRow>,
+ 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<Item = DiffHunk<MultiBufferRow>>,
+ hunks: impl Iterator<Item = MultiBufferDiffHunk>,
cx: &mut ViewContext<Editor>,
) -> 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<Self>,
+ ) {
+ 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::<usize>(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::<Vec<_>>();
+ 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>) {
self.open_excerpts_common(true, cx)
}
@@ -12399,7 +12449,7 @@ impl Editor {
fn hunks_for_selections(
multi_buffer_snapshot: &MultiBufferSnapshot,
selections: &[Selection<Anchor>],
-) -> Vec<DiffHunk<MultiBufferRow>> {
+) -> Vec<MultiBufferDiffHunk> {
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<Item = Range<MultiBufferRow>>,
multi_buffer_snapshot: &MultiBufferSnapshot,
-) -> Vec<DiffHunk<MultiBufferRow>> {
+) -> Vec<MultiBufferDiffHunk> {
let mut hunks = Vec::new();
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
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<DisplayRow> {
}
}
-fn hunk_status(hunk: &DiffHunk<MultiBufferRow>) -> 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
@@ -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 {
@@ -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<MultiBufferRow>,
+ 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::<Vec<_>>(),
&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::<Vec<_>>(),
expected
.iter()
@@ -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<DiffHunk<MultiBufferRow>>,
+ hunks_to_toggle: Vec<MultiBufferDiffHunk>,
cx: &mut ViewContext<Self>,
) {
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<DiffHunk<MultiBufferRow>> {
+) -> Option<MultiBufferDiffHunk> {
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<Point>,
-) -> Option<DiffHunk<MultiBufferRow>> {
+) -> Option<MultiBufferDiffHunk> {
let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
);
@@ -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<Editor>,
+ _subscriptions: Vec<Subscription>,
+ _recalculate_diffs_task: Task<Option<()>>,
+ recalculate_diffs_tx: mpsc::UnboundedSender<Model<Buffer>>,
+}
+
+pub struct ProposedChangesBuffer<T> {
+ pub buffer: Model<Buffer>,
+ pub ranges: Vec<Range<T>>,
+}
+
+impl ProposedChangesEditor {
+ pub fn new<T: ToOffset>(
+ buffers: Vec<ProposedChangesBuffer<T>>,
+ project: Option<Model<Project>>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Buffer>,
+ event: &BufferEvent,
+ _cx: &mut ViewContext<Self>,
+ ) {
+ if let BufferEvent::Edited = event {
+ self.recalculate_diffs_tx.unbounded_send(buffer).ok();
+ }
+ }
+}
+
+impl Render for ProposedChangesEditor {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ self.editor.clone()
+ }
+}
+
+impl FocusableView for ProposedChangesEditor {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.editor.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
+
+impl Item for ProposedChangesEditor {
+ type Event = EditorEvent;
+
+ fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
+ Some(Icon::new(IconName::Pencil))
+ }
+
+ fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
+ Some("Proposed changes".into())
+ }
+}
@@ -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)
@@ -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<T> {
- /// 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<T>,
- /// 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<u32>,
+ /// The range in the buffer to which this hunk corresponds.
pub buffer_range: Range<Anchor>,
- /// 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<usize>,
}
-impl sum_tree::Item for DiffHunk<Anchor> {
+/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
+#[derive(Debug, Clone)]
+struct InternalDiffHunk {
+ buffer_range: Range<Anchor>,
+ diff_base_byte_range: Range<usize>,
+}
+
+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<clock::Global>,
- tree: SumTree<DiffHunk<Anchor>>,
+ tree: SumTree<InternalDiffHunk>,
}
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<u32>,
buffer: &'a BufferSnapshot,
- ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
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<Anchor>,
buffer: &'a BufferSnapshot,
- ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
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<Anchor>,
buffer: &'a BufferSnapshot,
- ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
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<Item = DiffHunk<u32>> {
+ fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
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<Anchor> {
+ ) -> 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<Iter>(
diff_base: &str,
expected_hunks: &[(Range<u32>, &str, &str)],
) where
- Iter: Iterator<Item = DiffHunk<u32>>,
+ Iter: Iterator<Item = DiffHunk>,
{
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::<String>(),
)
@@ -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<Buffer>, 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<Rope>,
+ diff_base: Option<BufferDiffBase>,
git_diff: git::diff::BufferDiff,
file: Option<Arc<dyn File>>,
/// 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<gpui::Subscription>,
}
#[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<Self>) -> Model<Self> {
+ 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<Self>, cx: &mut ModelContext<Self>) {
+ let branch = branch.read(cx);
+ let edits = branch
+ .edits_since::<usize>(&self.version)
+ .map(|edit| {
+ (
+ edit.old,
+ branch.text_for_range(edit.new).collect::<String>(),
+ )
+ })
+ .collect::<Vec<_>>();
+ 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<String>, cx: &mut ModelContext<Self>) {
- 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<Self>) -> Option<Task<()>> {
- let diff_base = self.diff_base.clone()?;
+ /// Recomputes the diff.
+ pub fn recalculate_diff(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
+ 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<Self>) {
@@ -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::<Vec<_>>();
+ 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<Self>) {
- cx.emit(BufferEvent::Operation(operation));
+ fn send_operation(
+ &mut self,
+ operation: Operation,
+ is_local: bool,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<BufferRow>,
- ) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
+ ) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
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<Anchor>,
- ) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
+ ) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
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<Anchor>,
- ) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
+ ) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
self.git_diff.hunks_intersecting_range_rev(range, self)
}
@@ -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<Buffer>,
+ cx: &mut TestAppContext,
+ expected_hunks: &[(Range<u32>, &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)],
);
}
})
@@ -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
@@ -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<MultiBufferRow>,
+ /// 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<text::Anchor>,
+ /// The range within the buffer's diff base that this hunk corresponds to.
+ pub diff_base_byte_range: Range<usize>,
+}
+
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<MultiBufferRow>,
- ) -> impl Iterator<Item = DiffHunk<MultiBufferRow>> + '_ {
+ ) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
let mut cursor = self.excerpts.cursor::<Point>(&());
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<MultiBufferRow>,
- ) -> impl Iterator<Item = DiffHunk<MultiBufferRow>> + '_ {
+ ) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
let mut cursor = self.excerpts.cursor::<Point>(&());
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,
}
});
@@ -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()
})
@@ -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()),
}
})
@@ -146,12 +146,15 @@ impl HeadlessProject {
cx: &mut ModelContext<Self>,
) {
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(),
_ => {}
@@ -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
}