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:
parent
31796171de
commit
a2115e7242
29 changed files with 1832 additions and 1651 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue