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
co-authored by Nate Butler Conrad Irwin
parent bf4645b1fe
commit 18c2e8f6ca
6 changed files with 628 additions and 169 deletions
+121 -33
View File
@@ -78,7 +78,7 @@ use gpui::{
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
use hunk_diff::ExpandedHunks;
pub(crate) use hunk_diff::HunkToExpand;
pub(crate) use hunk_diff::HoveredHunk;
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
@@ -2873,7 +2873,10 @@ impl Editor {
}
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
self.clear_expanded_diff_hunks(cx);
if self.clear_clicked_diff_hunks(cx) {
cx.notify();
return;
}
if self.dismiss_menus_and_popups(true, cx) {
return;
}
@@ -2908,6 +2911,10 @@ impl Editor {
return true;
}
if self.mouse_context_menu.take().is_some() {
return true;
}
if self.discard_inline_completion(should_report_inline_completion_event, cx) {
return true;
}
@@ -5125,6 +5132,23 @@ impl Editor {
}))
}
fn render_close_hunk_diff_button(
&self,
hunk: HoveredHunk,
row: DisplayRow,
cx: &mut ViewContext<Self>,
) -> IconButton {
IconButton::new(
("close_hunk_diff_indicator", row.0 as usize),
ui::IconName::Close,
)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(|cx| Tooltip::for_action("Close hunk diff", &ToggleHunkDiff, cx))
.on_click(cx.listener(move |editor, _e, cx| editor.toggle_hovered_hunk(&hunk, cx)))
}
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.read()
@@ -5879,22 +5903,7 @@ impl Editor {
let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx);
if !revert_changes.is_empty() {
self.transact(cx, |editor, cx| {
editor.buffer().update(cx, |multi_buffer, cx| {
for (buffer_id, changes) in revert_changes {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(
changes.into_iter().map(|(range, text)| {
(range, text.to_string().map(Arc::<str>::from))
}),
None,
cx,
);
});
}
}
});
editor.change_selections(None, cx, |selections| selections.refresh());
editor.revert(revert_changes, cx);
});
}
}
@@ -5924,22 +5933,20 @@ impl Editor {
cx: &mut ViewContext<'_, Editor>,
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
let mut revert_changes = HashMap::default();
self.buffer.update(cx, |multi_buffer, cx| {
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) {
Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx);
}
});
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);
}
revert_changes
}
fn prepare_revert_change(
pub fn prepare_revert_change(
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
multi_buffer: &MultiBuffer,
multi_buffer: &Model<MultiBuffer>,
hunk: &DiffHunk<MultiBufferRow>,
cx: &mut AppContext,
cx: &AppContext,
) -> Option<()> {
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
let buffer = multi_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 buffer_snapshot = buffer.snapshot();
@@ -11737,15 +11744,81 @@ impl Editor {
pub fn file_header_size(&self) -> u8 {
self.file_header_size
}
pub fn revert(
&mut self,
revert_changes: HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
cx: &mut ViewContext<Self>,
) {
self.buffer().update(cx, |multi_buffer, cx| {
for (buffer_id, changes) in revert_changes {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(
changes.into_iter().map(|(range, text)| {
(range, text.to_string().map(Arc::<str>::from))
}),
None,
cx,
);
});
}
}
});
self.change_selections(None, cx, |selections| selections.refresh());
}
pub fn to_pixel_point(
&mut self,
source: multi_buffer::Anchor,
editor_snapshot: &EditorSnapshot,
cx: &mut ViewContext<Self>,
) -> Option<gpui::Point<Pixels>> {
let text_layout_details = self.text_layout_details(cx);
let line_height = text_layout_details
.editor_style
.text
.line_height_in_pixels(cx.rem_size());
let source_point = source.to_display_point(editor_snapshot);
let first_visible_line = text_layout_details
.scroll_anchor
.anchor
.to_display_point(editor_snapshot);
if first_visible_line > source_point {
return None;
}
let source_x = editor_snapshot.x_for_display_point(source_point, &text_layout_details);
let source_y = line_height
* ((source_point.row() - first_visible_line.row()).0 as f32
- text_layout_details.scroll_anchor.offset.y);
Some(gpui::Point::new(source_x, source_y))
}
pub fn display_to_pixel_point(
&mut self,
source: DisplayPoint,
editor_snapshot: &EditorSnapshot,
cx: &mut ViewContext<Self>,
) -> Option<gpui::Point<Pixels>> {
let line_height = self.style()?.text.line_height_in_pixels(cx.rem_size());
let text_layout_details = self.text_layout_details(cx);
let first_visible_line = text_layout_details
.scroll_anchor
.anchor
.to_display_point(editor_snapshot);
if first_visible_line > source {
return None;
}
let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details);
let source_y = line_height * (source.row() - first_visible_line.row()).0 as f32;
Some(gpui::Point::new(source_x, source_y))
}
}
fn hunks_for_selections(
multi_buffer_snapshot: &MultiBufferSnapshot,
selections: &[Selection<Anchor>],
) -> Vec<DiffHunk<MultiBufferRow>> {
let mut hunks = Vec::with_capacity(selections.len());
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
HashMap::default();
let buffer_rows_for_selections = selections.iter().map(|selection| {
let head = selection.head();
let tail = selection.tail();
@@ -11758,7 +11831,17 @@ fn hunks_for_selections(
}
});
for selected_multi_buffer_rows in buffer_rows_for_selections {
hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot)
}
pub fn hunks_for_rows(
rows: impl Iterator<Item = Range<MultiBufferRow>>,
multi_buffer_snapshot: &MultiBufferSnapshot,
) -> Vec<DiffHunk<MultiBufferRow>> {
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 {
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()) {
@@ -12968,8 +13051,13 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
})
}
pub trait RangeToAnchorExt {
pub trait RangeToAnchorExt: Sized {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
fn to_display_points(self, snapshot: &EditorSnapshot) -> Range<DisplayPoint> {
let anchor_range = self.to_anchors(&snapshot.buffer_snapshot);
anchor_range.start.to_display_point(&snapshot)..anchor_range.end.to_display_point(&snapshot)
}
}
impl<T: ToOffset> RangeToAnchorExt for Range<T> {