Rework mouse handling of git hunks diff (#14727)

Closes https://github.com/zed-industries/zed/issues/12404

![Screenshot 2024-07-18 at 14 02
31](https://github.com/user-attachments/assets/a8addd22-0ed9-4f4b-852a-f347314c27ce)

![Screenshot 2024-07-18 at 14 02
43](https://github.com/user-attachments/assets/0daaed10-b9f3-4d4b-b8d7-189aa7e013b9)

Video:


https://github.com/user-attachments/assets/58e62527-da75-4017-a43e-a37803bd7b49


* now shows a context menu on left click instead of expanding the hunk
diff
* hunk diffs can be toggled with a single cmd-click still
* adds a X mark into gutter for every hunk expanded
* makes `editor::ToggleDiffHunk` to work inside the deleted hunk editors

Additionally, changes the way editor context menus behave when the
editor is scrolled — right click and diff hunks context menu now will
stick to the place it was invoked at, instead of staying onscreen at the
same pixel positions.

Release Notes:

- Improved the way git hunks diff can be toggled with mouse
([#12404](https://github.com/zed-industries/zed/issues/12404))

---------

Co-authored-by: Nate Butler <nate@zed.dev>
Co-authored-by: Conrad Irwin <conrad@zed.dev>
This commit is contained in:
Kirill Bulatov 2024-07-19 13:48:04 +03:00 committed by GitHub
parent bf4645b1fe
commit 18c2e8f6ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 628 additions and 169 deletions

View file

@ -5,28 +5,31 @@ use std::{
use collections::{hash_map, HashMap, HashSet};
use git::diff::{DiffHunk, DiffHunkStatus};
use gpui::{AppContext, Hsla, Model, Task, View};
use gpui::{Action, AppContext, Hsla, Model, MouseButton, Subscription, Task, View};
use language::Buffer;
use multi_buffer::{
Anchor, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
};
use settings::SettingsStore;
use text::{BufferId, Point};
use ui::{
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
ParentElement, Pixels, Styled, ViewContext, VisualContext,
};
use util::{debug_panic, RangeExt};
use crate::{
editor_settings::CurrentLineHighlight,
git::{diff_hunk_to_display, DisplayDiffHunk},
hunk_status, hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle,
DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
hunk_status, hunks_for_selections,
mouse_context_menu::MouseContextMenu,
BlockDisposition, BlockId, BlockProperties, BlockStyle, DiffRowHighlight, Editor,
EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint,
ToggleHunkDiff,
};
#[derive(Debug, Clone)]
pub(super) struct HunkToExpand {
pub(super) struct HoveredHunk {
pub multi_buffer_range: Range<Anchor>,
pub status: DiffHunkStatus,
pub diff_base_byte_range: Range<usize>,
@ -63,6 +66,123 @@ pub(super) struct ExpandedHunk {
}
impl Editor {
pub(super) fn open_hunk_context_menu(
&mut self,
hovered_hunk: HoveredHunk,
clicked_point: gpui::Point<Pixels>,
cx: &mut ViewContext<Editor>,
) {
let focus_handle = self.focus_handle.clone();
let expanded = self
.expanded_hunks
.hunks(false)
.any(|expanded_hunk| expanded_hunk.hunk_range == hovered_hunk.multi_buffer_range);
let editor_handle = cx.view().clone();
let editor_snapshot = self.snapshot(cx);
let start_point = self
.to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
.unwrap_or(clicked_point);
let end_point = self
.to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
.unwrap_or(clicked_point);
let norm =
|a: gpui::Point<Pixels>, b: gpui::Point<Pixels>| (a.x - b.x).abs() + (a.y - b.y).abs();
let closest_source = if norm(start_point, clicked_point) < norm(end_point, clicked_point) {
hovered_hunk.multi_buffer_range.start
} else {
hovered_hunk.multi_buffer_range.end
};
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
self,
closest_source,
clicked_point,
ContextMenu::build(cx, move |menu, _| {
menu.on_blur_subscription(Subscription::new(|| {}))
.context(focus_handle)
.entry(
if expanded {
"Collapse Hunk"
} else {
"Expand Hunk"
},
Some(ToggleHunkDiff.boxed_clone()),
{
let editor = editor_handle.clone();
let hunk = hovered_hunk.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
});
}
},
)
.entry("Revert Hunk", Some(RevertSelectedHunks.boxed_clone()), {
let editor = editor_handle.clone();
let hunk = hovered_hunk.clone();
move |cx| {
let multi_buffer = editor.read(cx).buffer().clone();
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let mut revert_changes = HashMap::default();
if let Some(hunk) =
crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot)
{
Editor::prepare_revert_change(
&mut revert_changes,
&multi_buffer,
&hunk,
cx,
);
}
if !revert_changes.is_empty() {
editor.update(cx, |editor, cx| editor.revert(revert_changes, cx));
}
}
})
.entry("Revert File", None, {
let editor = editor_handle.clone();
move |cx| {
let mut revert_changes = HashMap::default();
let multi_buffer = editor.read(cx).buffer().clone();
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
for hunk in crate::hunks_for_rows(
Some(MultiBufferRow(0)..multi_buffer_snapshot.max_buffer_row())
.into_iter(),
&multi_buffer_snapshot,
) {
Editor::prepare_revert_change(
&mut revert_changes,
&multi_buffer,
&hunk,
cx,
);
}
if !revert_changes.is_empty() {
editor.update(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.revert(revert_changes, cx);
});
});
}
}
})
}),
cx,
)
}
pub(super) fn toggle_hovered_hunk(
&mut self,
hovered_hunk: &HoveredHunk,
cx: &mut ViewContext<Editor>,
) {
let editor_snapshot = self.snapshot(cx);
if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) {
self.toggle_hunks_expanded(vec![diff_hunk], cx);
self.change_selections(None, cx, |selections| selections.refresh());
}
}
pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let selections = self.selections.disjoint_anchors();
@ -164,7 +284,7 @@ impl Editor {
retain = false;
break;
} else {
hunks_to_expand.push(HunkToExpand {
hunks_to_expand.push(HoveredHunk {
status,
multi_buffer_range,
diff_base_byte_range,
@ -182,7 +302,7 @@ impl Editor {
let remaining_hunk_point_range =
Point::new(remaining_hunk.associated_range.start.0, 0)
..Point::new(remaining_hunk.associated_range.end.0, 0);
hunks_to_expand.push(HunkToExpand {
hunks_to_expand.push(HoveredHunk {
status: hunk_status(&remaining_hunk),
multi_buffer_range: remaining_hunk_point_range
.to_anchors(&snapshot.buffer_snapshot),
@ -215,7 +335,7 @@ impl Editor {
pub(super) fn expand_diff_hunk(
&mut self,
diff_base_buffer: Option<Model<Buffer>>,
hunk: &HunkToExpand,
hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Editor>,
) -> Option<()> {
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
@ -303,28 +423,58 @@ impl Editor {
&mut self,
diff_base_buffer: Model<Buffer>,
deleted_text_height: u8,
hunk: &HunkToExpand,
hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Self>,
) -> Option<BlockId> {
let deleted_hunk_color = deleted_hunk_color(cx);
let (editor_height, editor_with_deleted_text) =
editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
let editor = cx.view().clone();
let editor_model = cx.model().clone();
let hunk = hunk.clone();
let mut new_block_ids = self.insert_blocks(
Some(BlockProperties {
position: hunk.multi_buffer_range.start,
height: editor_height.max(deleted_text_height),
style: BlockStyle::Flex,
disposition: BlockDisposition::Above,
render: Box::new(move |cx| {
let close_button = editor.update(cx.context, |editor, cx| {
let editor_snapshot = editor.snapshot(cx);
let hunk_start_row = hunk
.multi_buffer_range
.start
.to_display_point(&editor_snapshot)
.row();
editor.render_close_hunk_diff_button(hunk.clone(), hunk_start_row, cx)
});
let gutter_dimensions = editor_model.read(cx).gutter_dimensions;
div()
let click_editor = editor.clone();
h_flex()
.bg(deleted_hunk_color)
.size_full()
.pl(gutter_dimensions.full_width())
.child(
v_flex()
.justify_center()
.max_w(gutter_dimensions.full_width())
.min_w(gutter_dimensions.full_width())
.size_full()
.on_mouse_down(MouseButton::Left, {
let click_hunk = hunk.clone();
move |e, cx| {
let modifiers = e.modifiers;
if modifiers.control || modifiers.platform {
click_editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&click_hunk, cx);
});
}
}
})
.child(close_button),
)
.child(editor_with_deleted_text.clone())
.into_any_element()
}),
disposition: BlockDisposition::Above,
}),
None,
cx,
@ -339,16 +489,21 @@ impl Editor {
}
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) {
pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
self.expanded_hunks.hunk_update_tasks.clear();
self.clear_row_highlights::<DiffRowHighlight>();
let to_remove = self
.expanded_hunks
.hunks
.drain(..)
.filter_map(|expanded_hunk| expanded_hunk.block)
.collect();
self.clear_row_highlights::<DiffRowHighlight>();
self.remove_blocks(to_remove, None, cx);
.collect::<HashSet<_>>();
if to_remove.is_empty() {
false
} else {
self.remove_blocks(to_remove, None, cx);
true
}
}
pub(super) fn sync_expanded_diff_hunks(
@ -457,7 +612,7 @@ impl Editor {
recalculated_hunks.next();
retain = true;
} else {
hunks_to_reexpand.push(HunkToExpand {
hunks_to_reexpand.push(HoveredHunk {
status,
multi_buffer_range,
diff_base_byte_range,
@ -522,6 +677,29 @@ impl Editor {
}
}
fn to_diff_hunk(
hovered_hunk: &HoveredHunk,
multi_buffer_snapshot: &MultiBufferSnapshot,
) -> Option<DiffHunk<MultiBufferRow>> {
let buffer_id = hovered_hunk
.multi_buffer_range
.start
.buffer_id
.or_else(|| hovered_hunk.multi_buffer_range.end.buffer_id)?;
let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
..hovered_hunk.multi_buffer_range.end.text_anchor;
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),
buffer_id,
buffer_range,
diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
})
}
fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
buffer
.update(cx, |buffer, _| {
@ -555,7 +733,7 @@ fn deleted_hunk_color(cx: &AppContext) -> Hsla {
fn editor_with_deleted_text(
diff_base_buffer: Model<Buffer>,
deleted_color: Hsla,
hunk: &HunkToExpand,
hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Editor>,
) -> (u8, View<Editor>) {
let parent_editor = cx.view().downgrade();
@ -613,11 +791,12 @@ fn editor_with_deleted_text(
}
}),
]);
let parent_editor_for_reverts = parent_editor.clone();
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
let diff_base_range = hunk.diff_base_byte_range.clone();
editor
.register_action::<RevertSelectedHunks>(move |_, cx| {
parent_editor
parent_editor_for_reverts
.update(cx, |editor, cx| {
let Some((buffer, original_text)) =
editor.buffer().update(cx, |buffer, cx| {
@ -645,6 +824,16 @@ fn editor_with_deleted_text(
.ok();
})
.detach();
let hunk = hunk.clone();
editor
.register_action::<ToggleHunkDiff>(move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
})
.ok();
})
.detach();
editor
});