Restructure git diff state management to allow viewing buffers with different diff bases (#21258)

This is a pure refactor of our Git diff state management. Buffers are no
longer are associated with one single diff (the unstaged changes).
Instead, there is an explicit project API for retrieving a buffer's
unstaged changes, and the `Editor` view layer is responsible for
choosing what diff to associate with a buffer.

The reason for this change is that we'll soon want to add multiple "git
diff views" to Zed, one of which will show the *uncommitted* changes for
a buffer. But that view will need to co-exist with other views of the
same buffer, which may want to show the unstaged changes.

### Todo

* [x] Get git gutter and git hunks working with new structure
* [x] Update editor tests to use new APIs
* [x] Update buffer tests
* [x] Restructure remoting/collab protocol
* [x] Update assertions about staged text in
`random_project_collaboration_tests`
* [x] Move buffer tests for git diff management to a new spot, using the
new APIs

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-12-04 15:02:33 -08:00 committed by GitHub
parent 31796171de
commit a2115e7242
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1832 additions and 1651 deletions

View file

@ -83,7 +83,7 @@ use gpui::{
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
pub(crate) use hunk_diff::HoveredHunk;
use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot};
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion::Direction;
@ -625,7 +625,7 @@ pub struct Editor {
enable_inline_completions: bool,
show_inline_completions_override: Option<bool>,
inlay_hint_cache: InlayHintCache,
expanded_hunks: ExpandedHunks,
diff_map: DiffMap,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
@ -692,6 +692,7 @@ pub struct EditorSnapshot {
git_blame_gutter_max_author_length: Option<usize>,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
diff_map: DiffMapSnapshot,
is_focused: bool,
scroll_anchor: ScrollAnchor,
ongoing_scroll: OngoingScroll,
@ -2002,11 +2003,10 @@ impl Editor {
}
}
let inlay_hint_settings = inlay_hint_settings(
selections.newest_anchor().head(),
&buffer.read(cx).snapshot(cx),
cx,
);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let inlay_hint_settings =
inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx);
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::handle_focus).detach();
cx.on_focus_in(&focus_handle, Self::handle_focus_in)
@ -2023,6 +2023,28 @@ impl Editor {
let mut code_action_providers = Vec::new();
if let Some(project) = project.clone() {
let mut tasks = Vec::new();
buffer.update(cx, |multibuffer, cx| {
project.update(cx, |project, cx| {
multibuffer.for_each_buffer(|buffer| {
tasks.push(project.open_unstaged_changes(buffer.clone(), cx))
});
});
});
cx.spawn(|this, mut cx| async move {
let change_sets = futures::future::join_all(tasks).await;
this.update(&mut cx, |this, cx| {
for change_set in change_sets {
if let Some(change_set) = change_set.log_err() {
this.diff_map.add_change_set(change_set, cx);
}
}
})
.ok();
})
.detach();
code_action_providers.push(Arc::new(project) as Arc<_>);
}
@ -2105,7 +2127,7 @@ impl Editor {
inline_completion_provider: None,
active_inline_completion: None,
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
expanded_hunks: ExpandedHunks::default(),
diff_map: DiffMap::default(),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
@ -2365,6 +2387,7 @@ impl Editor {
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
placeholder_text: self.placeholder_text.clone(),
diff_map: self.diff_map.snapshot(),
is_focused: self.focus_handle.is_focused(cx),
current_line_highlight: self
.current_line_highlight
@ -6503,12 +6526,12 @@ impl Editor {
pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext<Self>) {
let mut revert_changes = HashMap::default();
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
for hunk in hunks_for_rows(
Some(MultiBufferRow(0)..multi_buffer_snapshot.max_row()).into_iter(),
&multi_buffer_snapshot,
let snapshot = self.snapshot(cx);
for hunk in hunks_for_ranges(
Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(),
&snapshot,
) {
Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx);
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
}
if !revert_changes.is_empty() {
self.transact(cx, |editor, cx| {
@ -6525,7 +6548,7 @@ impl Editor {
}
pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext<Self>) {
let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx);
let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx);
if !revert_changes.is_empty() {
self.transact(cx, |editor, cx| {
editor.revert(revert_changes, cx);
@ -6533,6 +6556,18 @@ impl Editor {
}
}
fn revert_hunk(&mut self, hunk: HoveredHunk, cx: &mut ViewContext<Editor>) {
let snapshot = self.buffer.read(cx).read(cx);
if let Some(hunk) = crate::hunk_diff::to_diff_hunk(&hunk, &snapshot) {
drop(snapshot);
let mut revert_changes = HashMap::default();
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
if !revert_changes.is_empty() {
self.revert(revert_changes, cx)
}
}
}
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
let project_path = buffer.read(cx).project_path(cx)?;
@ -6552,26 +6587,33 @@ impl Editor {
fn gather_revert_changes(
&mut self,
selections: &[Selection<Anchor>],
selections: &[Selection<Point>],
cx: &mut ViewContext<'_, Editor>,
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
let mut revert_changes = HashMap::default();
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) {
Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx);
let snapshot = self.snapshot(cx);
for hunk in hunks_for_selections(&snapshot, selections) {
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
}
revert_changes
}
pub fn prepare_revert_change(
&mut self,
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
multi_buffer: &Model<MultiBuffer>,
hunk: &MultiBufferDiffHunk,
cx: &AppContext,
) -> Option<()> {
let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?;
let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?;
let buffer = buffer.read(cx);
let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone());
let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set;
let original_text = change_set
.read(cx)
.base_text
.as_ref()?
.read(cx)
.as_rope()
.slice(hunk.diff_base_byte_range.clone());
let buffer_snapshot = buffer.snapshot();
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
@ -9752,80 +9794,63 @@ impl Editor {
}
fn go_to_next_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
let snapshot = self
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let snapshot = self.snapshot(cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_position(&snapshot, selection.head(), cx);
}
fn go_to_hunk_after_position(
&mut self,
snapshot: &DisplaySnapshot,
snapshot: &EditorSnapshot,
position: Point,
cx: &mut ViewContext<'_, Editor>,
) -> Option<MultiBufferDiffHunk> {
if let Some(hunk) = self.go_to_next_hunk_in_direction(
snapshot,
position,
false,
snapshot
.buffer_snapshot
.git_diff_hunks_in_range(MultiBufferRow(position.row + 1)..MultiBufferRow::MAX),
cx,
) {
return Some(hunk);
for (ix, position) in [position, Point::zero()].into_iter().enumerate() {
if let Some(hunk) = self.go_to_next_hunk_in_direction(
snapshot,
position,
ix > 0,
snapshot.diff_map.diff_hunks_in_range(
position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(),
&snapshot.buffer_snapshot,
),
cx,
) {
return Some(hunk);
}
}
let wrapped_point = Point::zero();
self.go_to_next_hunk_in_direction(
snapshot,
wrapped_point,
true,
snapshot.buffer_snapshot.git_diff_hunks_in_range(
MultiBufferRow(wrapped_point.row + 1)..MultiBufferRow::MAX,
),
cx,
)
None
}
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
let snapshot = self
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let snapshot = self.snapshot(cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_before_position(&snapshot, selection.head(), cx);
}
fn go_to_hunk_before_position(
&mut self,
snapshot: &DisplaySnapshot,
snapshot: &EditorSnapshot,
position: Point,
cx: &mut ViewContext<'_, Editor>,
) -> Option<MultiBufferDiffHunk> {
if let Some(hunk) = self.go_to_next_hunk_in_direction(
snapshot,
position,
false,
snapshot
.buffer_snapshot
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(position.row)),
cx,
) {
return Some(hunk);
for (ix, position) in [position, snapshot.buffer_snapshot.max_point()]
.into_iter()
.enumerate()
{
if let Some(hunk) = self.go_to_next_hunk_in_direction(
snapshot,
position,
ix > 0,
snapshot
.diff_map
.diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot),
cx,
) {
return Some(hunk);
}
}
let wrapped_point = snapshot.buffer_snapshot.max_point();
self.go_to_next_hunk_in_direction(
snapshot,
wrapped_point,
true,
snapshot
.buffer_snapshot
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(wrapped_point.row)),
cx,
)
None
}
fn go_to_next_hunk_in_direction(
@ -11270,13 +11295,13 @@ impl Editor {
return;
}
let mut buffers_affected = HashMap::default();
let mut buffers_affected = HashSet::default();
let multi_buffer = self.buffer().read(cx);
for crease in &creases {
if let Some((_, buffer, _)) =
multi_buffer.excerpt_containing(crease.range().start.clone(), cx)
{
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
buffers_affected.insert(buffer.read(cx).remote_id());
};
}
@ -11286,8 +11311,8 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx);
}
for buffer in buffers_affected.into_values() {
self.sync_expanded_diff_hunks(buffer, cx);
for buffer_id in buffers_affected {
Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx);
}
cx.notify();
@ -11344,11 +11369,11 @@ impl Editor {
return;
}
let mut buffers_affected = HashMap::default();
let mut buffers_affected = HashSet::default();
let multi_buffer = self.buffer().read(cx);
for range in ranges {
if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) {
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
buffers_affected.insert(buffer.read(cx).remote_id());
};
}
@ -11358,8 +11383,8 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx);
}
for buffer in buffers_affected.into_values() {
self.sync_expanded_diff_hunks(buffer, cx);
for buffer_id in buffers_affected {
Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx);
}
cx.notify();
@ -12653,15 +12678,11 @@ impl Editor {
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
cx.emit(EditorEvent::TitleChanged)
}
multi_buffer::Event::DiffBaseChanged => {
self.scrollbar_marker_state.dirty = true;
cx.emit(EditorEvent::DiffBaseChanged);
cx.notify();
}
multi_buffer::Event::DiffUpdated { buffer } => {
self.sync_expanded_diff_hunks(buffer.clone(), cx);
cx.notify();
}
// multi_buffer::Event::DiffBaseChanged => {
// self.scrollbar_marker_state.dirty = true;
// cx.emit(EditorEvent::DiffBaseChanged);
// cx.notify();
// }
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
@ -12829,7 +12850,7 @@ impl Editor {
// When editing branch buffers, jump to the corresponding location
// in their base buffer.
let buffer = buffer_handle.read(cx);
if let Some(base_buffer) = buffer.diff_base_buffer() {
if let Some(base_buffer) = buffer.base_buffer() {
range = buffer.range_to_version(range, &base_buffer.read(cx).version());
buffer_handle = base_buffer;
}
@ -13606,35 +13627,29 @@ fn test_wrap_with_prefix() {
}
fn hunks_for_selections(
multi_buffer_snapshot: &MultiBufferSnapshot,
selections: &[Selection<Anchor>],
snapshot: &EditorSnapshot,
selections: &[Selection<Point>],
) -> Vec<MultiBufferDiffHunk> {
let buffer_rows_for_selections = selections.iter().map(|selection| {
let head = selection.head();
let tail = selection.tail();
let start = MultiBufferRow(tail.to_point(multi_buffer_snapshot).row);
let end = MultiBufferRow(head.to_point(multi_buffer_snapshot).row);
if start > end {
end..start
} else {
start..end
}
});
hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot)
hunks_for_ranges(
selections.iter().map(|selection| selection.range()),
snapshot,
)
}
pub fn hunks_for_rows(
rows: impl Iterator<Item = Range<MultiBufferRow>>,
multi_buffer_snapshot: &MultiBufferSnapshot,
pub fn hunks_for_ranges(
ranges: impl Iterator<Item = Range<Point>>,
snapshot: &EditorSnapshot,
) -> Vec<MultiBufferDiffHunk> {
let mut hunks = Vec::new();
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
HashMap::default();
for selected_multi_buffer_rows in rows {
for query_range in ranges {
let query_rows =
selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row();
for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) {
MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1);
for hunk in snapshot.diff_map.diff_hunks_in_range(
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
&snapshot.buffer_snapshot,
) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
@ -13643,10 +13658,7 @@ pub fn hunks_for_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.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
hunk.row_range.overlaps(&query_rows)
};
if related_to_selection {
if !processed_buffer_rows