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 <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-09-20 15:28:50 -07:00 committed by GitHub
parent e309fbda2a
commit 743feb98bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 622 additions and 186 deletions

1
Cargo.lock generated
View file

@ -7055,7 +7055,6 @@ dependencies = [
"ctor",
"env_logger",
"futures 0.3.30",
"git",
"gpui",
"itertools 0.13.0",
"language",

View file

@ -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);

View file

@ -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() {

View file

@ -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<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, "}}")
}
}

View file

@ -273,6 +273,7 @@ gpui::actions!(
NextScreen,
OpenExcerpts,
OpenExcerptsSplit,
OpenProposedChangesEditor,
OpenFile,
OpenPermalinkToLine,
OpenUrl,

View file

@ -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

View file

@ -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 {

View file

@ -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()

View file

@ -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),
);

View file

@ -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())
}
}

View file

@ -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)

View file

@ -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>(),
)

View file

@ -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)
}

View file

@ -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)],
);
}
})

View file

@ -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

View file

@ -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,
}
});

View file

@ -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()
})

View file

@ -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()),
}
})

View file

@ -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(),
_ => {}

View file

@ -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
}