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;