Expand git diffs when clicking the gutter strip, display their controls in a block above (#18313)

Todo:

* [x] Tooltips for hunk buttons
* [x] Buttons to go to next and previous hunk
* [x] Ellipsis button that opens a context menu with `Revert all`

/cc @iamnbutler @danilo-leal for design 👀 

Release Notes:

- Changed the behavior of the git gutter so that diff hunk are expanded
immediately when clicking the gutter, and hunk controls are displayed
above the hunk.

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Max Brunsfeld 2024-09-25 12:50:38 -07:00 committed by GitHub
parent ae6a3d15af
commit 21a023980d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 580 additions and 758 deletions

View file

@ -7,18 +7,12 @@ use collections::HashMap;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
RevertSelectedHunks, ToggleCodeActions, Undo,
},
display_map::DisplayRow,
test::{
editor_hunks,
editor_test_context::{AssertionContextManager, EditorTestContext},
expanded_hunks, expanded_hunks_background_highlights,
ToggleCodeActions, Undo,
},
test::editor_test_context::{AssertionContextManager, EditorTestContext},
Editor,
};
use futures::StreamExt;
use git::diff::DiffHunkStatus;
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
@ -1970,285 +1964,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
});
}
#[gpui::test]
async fn test_multiple_hunk_types_revert(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
client_a.language_registry().add(rust_lang());
client_b.language_registry().add(rust_lang());
let base_text = indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;
struct Row9;
struct Row10;"#};
client_a
.fs()
.insert_tree(
"/a",
json!({
"main.rs": base_text,
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let mut editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
editor: editor_a,
assertion_cx: AssertionContextManager::new(),
};
let mut editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),
editor: editor_b,
assertion_cx: AssertionContextManager::new(),
};
// host edits the file, that differs from the base text, producing diff hunks
editor_cx_a.set_state(indoc! {r#"struct Row;
struct Row0.1;
struct Row0.2;
struct Row1;
struct Row4;
struct Row5444;
struct Row6;
struct Row9;
struct Row1220;ˇ"#});
editor_cx_a.update_editor(|editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_diff_base(Some(base_text.into()), cx);
});
});
editor_cx_b.update_editor(|editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_diff_base(Some(base_text.into()), cx);
});
});
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
// the client selects a range in the updated buffer, expands it to see the diff for each hunk in the selection
// the host does not see the diffs toggled
editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
struct Row0.1;
struct Row0.2;
struct Row1;
struct Row4;
struct Row5444;
struct Row6;
struct R»ow9;
struct Row1220;"#});
editor_cx_b
.update_editor(|editor, cx| editor.toggle_hunk_diff(&editor::actions::ToggleHunkDiff, cx));
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
editor_cx_a.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
assert_eq!(
all_hunks,
vec![
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(1)..DisplayRow(3)
),
(
"struct Row2;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(4)..DisplayRow(4)
),
(
"struct Row5;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(7)
),
(
"struct Row8;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(9)..DisplayRow(9)
),
(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
DisplayRow(10)..DisplayRow(10),
),
]
);
assert_eq!(all_expanded_hunks, Vec::new());
});
editor_cx_b.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(1)..=DisplayRow(2), DisplayRow(8)..=DisplayRow(8)],
);
assert_eq!(
all_hunks,
vec![
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(1)..DisplayRow(3)
),
(
"struct Row2;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(5)..DisplayRow(5)
),
(
"struct Row5;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(8)..DisplayRow(9)
),
(
"struct Row8;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(12)..DisplayRow(12)
),
(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
DisplayRow(13)..DisplayRow(13),
),
]
);
assert_eq!(all_expanded_hunks, &all_hunks[..all_hunks.len() - 1]);
});
// the client reverts the hunks, removing the expanded diffs too
// both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
editor_cx_b.update_editor(|editor, cx| {
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
});
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
editor_cx_a.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
assert_eq!(
all_hunks,
vec![(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
DisplayRow(10)..DisplayRow(10),
)]
);
assert_eq!(all_expanded_hunks, Vec::new());
});
editor_cx_b.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
assert_eq!(
all_hunks,
vec![(
"struct Row10;".to_string(),
DiffHunkStatus::Modified,
DisplayRow(10)..DisplayRow(10),
)]
);
assert_eq!(all_expanded_hunks, Vec::new());
});
editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;
struct Row9;
struct Row1220;ˇ"#});
editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
struct Row1;
struct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;
struct R»ow9;
struct Row1220;"#});
}
#[gpui::test(iterations = 10)]
async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;

View file

@ -154,7 +154,7 @@ use theme::{
};
use ui::{
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
ListItem, Popover, Tooltip,
ListItem, Popover, PopoverMenuHandle, Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::{ItemHandle, PreviewTabsSettings};
@ -562,6 +562,7 @@ pub struct Editor {
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
signature_help_state: SignatureHelpState,
auto_signature_help: Option<bool>,
@ -1938,6 +1939,7 @@ impl Editor {
nav_history: None,
context_menu: RwLock::new(None),
mouse_context_menu: None,
hunk_controls_menu_handle: PopoverMenuHandle::default(),
completion_tasks: Default::default(),
signature_help_state: SignatureHelpState::default(),
auto_signature_help: None,
@ -5383,23 +5385,6 @@ impl Editor {
}))
}
fn 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()
@ -9335,32 +9320,42 @@ impl Editor {
}
}
fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
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 selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_position(&snapshot, selection.head(), cx);
}
if !self.seek_in_direction(
&snapshot,
selection.head(),
fn go_to_hunk_after_position(
&mut self,
snapshot: &DisplaySnapshot,
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(selection.head().row + 1)..MultiBufferRow::MAX,
),
snapshot
.buffer_snapshot
.git_diff_hunks_in_range(MultiBufferRow(position.row + 1)..MultiBufferRow::MAX),
cx,
) {
let wrapped_point = Point::zero();
self.seek_in_direction(
&snapshot,
wrapped_point,
true,
snapshot.buffer_snapshot.git_diff_hunks_in_range(
MultiBufferRow(wrapped_point.row + 1)..MultiBufferRow::MAX,
),
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,
)
}
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
@ -9369,52 +9364,65 @@ impl Editor {
.update(cx, |display_map, cx| display_map.snapshot(cx));
let selection = self.selections.newest::<Point>(cx);
if !self.seek_in_direction(
&snapshot,
selection.head(),
false,
snapshot.buffer_snapshot.git_diff_hunks_in_range_rev(
MultiBufferRow(0)..MultiBufferRow(selection.head().row),
),
cx,
) {
let wrapped_point = snapshot.buffer_snapshot.max_point();
self.seek_in_direction(
&snapshot,
wrapped_point,
true,
snapshot.buffer_snapshot.git_diff_hunks_in_range_rev(
MultiBufferRow(0)..MultiBufferRow(wrapped_point.row),
),
cx,
);
}
self.go_to_hunk_before_position(&snapshot, selection.head(), cx);
}
fn seek_in_direction(
fn go_to_hunk_before_position(
&mut self,
snapshot: &DisplaySnapshot,
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);
}
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,
)
}
fn go_to_next_hunk_in_direction(
&mut self,
snapshot: &DisplaySnapshot,
initial_point: Point,
is_wrapped: bool,
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
cx: &mut ViewContext<Editor>,
) -> bool {
) -> Option<MultiBufferDiffHunk> {
let display_point = initial_point.to_display_point(snapshot);
let mut hunks = hunks
.map(|hunk| diff_hunk_to_display(&hunk, snapshot))
.filter(|hunk| is_wrapped || !hunk.contains_display_row(display_point.row()))
.map(|hunk| (diff_hunk_to_display(&hunk, snapshot), hunk))
.filter(|(display_hunk, _)| {
is_wrapped || !display_hunk.contains_display_row(display_point.row())
})
.dedup();
if let Some(hunk) = hunks.next() {
if let Some((display_hunk, hunk)) = hunks.next() {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let row = hunk.start_display_row();
let row = display_hunk.start_display_row();
let point = DisplayPoint::new(row, 0);
s.select_display_ranges([point..point]);
});
true
Some(hunk)
} else {
false
None
}
}

View file

@ -9623,7 +9623,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext)
cx.update_editor(|editor, cx| {
//Wrap around the bottom of the buffer
for _ in 0..3 {
editor.go_to_hunk(&GoToHunk, cx);
editor.go_to_next_hunk(&GoToHunk, cx);
}
});
@ -9709,7 +9709,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext)
//Make sure that the fold only gets one hunk
for _ in 0..4 {
editor.go_to_hunk(&GoToHunk, cx);
editor.go_to_next_hunk(&GoToHunk, cx);
}
});
@ -11226,7 +11226,7 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
cx.update_editor(|editor, cx| {
for _ in 0..4 {
editor.go_to_hunk(&GoToHunk, cx);
editor.go_to_next_hunk(&GoToHunk, cx);
editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
}
});
@ -11249,18 +11249,13 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(1)..=DisplayRow(1), DisplayRow(7)..=DisplayRow(7), DisplayRow(9)..=DisplayRow(9)],
"After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
);
assert_eq!(
all_hunks,
vec![
("use some::mod;\n".to_string(), DiffHunkStatus::Modified, DisplayRow(1)..DisplayRow(2)),
("const A: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(4)..DisplayRow(4)),
(" println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(7)..DisplayRow(8)),
("".to_string(), DiffHunkStatus::Added, DisplayRow(9)..DisplayRow(10)),
("use some::mod;\n".to_string(), DiffHunkStatus::Modified, DisplayRow(2)..DisplayRow(3)),
("const A: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(6)..DisplayRow(6)),
(" println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(10)..DisplayRow(11)),
("".to_string(), DiffHunkStatus::Added, DisplayRow(13)..DisplayRow(14)),
],
"After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \
(from modified and removed hunks)"
@ -11269,6 +11264,11 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
all_hunks, all_expanded_hunks,
"Editor hunks should not change and all be expanded"
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(2)..=DisplayRow(2), DisplayRow(10)..=DisplayRow(10), DisplayRow(13)..=DisplayRow(13)],
"After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
);
});
cx.update_editor(|editor, cx| {
@ -11311,7 +11311,7 @@ async fn test_toggled_diff_base_change(
const B: u32 = 42;
const C: u32 = 42;
fn main(ˇ) {
fn main() {
println!("hello");
println!("world");
@ -11356,9 +11356,9 @@ async fn test_toggled_diff_base_change(
DisplayRow(3)..DisplayRow(3)
),
(
"fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
" println!(\"hello\");\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(5)..DisplayRow(7)
DisplayRow(6)..DisplayRow(7)
),
(
"".to_string(),
@ -11390,22 +11390,18 @@ async fn test_toggled_diff_base_change(
"#
.unindent(),
);
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(9)..=DisplayRow(10), DisplayRow(13)..=DisplayRow(14)],
"After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
);
assert_eq!(
all_hunks,
vec![
("use some::mod1;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(1)..DisplayRow(1)),
("const B: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(5)..DisplayRow(5)),
("fn main(ˇ) {\n println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(9)..DisplayRow(11)),
("".to_string(), DiffHunkStatus::Added, DisplayRow(13)..DisplayRow(15)),
("use some::mod1;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(2)..DisplayRow(2)),
("const B: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(7)..DisplayRow(7)),
(" println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(12)..DisplayRow(13)),
("".to_string(), DiffHunkStatus::Added, DisplayRow(16)..DisplayRow(18)),
],
"After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \
(from modified and removed hunks)"
@ -11414,6 +11410,11 @@ async fn test_toggled_diff_base_change(
all_hunks, all_expanded_hunks,
"Editor hunks should not change and all be expanded"
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(12)..=DisplayRow(12), DisplayRow(16)..=DisplayRow(17)],
"After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
);
});
cx.set_diff_base(Some("new diff base!"));
@ -11459,7 +11460,7 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
const B: u32 = 42;
const C: u32 = 42;
fn main(ˇ) {
fn main() {
println!("hello");
println!("world");
@ -11520,9 +11521,9 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
DisplayRow(3)..DisplayRow(3)
),
(
"fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
" println!(\"hello\");\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(5)..DisplayRow(7)
DisplayRow(6)..DisplayRow(7)
),
(
"".to_string(),
@ -11576,50 +11577,50 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![
DisplayRow(9)..=DisplayRow(10),
DisplayRow(13)..=DisplayRow(14),
DisplayRow(19)..=DisplayRow(19)
]
);
assert_eq!(
all_hunks,
vec![
(
"use some::mod1;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(1)..DisplayRow(1)
DisplayRow(2)..DisplayRow(2)
),
(
"const B: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(5)..DisplayRow(5)
DisplayRow(7)..DisplayRow(7)
),
(
"fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
" println!(\"hello\");\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(9)..DisplayRow(11)
DisplayRow(12)..DisplayRow(13)
),
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(13)..DisplayRow(15)
DisplayRow(16)..DisplayRow(18)
),
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(19)..DisplayRow(20)
DisplayRow(23)..DisplayRow(24)
),
(
"fn another2() {\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(23)..DisplayRow(23)
DisplayRow(28)..DisplayRow(28)
),
],
);
assert_eq!(all_hunks, all_expanded_hunks);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![
DisplayRow(12)..=DisplayRow(12),
DisplayRow(16)..=DisplayRow(17),
DisplayRow(23)..=DisplayRow(23)
]
);
});
cx.update_editor(|editor, cx| editor.fold_selected_ranges(&FoldSelectedRanges, cx));
@ -11653,11 +11654,6 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(0)..=DisplayRow(0), DisplayRow(5)..=DisplayRow(5)],
"Only one hunk is left not folded, its highlight should be visible"
);
assert_eq!(
all_hunks,
vec![
@ -11672,7 +11668,7 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
DisplayRow(0)..DisplayRow(0)
),
(
"fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
" println!(\"hello\");\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(0)..DisplayRow(0)
),
@ -11684,12 +11680,12 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(5)..DisplayRow(6)
DisplayRow(6)..DisplayRow(7)
),
(
"fn another2() {\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(9)..DisplayRow(9)
DisplayRow(11)..DisplayRow(11)
),
],
"Hunk list should still return shifted folded hunks"
@ -11700,16 +11696,21 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(5)..DisplayRow(6)
DisplayRow(6)..DisplayRow(7)
),
(
"fn another2() {\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(9)..DisplayRow(9)
DisplayRow(11)..DisplayRow(11)
),
],
"Only non-folded hunks should be left expanded"
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(0)..=DisplayRow(0), DisplayRow(6)..=DisplayRow(6)],
"Only one hunk is left not folded, its highlight should be visible"
);
});
cx.update_editor(|editor, cx| {
@ -11746,51 +11747,51 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
let snapshot = editor.snapshot(cx);
let all_hunks = editor_hunks(editor, &snapshot, cx);
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![
DisplayRow(9)..=DisplayRow(10),
DisplayRow(13)..=DisplayRow(14),
DisplayRow(19)..=DisplayRow(19)
],
"After unfolding, all hunk diffs should be visible again"
);
assert_eq!(
all_hunks,
vec![
(
"use some::mod1;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(1)..DisplayRow(1)
DisplayRow(2)..DisplayRow(2)
),
(
"const B: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(5)..DisplayRow(5)
DisplayRow(7)..DisplayRow(7)
),
(
"fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
" println!(\"hello\");\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(9)..DisplayRow(11)
DisplayRow(12)..DisplayRow(13)
),
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(13)..DisplayRow(15)
DisplayRow(16)..DisplayRow(18)
),
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(19)..DisplayRow(20)
DisplayRow(23)..DisplayRow(24)
),
(
"fn another2() {\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(23)..DisplayRow(23)
DisplayRow(28)..DisplayRow(28)
),
],
);
assert_eq!(all_hunks, all_expanded_hunks);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![
DisplayRow(12)..=DisplayRow(12),
DisplayRow(16)..=DisplayRow(17),
DisplayRow(23)..=DisplayRow(23)
],
"After unfolding, all hunk diffs should be visible again"
);
});
}
@ -11940,17 +11941,17 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
(
"bbbb\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(5)..DisplayRow(5),
DisplayRow(6)..DisplayRow(6),
),
(
"nnnn\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(23)..DisplayRow(24),
DisplayRow(25)..DisplayRow(26),
),
(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(43)..DisplayRow(44),
DisplayRow(46)..DisplayRow(47),
),
];
@ -11975,8 +11976,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![
DisplayRow(23)..=DisplayRow(23),
DisplayRow(43)..=DisplayRow(43)
DisplayRow(25)..=DisplayRow(25),
DisplayRow(46)..=DisplayRow(46)
],
);
assert_eq!(all_hunks, expected_all_hunks_shifted);
@ -12007,8 +12008,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![
DisplayRow(23)..=DisplayRow(23),
DisplayRow(43)..=DisplayRow(43)
DisplayRow(25)..=DisplayRow(25),
DisplayRow(46)..=DisplayRow(46)
],
);
assert_eq!(all_hunks, expected_all_hunks_shifted);
@ -12116,12 +12117,12 @@ async fn test_edits_around_toggled_additions(
vec![(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(4)..DisplayRow(7)
DisplayRow(5)..DisplayRow(8)
)]
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(4)..=DisplayRow(6)]
vec![DisplayRow(5)..=DisplayRow(7)]
);
assert_eq!(all_hunks, all_expanded_hunks);
});
@ -12156,12 +12157,12 @@ async fn test_edits_around_toggled_additions(
vec![(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(4)..DisplayRow(8)
DisplayRow(5)..DisplayRow(9)
)]
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(4)..=DisplayRow(6)],
vec![DisplayRow(5)..=DisplayRow(7)],
"Edited hunk should have one more line added"
);
assert_eq!(
@ -12201,12 +12202,12 @@ async fn test_edits_around_toggled_additions(
vec![(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(4)..DisplayRow(9)
DisplayRow(5)..DisplayRow(10)
)]
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(4)..=DisplayRow(6)],
vec![DisplayRow(5)..=DisplayRow(7)],
"Edited hunk should have one more line added"
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12245,12 +12246,12 @@ async fn test_edits_around_toggled_additions(
vec![(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(4)..DisplayRow(8)
DisplayRow(5)..DisplayRow(9)
)]
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(4)..=DisplayRow(6)],
vec![DisplayRow(5)..=DisplayRow(7)],
"Deleting a line should shrint the hunk"
);
assert_eq!(
@ -12293,12 +12294,12 @@ async fn test_edits_around_toggled_additions(
vec![(
"".to_string(),
DiffHunkStatus::Added,
DisplayRow(5)..DisplayRow(6)
DisplayRow(6)..DisplayRow(7)
)]
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(5)..=DisplayRow(5)]
vec![DisplayRow(6)..=DisplayRow(6)]
);
assert_eq!(all_hunks, all_expanded_hunks);
});
@ -12335,7 +12336,7 @@ async fn test_edits_around_toggled_additions(
(
"const A: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(2)..DisplayRow(2)
DisplayRow(3)..DisplayRow(3)
)
]
);
@ -12349,7 +12350,7 @@ async fn test_edits_around_toggled_additions(
vec![(
"const A: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(2)..DisplayRow(2)
DisplayRow(3)..DisplayRow(3)
)],
"Should open hunks that were adjacent to the stale addition one"
);
@ -12445,7 +12446,7 @@ async fn test_edits_around_toggled_deletions(
vec![(
"const A: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(4)..DisplayRow(4)
DisplayRow(5)..DisplayRow(5)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12485,7 +12486,7 @@ async fn test_edits_around_toggled_deletions(
vec![(
"const A: u32 = 42;\nconst B: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(5)..DisplayRow(5)
DisplayRow(6)..DisplayRow(6)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12520,7 +12521,7 @@ async fn test_edits_around_toggled_deletions(
vec![(
"const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
DiffHunkStatus::Removed,
DisplayRow(6)..DisplayRow(6)
DisplayRow(7)..DisplayRow(7)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12554,12 +12555,12 @@ async fn test_edits_around_toggled_deletions(
vec![(
"const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(7)..DisplayRow(8)
DisplayRow(8)..DisplayRow(9)
)]
);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(7)..=DisplayRow(7)],
vec![DisplayRow(8)..=DisplayRow(8)],
"Modified expanded hunks should display additions and highlight their background"
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12653,14 +12654,14 @@ async fn test_edits_around_toggled_modifications(
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(6)..=DisplayRow(6)],
vec![DisplayRow(7)..=DisplayRow(7)],
);
assert_eq!(
all_hunks,
vec![(
"const C: u32 = 42;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(7)
DisplayRow(7)..DisplayRow(8)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12696,7 +12697,7 @@ async fn test_edits_around_toggled_modifications(
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(6)..=DisplayRow(6)],
vec![DisplayRow(7)..=DisplayRow(7)],
"Modified hunk should grow highlighted lines on more text additions"
);
assert_eq!(
@ -12704,7 +12705,7 @@ async fn test_edits_around_toggled_modifications(
vec![(
"const C: u32 = 42;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(9)
DisplayRow(7)..DisplayRow(10)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12742,14 +12743,14 @@ async fn test_edits_around_toggled_modifications(
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(6)..=DisplayRow(8)],
vec![DisplayRow(7)..=DisplayRow(9)],
);
assert_eq!(
all_hunks,
vec![(
"const B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(9)
DisplayRow(7)..DisplayRow(10)
)],
"Modified hunk should grow deleted lines on text deletions above"
);
@ -12786,7 +12787,7 @@ async fn test_edits_around_toggled_modifications(
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(6)..=DisplayRow(9)],
vec![DisplayRow(7)..=DisplayRow(10)],
"Modified hunk should grow deleted lines on text modifications above"
);
assert_eq!(
@ -12794,7 +12795,7 @@ async fn test_edits_around_toggled_modifications(
vec![(
"const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(10)
DisplayRow(7)..DisplayRow(11)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12830,7 +12831,7 @@ async fn test_edits_around_toggled_modifications(
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(6)..=DisplayRow(8)],
vec![DisplayRow(7)..=DisplayRow(9)],
"Modified hunk should grow shrink lines on modification lines removal"
);
assert_eq!(
@ -12838,7 +12839,7 @@ async fn test_edits_around_toggled_modifications(
vec![(
"const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(9)
DisplayRow(7)..DisplayRow(10)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12880,7 +12881,7 @@ async fn test_edits_around_toggled_modifications(
"const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\nconst D: u32 = 42;\n"
.to_string(),
DiffHunkStatus::Removed,
DisplayRow(7)..DisplayRow(7)
DisplayRow(8)..DisplayRow(8)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);
@ -12974,14 +12975,14 @@ async fn test_multiple_expanded_hunks_merge(
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
assert_eq!(
expanded_hunks_background_highlights(editor, cx),
vec![DisplayRow(6)..=DisplayRow(6)],
vec![DisplayRow(7)..=DisplayRow(7)],
);
assert_eq!(
all_hunks,
vec![(
"const C: u32 = 42;\n".to_string(),
DiffHunkStatus::Modified,
DisplayRow(6)..DisplayRow(7)
DisplayRow(7)..DisplayRow(8)
)]
);
assert_eq!(all_hunks, all_expanded_hunks);

View file

@ -11,7 +11,7 @@ use crate::{
hover_popover::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
hunk_diff::{diff_hunk_to_display, DisplayDiffHunk, ExpandedHunk},
hunk_diff::{diff_hunk_to_display, DisplayDiffHunk},
hunk_status,
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
@ -20,8 +20,8 @@ use crate::{
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown,
PageUp, Point, RangeToAnchorExt, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap};
@ -302,7 +302,7 @@ impl EditorElement {
}
register_action(view, cx, Editor::go_to_diagnostic);
register_action(view, cx, Editor::go_to_prev_diagnostic);
register_action(view, cx, Editor::go_to_hunk);
register_action(view, cx, Editor::go_to_next_hunk);
register_action(view, cx, Editor::go_to_prev_hunk);
register_action(view, cx, |editor, a, cx| {
editor.go_to_definition(a, cx).detach_and_log_err(cx);
@ -489,28 +489,7 @@ impl EditorElement {
let mut modifiers = event.modifiers;
if let Some(hovered_hunk) = hovered_hunk {
if modifiers.control || modifiers.platform {
editor.toggle_hovered_hunk(&hovered_hunk, cx);
} else {
let display_range = hovered_hunk
.multi_buffer_range
.clone()
.to_display_points(&position_map.snapshot);
let hunk_bounds = Self::diff_hunk_bounds(
&position_map.snapshot,
position_map.line_height,
gutter_hitbox.bounds,
&DisplayDiffHunk::Unfolded {
diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
display_row_range: display_range.start.row()..display_range.end.row(),
multi_buffer_range: hovered_hunk.multi_buffer_range.clone(),
status: hovered_hunk.status,
},
);
if hunk_bounds.contains(&event.position) {
editor.open_hunk_context_menu(hovered_hunk, event.position, cx);
}
}
editor.toggle_hovered_hunk(&hovered_hunk, cx);
cx.notify();
return;
} else if gutter_hitbox.is_hovered(cx) {
@ -1303,13 +1282,13 @@ impl EditorElement {
let display_hunks = buffer_snapshot
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.filter_map(|hunk| {
let mut display_hunk = diff_hunk_to_display(&hunk, snapshot);
let display_hunk = diff_hunk_to_display(&hunk, snapshot);
if let DisplayDiffHunk::Unfolded {
multi_buffer_range,
status,
..
} = &mut display_hunk
} = &display_hunk
{
let mut is_expanded = false;
while let Some(expanded_hunk) = expanded_hunks.peek() {
@ -1332,11 +1311,7 @@ impl EditorElement {
}
match status {
DiffHunkStatus::Added => {}
DiffHunkStatus::Modified => {
if is_expanded {
*status = DiffHunkStatus::Added;
}
}
DiffHunkStatus::Modified => {}
DiffHunkStatus::Removed => {
if is_expanded {
return None;
@ -3371,9 +3346,6 @@ impl EditorElement {
for test_indicator in layout.test_indicators.iter_mut() {
test_indicator.paint(cx);
}
for close_indicator in layout.close_indicators.iter_mut() {
close_indicator.paint(cx);
}
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
indicator.paint(cx);
@ -4159,46 +4131,6 @@ impl EditorElement {
+ 1;
self.column_pixels(digit_count, cx)
}
#[allow(clippy::too_many_arguments)]
fn layout_hunk_diff_close_indicators(
&self,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
expanded_hunks_by_rows: HashMap<DisplayRow, ExpandedHunk>,
cx: &mut WindowContext,
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
expanded_hunks_by_rows
.into_iter()
.map(|(display_row, hunk)| {
let button = editor.close_hunk_diff_button(
HoveredHunk {
multi_buffer_range: hunk.hunk_range,
status: hunk.status,
diff_base_byte_range: hunk.diff_base_byte_range,
},
display_row,
cx,
);
prepaint_gutter_button(
button,
display_row,
line_height,
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
rows_with_hunk_bounds,
cx,
)
})
.collect()
})
}
}
#[allow(clippy::too_many_arguments)]
@ -5549,15 +5481,6 @@ impl Element for EditorElement {
} else {
Vec::new()
};
let close_indicators = self.layout_hunk_diff_close_indicators(
line_height,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&rows_with_hunk_bounds,
expanded_add_hunks_by_rows,
cx,
);
self.layout_signature_help(
&hitbox,
@ -5670,7 +5593,6 @@ impl Element for EditorElement {
selections,
mouse_context_menu,
test_indicators,
close_indicators,
code_actions_indicator,
gutter_fold_toggles,
crease_trailers,
@ -5812,7 +5734,6 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
close_indicators: Vec<AnyElement>,
gutter_fold_toggles: Vec<Option<AnyElement>>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
mouse_context_menu: Option<AnyElement>,

View file

@ -1,28 +1,26 @@
use collections::{hash_map, HashMap, HashSet};
use git::diff::DiffHunkStatus;
use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View};
use language::{Buffer, BufferId, Point};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
MultiBufferSnapshot, ToPoint,
};
use settings::SettingsStore;
use std::{
ops::{Range, RangeInclusive},
sync::Arc,
};
use ui::{
prelude::*, ActiveTheme, ContextMenu, InteractiveElement, IntoElement, ParentElement, Pixels,
Styled, ViewContext, VisualContext,
prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext,
};
use util::{debug_panic, RangeExt};
use util::RangeExt;
use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections,
mouse_context_menu::MouseContextMenu, BlockDisposition, BlockProperties, BlockStyle,
CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement,
EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertFile, RevertSelectedHunks,
ToDisplayPoint, ToggleHunkDiff,
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, BlockDisposition,
BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot,
Editor, EditorElement, EditorSnapshot, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
RangeToAnchorExt, RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
#[derive(Debug, Clone)]
@ -41,7 +39,7 @@ pub(super) struct ExpandedHunks {
#[derive(Debug, Clone)]
pub(super) struct ExpandedHunk {
pub block: Option<CustomBlockId>,
pub blocks: Vec<CustomBlockId>,
pub hunk_range: Range<Anchor>,
pub diff_base_byte_range: Range<usize>,
pub status: DiffHunkStatus,
@ -77,85 +75,6 @@ impl ExpandedHunks {
}
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));
}
}
})
.action("Revert File", RevertFile.boxed_clone())
}),
cx,
)
}
pub(super) fn toggle_hovered_hunk(
&mut self,
hovered_hunk: &HoveredHunk,
@ -264,7 +183,8 @@ impl Editor {
break;
} else if expanded_hunk_row_range == hunk_to_toggle_row_range {
highlights_to_remove.push(expanded_hunk.hunk_range.clone());
blocks_to_remove.extend(expanded_hunk.block);
blocks_to_remove
.extend(expanded_hunk.blocks.iter().copied());
hunks_to_toggle.next();
retain = false;
break;
@ -371,9 +291,17 @@ impl Editor {
Err(ix) => ix,
};
let block = match hunk.status {
let blocks;
match hunk.status {
DiffHunkStatus::Removed => {
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, hunk, cx)
blocks = self.insert_blocks(
[
self.hunk_header_block(&hunk, cx),
Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
],
None,
cx,
);
}
DiffHunkStatus::Added => {
self.highlight_rows::<DiffRowHighlight>(
@ -382,7 +310,7 @@ impl Editor {
false,
cx,
);
None
blocks = self.insert_blocks([self.hunk_header_block(&hunk, cx)], None, cx);
}
DiffHunkStatus::Modified => {
self.highlight_rows::<DiffRowHighlight>(
@ -391,13 +319,20 @@ impl Editor {
false,
cx,
);
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, hunk, cx)
blocks = self.insert_blocks(
[
self.hunk_header_block(&hunk, cx),
Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
],
None,
cx,
);
}
};
self.expanded_hunks.hunks.insert(
block_insert_index,
ExpandedHunk {
block,
blocks,
hunk_range: hunk_start..hunk_end,
status: hunk.status,
folded: false,
@ -408,109 +343,368 @@ impl Editor {
Some(())
}
fn insert_deleted_text_block(
&mut self,
fn hunk_header_block(
&self,
hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Editor>,
) -> BlockProperties<Anchor> {
let border_color = cx.theme().colors().border_disabled;
let gutter_color = match hunk.status {
DiffHunkStatus::Added => cx.theme().status().created,
DiffHunkStatus::Modified => cx.theme().status().modified,
DiffHunkStatus::Removed => cx.theme().status().deleted,
};
BlockProperties {
position: hunk.multi_buffer_range.start,
height: 1,
style: BlockStyle::Sticky,
disposition: BlockDisposition::Above,
priority: 0,
render: Box::new({
let editor = cx.view().clone();
let hunk = hunk.clone();
move |cx| {
let hunk_controls_menu_handle =
editor.read(cx).hunk_controls_menu_handle.clone();
h_flex()
.id(cx.block_id)
.w_full()
.h(cx.line_height())
.child(
div()
.id("gutter-strip")
.w(EditorElement::diff_hunk_strip_width(cx.line_height()))
.h_full()
.bg(gutter_color)
.cursor(CursorStyle::PointingHand)
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
});
}
}),
)
.child(
h_flex()
.size_full()
.justify_between()
.border_t_1()
.border_color(border_color)
.child(
h_flex()
.gap_2()
.pl_6()
.child(
IconButton::new("next-hunk", IconName::ArrowDown)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Next Hunk",
&GoToHunk,
&focus_handle,
cx,
)
}
})
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let position = hunk
.multi_buffer_range
.end
.to_point(
&snapshot.buffer_snapshot,
);
if let Some(hunk) = editor
.go_to_hunk_after_position(
&snapshot, position, cx,
)
{
let multi_buffer_start = snapshot
.buffer_snapshot
.anchor_before(Point::new(
hunk.row_range.start.0,
0,
));
let multi_buffer_end = snapshot
.buffer_snapshot
.anchor_after(Point::new(
hunk.row_range.end.0,
0,
));
editor.expand_diff_hunk(
None,
&HoveredHunk {
multi_buffer_range:
multi_buffer_start
..multi_buffer_end,
status: hunk_status(&hunk),
diff_base_byte_range: hunk
.diff_base_byte_range,
},
cx,
);
}
});
}
}),
)
.child(
IconButton::new("prev-hunk", IconName::ArrowUp)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Previous Hunk",
&GoToPrevHunk,
&focus_handle,
cx,
)
}
})
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let position = hunk
.multi_buffer_range
.start
.to_point(
&snapshot.buffer_snapshot,
);
let hunk = editor
.go_to_hunk_before_position(
&snapshot, position, cx,
);
if let Some(hunk) = hunk {
let multi_buffer_start = snapshot
.buffer_snapshot
.anchor_before(Point::new(
hunk.row_range.start.0,
0,
));
let multi_buffer_end = snapshot
.buffer_snapshot
.anchor_after(Point::new(
hunk.row_range.end.0,
0,
));
editor.expand_diff_hunk(
None,
&HoveredHunk {
multi_buffer_range:
multi_buffer_start
..multi_buffer_end,
status: hunk_status(&hunk),
diff_base_byte_range: hunk
.diff_base_byte_range,
},
cx,
);
}
});
}
}),
),
)
.child(
h_flex()
.gap_2()
.pr_6()
.child({
let focus = editor.focus_handle(cx);
PopoverMenu::new("hunk-controls-dropdown")
.trigger(
IconButton::new(
"toggle_editor_selections_icon",
IconName::EllipsisVertical,
)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(
hunk_controls_menu_handle.is_deployed(),
)
.when(
!hunk_controls_menu_handle.is_deployed(),
|this| {
this.tooltip(|cx| {
Tooltip::text("Hunk Controls", cx)
})
},
),
)
.anchor(AnchorCorner::TopRight)
.with_handle(hunk_controls_menu_handle)
.menu(move |cx| {
let focus = focus.clone();
let menu =
ContextMenu::build(cx, move |menu, _| {
menu.context(focus.clone()).action(
"Discard All",
RevertFile.boxed_clone(),
)
});
Some(menu)
})
})
.child(
IconButton::new("discard", IconName::RotateCcw)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Discard Hunk",
&RevertSelectedHunks,
&focus_handle,
cx,
)
}
})
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, 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)
});
}
}
}),
)
.child(
IconButton::new("collapse", IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Collapse Hunk",
&ToggleHunkDiff,
&focus_handle,
cx,
)
}
})
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
});
}
}),
),
),
)
.into_any_element()
}
}),
}
}
fn deleted_text_block(
hunk: &HoveredHunk,
diff_base_buffer: Model<Buffer>,
deleted_text_height: u32,
hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Self>,
) -> Option<CustomBlockId> {
cx: &mut ViewContext<'_, Editor>,
) -> BlockProperties<Anchor> {
let gutter_color = match hunk.status {
DiffHunkStatus::Added => unreachable!(),
DiffHunkStatus::Modified => cx.theme().status().modified,
DiffHunkStatus::Removed => cx.theme().status().deleted,
};
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 hunk = hunk.clone();
let height = editor_height.max(deleted_text_height);
let mut new_block_ids = self.insert_blocks(
Some(BlockProperties {
position: hunk.multi_buffer_range.start,
height,
style: BlockStyle::Flex,
disposition: BlockDisposition::Above,
render: Box::new(move |cx| {
let width = EditorElement::diff_hunk_strip_width(cx.line_height());
let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
BlockProperties {
position: hunk.multi_buffer_range.start,
height,
style: BlockStyle::Flex,
disposition: BlockDisposition::Above,
priority: 0,
render: Box::new(move |cx| {
let width = EditorElement::diff_hunk_strip_width(cx.line_height());
let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
let close_button = editor.update(cx.context, |editor, cx| {
let editor_snapshot = editor.snapshot(cx);
let hunk_display_range = hunk
.multi_buffer_range
.clone()
.to_display_points(&editor_snapshot);
editor.close_hunk_diff_button(
hunk.clone(),
hunk_display_range.start.row(),
cx,
)
});
h_flex()
.id("gutter with editor")
.bg(deleted_hunk_color)
.h(height as f32 * cx.line_height())
.w_full()
.child(
h_flex()
.id("gutter")
.max_w(gutter_dimensions.full_width())
.min_w(gutter_dimensions.full_width())
.size_full()
.child(
h_flex()
.id("gutter hunk")
.bg(cx.theme().status().deleted)
.pl(gutter_dimensions.margin
+ gutter_dimensions
.git_blame_entries_width
.unwrap_or_default())
.max_w(width)
.min_w(width)
.size_full()
.cursor(CursorStyle::PointingHand)
.on_mouse_down(MouseButton::Left, {
let editor = editor.clone();
let hunk = hunk.clone();
move |event, cx| {
let modifiers = event.modifiers;
if modifiers.control || modifiers.platform {
editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.open_hunk_context_menu(
hunk.clone(),
event.position,
cx,
);
});
}
}
}),
)
.child(
v_flex()
.size_full()
.pt(rems(0.25))
.justify_start()
.child(close_button),
),
)
.child(editor_with_deleted_text.clone())
.into_any_element()
}),
priority: 0,
h_flex()
.id(cx.block_id)
.bg(deleted_hunk_color)
.h(height as f32 * cx.line_height())
.w_full()
.child(
h_flex()
.id("gutter")
.max_w(gutter_dimensions.full_width())
.min_w(gutter_dimensions.full_width())
.size_full()
.child(
h_flex()
.id("gutter hunk")
.bg(gutter_color)
.pl(gutter_dimensions.margin
+ gutter_dimensions
.git_blame_entries_width
.unwrap_or_default())
.max_w(width)
.min_w(width)
.size_full()
.cursor(CursorStyle::PointingHand)
.on_mouse_down(MouseButton::Left, {
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
});
}
}),
),
)
.child(editor_with_deleted_text.clone())
.into_any_element()
}),
None,
cx,
);
if new_block_ids.len() == 1 {
new_block_ids.pop()
} else {
debug_panic!(
"Inserted one editor block but did not receive exactly one block id: {new_block_ids:?}"
);
None
}
}
@ -521,7 +715,7 @@ impl Editor {
.expanded_hunks
.hunks
.drain(..)
.filter_map(|expanded_hunk| expanded_hunk.block)
.flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
.collect::<HashSet<_>>();
if to_remove.is_empty() {
false
@ -603,7 +797,7 @@ impl Editor {
expanded_hunk.folded = true;
highlights_to_remove
.push(expanded_hunk.hunk_range.clone());
if let Some(block) = expanded_hunk.block.take() {
for block in expanded_hunk.blocks.drain(..) {
blocks_to_remove.insert(block);
}
break;
@ -650,7 +844,7 @@ impl Editor {
}
}
if !retain {
blocks_to_remove.extend(expanded_hunk.block);
blocks_to_remove.extend(expanded_hunk.blocks.drain(..));
highlights_to_remove.push(expanded_hunk.hunk_range.clone());
}
retain
@ -749,7 +943,7 @@ fn added_hunk_color(cx: &AppContext) -> Hsla {
}
fn deleted_hunk_color(cx: &AppContext) -> Hsla {
let mut deleted_color = cx.theme().status().git().deleted;
let mut deleted_color = cx.theme().status().deleted;
deleted_color.fade_out(0.7);
deleted_color
}
@ -788,32 +982,15 @@ fn editor_with_deleted_text(
false,
cx,
);
let subscription_editor = parent_editor.clone();
editor._subscriptions.extend([
cx.on_blur(&editor.focus_handle, |editor, cx| {
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor
._subscriptions
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.try_cancel();
});
cx.notify();
}),
cx.on_focus(&editor.focus_handle, move |editor, cx| {
let restored_highlight = if let Some(parent_editor) = subscription_editor.upgrade()
{
parent_editor.read(cx).current_line_highlight
} else {
None
};
editor.set_current_line_highlight(restored_highlight);
cx.notify();
}),
cx.observe_global::<SettingsStore>(|editor, cx| {
if !editor.is_focused(cx) {
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
}
}),
]);
})]);
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();