Add editor::RevertSelectedHunks to revert git diff hunks in the editor (#9068)

https://github.com/zed-industries/zed/assets/2690773/653b5658-e3f3-4aee-9a9d-0f2153b4141b

Release Notes:

- Added `editor::RevertSelectedHunks` (`cmd-alt-z` by default) for
reverting git hunks from the editor
This commit is contained in:
Kirill Bulatov 2024-03-09 01:37:24 +02:00 committed by GitHub
parent 6a7a3b257a
commit 347178039c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1003 additions and 72 deletions

View File

@ -118,7 +118,8 @@
"stop_at_soft_wraps": true
}
],
"ctrl-;": "editor::ToggleLineNumbers"
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-alt-z": "editor::RevertSelectedHunks"
}
},
{

View File

@ -153,7 +153,8 @@
}
],
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers"
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "editor::RevertSelectedHunks"
}
},
{

View File

@ -5,7 +5,8 @@ use crate::{
use call::ActiveCall;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo,
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks,
ToggleCodeActions, Undo,
},
test::editor_test_context::{AssertionContextManager, EditorTestContext},
Editor,
@ -1814,6 +1815,171 @@ async fn test_inlay_hint_refresh_is_forwarded(
});
}
#[gpui::test]
async fn test_multiple_types_reverts(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_remote_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.to_string()), 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.to_string()), cx);
});
});
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
// client, selects a range in the updated buffer, and reverts it
// both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
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.revert_selected_hunks(&RevertSelectedHunks, cx);
});
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
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;"#});
}
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new();
for hint in editor.inlay_hint_cache().hints() {

View File

@ -210,6 +210,7 @@ gpui::actions!(
PageDown,
PageUp,
Paste,
RevertSelectedHunks,
Redo,
RedoSelection,
Rename,

View File

@ -36,7 +36,7 @@ mod selections_collection;
mod editor_tests;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use ::git::diff::DiffHunk;
use ::git::diff::{DiffHunk, DiffHunkStatus};
pub(crate) use actions::*;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
@ -4908,6 +4908,105 @@ 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);
if !revert_changes.is_empty() {
self.transact(cx, |editor, cx| {
editor.buffer().update(cx, |multi_buffer, cx| {
for (buffer_id, buffer_revert_ranges) in revert_changes {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(buffer_revert_ranges, None, cx);
});
}
}
});
editor.change_selections(None, cx, |selections| selections.refresh());
});
}
}
fn gather_revert_changes(
&mut self,
selections: &[Selection<Anchor>],
cx: &mut ViewContext<'_, Editor>,
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>> {
let mut revert_changes = HashMap::default();
self.buffer.update(cx, |multi_buffer, cx| {
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let selected_multi_buffer_rows = selections.iter().map(|selection| {
let head = selection.head();
let tail = selection.tail();
let start = tail.to_point(&multi_buffer_snapshot).row;
let end = head.to_point(&multi_buffer_snapshot).row;
if start > end {
end..start
} else {
start..end
}
});
let mut processed_buffer_rows =
HashMap::<BufferId, HashSet<Range<text::Anchor>>>::default();
for selected_multi_buffer_rows in selected_multi_buffer_rows {
let query_rows =
selected_multi_buffer_rows.start..selected_multi_buffer_rows.end + 1;
for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) {
// 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() == DiffHunkStatus::Removed;
let related_to_selection = if allow_adjacent {
hunk.associated_range.overlaps(&query_rows)
|| hunk.associated_range.start == query_rows.end
|| hunk.associated_range.end == query_rows.start
} else {
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
// `hunk.associated_range` is exclusive (e.g. [2..3] means 2nd row is selected)
hunk.associated_range.overlaps(&selected_multi_buffer_rows)
|| selected_multi_buffer_rows.end == hunk.associated_range.start
};
if related_to_selection {
if !processed_buffer_rows
.entry(hunk.buffer_id)
.or_default()
.insert(hunk.buffer_range.start..hunk.buffer_range.end)
{
continue;
}
Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx);
}
}
}
});
revert_changes
}
fn prepare_revert_change(
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>>,
multi_buffer: &MultiBuffer,
hunk: &DiffHunk<u32>,
cx: &mut AppContext,
) -> Option<()> {
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
let buffer = buffer.read(cx);
let original_text = buffer.diff_base()?.get(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| {
probe
.0
.start
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
.then(probe.1.as_ref().cmp(original_text))
}) {
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), Arc::from(original_text)));
Some(())
} else {
None
}
}
pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
self.manipulate_lines(cx, |lines| lines.reverse())
}

View File

@ -8743,6 +8743,560 @@ async fn test_find_all_references(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let base_text = indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;
struct Row9;
struct Row10;"#};
// When addition hunks are not adjacent to carets, no hunk revert is performed
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row1;
struct Row1.1;
struct Row1.2;
struct Row2;ˇ
struct Row4;
struct Row5;
struct Row6;
struct Row8;
ˇstruct Row9;
struct Row9.1;
struct Row9.2;
struct Row9.3;
struct Row10;"#},
vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
indoc! {r#"struct Row;
struct Row1;
struct Row1.1;
struct Row1.2;
struct Row2;ˇ
struct Row4;
struct Row5;
struct Row6;
struct Row8;
ˇstruct Row9;
struct Row9.1;
struct Row9.2;
struct Row9.3;
struct Row10;"#},
base_text,
&mut cx,
);
// Same for selections
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row2.1;
struct Row2.2;
«ˇ
struct Row4;
struct» Row5;
«struct Row6;
ˇ»
struct Row9.1;
struct Row9.2;
struct Row9.3;
struct Row8;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row2.1;
struct Row2.2;
«ˇ
struct Row4;
struct» Row5;
«struct Row6;
ˇ»
struct Row9.1;
struct Row9.2;
struct Row9.3;
struct Row8;
struct Row9;
struct Row10;"#},
base_text,
&mut cx,
);
// When carets and selections intersect the addition hunks, those are reverted.
// Adjacent carets got merged.
assert_hunk_revert(
indoc! {r#"struct Row;
ˇ// something on the top
struct Row1;
struct Row2;
struct Roˇw3.1;
struct Row2.2;
struct Row2.3;ˇ
struct Row4;
struct ˇRow5.1;
struct Row5.2;
struct «Rowˇ»5.3;
struct Row5;
struct Row6;
ˇ
struct Row9.1;
struct «Rowˇ»9.2;
struct «ˇRow»9.3;
struct Row8;
struct Row9;
«ˇ// something on bottom»
struct Row10;"#},
vec![
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::Added,
],
indoc! {r#"struct Row;
ˇstruct Row1;
struct Row2;
ˇ
struct Row4;
ˇstruct Row5;
struct Row6;
ˇ
ˇstruct Row8;
struct Row9;
ˇstruct Row10;"#},
base_text,
&mut cx,
);
}
#[gpui::test]
async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let base_text = indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;
struct Row9;
struct Row10;"#};
// Modification hunks behave the same as the addition ones.
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row1;
struct Row33;
ˇ
struct Row4;
struct Row5;
struct Row6;
ˇ
struct Row99;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
indoc! {r#"struct Row;
struct Row1;
struct Row33;
ˇ
struct Row4;
struct Row5;
struct Row6;
ˇ
struct Row99;
struct Row9;
struct Row10;"#},
base_text,
&mut cx,
);
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row1;
struct Row33;
«ˇ
struct Row4;
struct» Row5;
«struct Row6;
ˇ»
struct Row99;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
indoc! {r#"struct Row;
struct Row1;
struct Row33;
«ˇ
struct Row4;
struct» Row5;
«struct Row6;
ˇ»
struct Row99;
struct Row9;
struct Row10;"#},
base_text,
&mut cx,
);
assert_hunk_revert(
indoc! {r#"ˇstruct Row1.1;
struct Row1;
«ˇstr»uct Row22;
struct ˇRow44;
struct Row5;
struct «»ow66;ˇ
«struˇ»ct Row88;
struct Row9;
struct Row1011;ˇ"#},
vec![
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
],
indoc! {r#"struct Row;
ˇstruct Row1;
struct Row2;
ˇ
struct Row4;
ˇstruct Row5;
struct Row6;
ˇ
struct Row8;
ˇstruct Row9;
struct Row10;ˇ"#},
base_text,
&mut cx,
);
}
#[gpui::test]
async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let base_text = indoc! {r#"struct Row;
struct Row1;
struct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;
struct Row9;
struct Row10;"#};
// Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row2;
ˇstruct Row4;
struct Row5;
struct Row6;
ˇ
struct Row8;
struct Row10;"#},
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
indoc! {r#"struct Row;
struct Row2;
ˇstruct Row4;
struct Row5;
struct Row6;
ˇ
struct Row8;
struct Row10;"#},
base_text,
&mut cx,
);
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row2;
«ˇstruct Row4;
struct» Row5;
«struct Row6;
ˇ»
struct Row8;
struct Row10;"#},
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
indoc! {r#"struct Row;
struct Row2;
«ˇstruct Row4;
struct» Row5;
«struct Row6;
ˇ»
struct Row8;
struct Row10;"#},
base_text,
&mut cx,
);
// Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
assert_hunk_revert(
indoc! {r#"struct Row;
ˇstruct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;ˇ
struct Row10;"#},
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
indoc! {r#"struct Row;
struct Row1;
ˇstruct Row2;
struct Row4;
struct Row5;
struct Row6;
struct Row8;ˇ
struct Row9;
struct Row10;"#},
base_text,
&mut cx,
);
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row2«ˇ;
struct Row4;
struct» Row5;
«struct Row6;
struct Row8;ˇ»
struct Row10;"#},
vec![
DiffHunkStatus::Removed,
DiffHunkStatus::Removed,
DiffHunkStatus::Removed,
],
indoc! {r#"struct Row;
struct Row1;
struct Row2«ˇ;
struct Row4;
struct» Row5;
«struct Row6;
struct Row8;ˇ»
struct Row9;
struct Row10;"#},
base_text,
&mut cx,
);
}
#[gpui::test]
async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let cols = 4;
let rows = 10;
let sample_text_1 = sample_text(rows, cols, 'a');
assert_eq!(
sample_text_1,
"aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
);
let sample_text_2 = sample_text(rows, cols, 'l');
assert_eq!(
sample_text_2,
"llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
);
let sample_text_3 = sample_text(rows, cols, 'v');
assert_eq!(
sample_text_3,
"vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
);
fn diff_every_buffer_row(
buffer: &Model<Buffer>,
sample_text: String,
cols: usize,
cx: &mut gpui::TestAppContext,
) {
// revert first character in each row, creating one large diff hunk per buffer
let is_first_char = |offset: usize| offset % cols == 0;
buffer.update(cx, |buffer, cx| {
buffer.set_text(
sample_text
.chars()
.enumerate()
.map(|(offset, c)| if is_first_char(offset) { 'X' } else { c })
.collect::<String>(),
cx,
);
buffer.set_diff_base(Some(sample_text), cx);
});
cx.executor().run_until_parked();
}
let buffer_1 = cx.new_model(|cx| {
Buffer::new(
0,
BufferId::new(cx.entity_id().as_u64()).unwrap(),
sample_text_1.clone(),
)
});
diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
let buffer_2 = cx.new_model(|cx| {
Buffer::new(
1,
BufferId::new(cx.entity_id().as_u64() + 1).unwrap(),
sample_text_2.clone(),
)
});
diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
let buffer_3 = cx.new_model(|cx| {
Buffer::new(
2,
BufferId::new(cx.entity_id().as_u64() + 2).unwrap(),
sample_text_3.clone(),
)
});
diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
let multibuffer = cx.new_model(|cx| {
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
multibuffer.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multibuffer.push_excerpts(
buffer_3.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multibuffer
});
let (editor, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
editor.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), "XaaaXbbbX\nccXc\ndXdd\n\nhXhh\nXiiiXjjjX\n\nXlllXmmmX\nnnXn\noXoo\n\nsXss\nXtttXuuuX\n\nXvvvXwwwX\nxxXx\nyXyy\n\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n");
editor.select_all(&SelectAll, cx);
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
});
cx.executor().run_until_parked();
// When all ranges are selected, all buffer hunks are reverted.
editor.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
});
buffer_1.update(cx, |buffer, _| {
assert_eq!(buffer.text(), sample_text_1);
});
buffer_2.update(cx, |buffer, _| {
assert_eq!(buffer.text(), sample_text_2);
});
buffer_3.update(cx, |buffer, _| {
assert_eq!(buffer.text(), sample_text_3);
});
diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
});
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
});
// Now, when all ranges selected belong to buffer_1, the revert should succeed,
// but not affect buffer_2 and its related excerpts.
editor.update(cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX\n\n\nXvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n\n"
);
});
buffer_1.update(cx, |buffer, _| {
assert_eq!(buffer.text(), sample_text_1);
});
buffer_2.update(cx, |buffer, _| {
assert_eq!(
buffer.text(),
"XlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX"
);
});
buffer_3.update(cx, |buffer, _| {
assert_eq!(
buffer.text(),
"XvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X"
);
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@ -8913,3 +9467,45 @@ pub(crate) fn rust_lang() -> Arc<Language> {
Some(tree_sitter_rust::language()),
))
}
#[track_caller]
fn assert_hunk_revert(
not_reverted_text_with_selections: &str,
expected_not_reverted_hunk_statuses: Vec<DiffHunkStatus>,
expected_reverted_text_with_selections: &str,
base_text: &str,
cx: &mut EditorLspTestContext,
) {
cx.set_state(not_reverted_text_with_selections);
cx.update_editor(|editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_diff_base(Some(base_text.to_string()), cx);
});
});
cx.executor().run_until_parked();
let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
let snapshot = editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.snapshot();
let reverted_hunk_statuses = snapshot
.git_diff_hunks_in_row_range(0..u32::MAX)
.map(|hunk| hunk.status())
.collect::<Vec<_>>();
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
reverted_hunk_statuses
});
cx.executor().run_until_parked();
cx.assert_editor_state(expected_reverted_text_with_selections);
assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses);
}

View File

@ -339,6 +339,7 @@ impl EditorElement {
register_action(view, cx, Editor::unique_lines_case_insensitive);
register_action(view, cx, Editor::unique_lines_case_sensitive);
register_action(view, cx, Editor::accept_partial_copilot_suggestion);
register_action(view, cx, Editor::revert_selected_hunks);
}
fn register_key_listeners(
@ -1452,12 +1453,12 @@ impl EditorElement {
.buffer_snapshot
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
{
let start_display = Point::new(hunk.buffer_range.start, 0)
let start_display = Point::new(hunk.associated_range.start, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.buffer_range.end, 0)
let end_display = Point::new(hunk.associated_range.end, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
let mut end_y = if hunk.associated_range.start == hunk.associated_range.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)

View File

@ -46,20 +46,20 @@ impl DisplayDiffHunk {
}
pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
let hunk_start_point = Point::new(hunk.associated_range.start, 0);
let hunk_start_point_sub = Point::new(hunk.associated_range.start.saturating_sub(1), 0);
let hunk_end_point_sub = Point::new(
hunk.buffer_range
hunk.associated_range
.end
.saturating_sub(1)
.max(hunk.buffer_range.start),
.max(hunk.associated_range.start),
0,
);
let is_removal = hunk.status() == DiffHunkStatus::Removed;
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
let folds_start = Point::new(hunk.associated_range.start.saturating_sub(2), 0);
let folds_end = Point::new(hunk.associated_range.end + 2, 0);
let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
@ -79,7 +79,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
} else {
let start = hunk_start_point.to_display_point(snapshot).row();
let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
let hunk_end_row = hunk.associated_range.end.max(hunk.associated_range.start);
let hunk_end_point = Point::new(hunk_end_row, 0);
let end = hunk_end_point.to_display_point(snapshot).row();
@ -264,7 +264,7 @@ mod tests {
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.map(|hunk| (hunk.status(), hunk.associated_range))
.collect::<Vec<_>>(),
&expected,
);
@ -272,7 +272,7 @@ mod tests {
assert_eq!(
snapshot
.git_diff_hunks_in_range_rev(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.map(|hunk| (hunk.status(), hunk.associated_range))
.collect::<Vec<_>>(),
expected
.iter()

View File

@ -274,7 +274,7 @@ impl EditorTestContext {
let buffer_text = self.buffer_text();
if buffer_text != unmarked_text {
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}\nRaw unmarked text\n{unmarked_text}");
}
self.assert_selections(expected_selections, marked_text.to_string())

View File

@ -1,6 +1,6 @@
use std::{iter, ops::Range};
use sum_tree::SumTree;
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point};
pub use git2 as libgit;
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
@ -12,17 +12,53 @@ pub enum DiffHunkStatus {
Removed,
}
/// A diff hunk, representing a range of consequent lines in a singleton buffer, associated with a generic range.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffHunk<T> {
pub buffer_range: Range<T>,
/// E.g. a range in multibuffer, that has an excerpt added, singleton buffer for which has this diff hunk.
/// Consider a singleton buffer with 10 lines, all of them are modified — so a corresponding diff hunk would have a range 0..10.
/// And a multibuffer with the excerpt of lines 2-6 from the singleton buffer.
/// If the multibuffer is searched for diff hunks, the associated range would be multibuffer rows, corresponding to rows 2..6 from the singleton buffer.
/// But the hunk range would be 0..10, same for any other excerpts from the same singleton buffer.
pub associated_range: Range<T>,
/// Singleton buffer ID this hunk belongs to.
pub buffer_id: BufferId,
/// A consequent range of lines in the singleton buffer, that were changed and produced this diff hunk.
pub buffer_range: Range<Anchor>,
/// Original singleton buffer text before the change, that was instead of the `buffer_range`.
pub diff_base_byte_range: Range<usize>,
}
impl<T> DiffHunk<T> {
fn buffer_range_empty(&self) -> bool {
if self.buffer_range.start == self.buffer_range.end {
return true;
}
// buffer diff hunks are per line, so if we arrive to the same line with different bias, it's the same hunk
let Anchor {
timestamp: timestamp_start,
offset: offset_start,
buffer_id: buffer_id_start,
bias: _,
} = self.buffer_range.start;
let Anchor {
timestamp: timestamp_end,
offset: offset_end,
buffer_id: buffer_id_end,
bias: _,
} = self.buffer_range.end;
timestamp_start == timestamp_end
&& offset_start == offset_end
&& buffer_id_start == buffer_id_end
}
}
impl DiffHunk<u32> {
pub fn status(&self) -> DiffHunkStatus {
if self.diff_base_byte_range.is_empty() {
DiffHunkStatus::Added
} else if self.buffer_range.is_empty() {
} else if self.buffer_range_empty() {
DiffHunkStatus::Removed
} else {
DiffHunkStatus::Modified
@ -35,7 +71,7 @@ impl sum_tree::Item for DiffHunk<Anchor> {
fn summary(&self) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
buffer_range: self.associated_range.clone(),
}
}
}
@ -57,7 +93,7 @@ impl sum_tree::Summary for DiffHunkSummary {
}
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct BufferDiff {
last_buffer_version: Option<clock::Global>,
tree: SumTree<DiffHunk<Anchor>>,
@ -103,8 +139,11 @@ impl BufferDiff {
})
.flat_map(move |hunk| {
[
(&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
(&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
(
&hunk.associated_range.start,
hunk.diff_base_byte_range.start,
),
(&hunk.associated_range.end, hunk.diff_base_byte_range.end),
]
.into_iter()
});
@ -112,17 +151,17 @@ impl BufferDiff {
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
iter::from_fn(move || {
let (start_point, start_base) = summaries.next()?;
let (end_point, end_base) = summaries.next()?;
let (mut end_point, end_base) = summaries.next()?;
let end_row = if end_point.column > 0 {
end_point.row + 1
} else {
end_point.row
};
if end_point.column > 0 {
end_point.row += 1;
}
Some(DiffHunk {
buffer_range: start_point.row..end_row,
associated_range: start_point.row..end_point.row,
diff_base_byte_range: start_base..end_base,
buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point),
buffer_id: buffer.remote_id(),
})
})
}
@ -142,7 +181,7 @@ impl BufferDiff {
cursor.prev(buffer);
let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
let range = hunk.associated_range.to_point(buffer);
let end_row = if range.end.column > 0 {
range.end.row + 1
} else {
@ -150,8 +189,10 @@ impl BufferDiff {
};
Some(DiffHunk {
buffer_range: range.start.row..end_row,
associated_range: range.start.row..end_row,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
buffer_id: hunk.buffer_id,
})
})
}
@ -269,8 +310,10 @@ impl BufferDiff {
let end = Point::new(buffer_row_range.end, 0);
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
DiffHunk {
associated_range: buffer_range.clone(),
buffer_range,
diff_base_byte_range,
buffer_id: buffer.remote_id(),
}
}
}
@ -289,12 +332,12 @@ pub fn assert_hunks<Iter>(
let actual_hunks = diff_hunks
.map(|hunk| {
(
hunk.buffer_range.clone(),
hunk.associated_range.clone(),
&diff_base[hunk.diff_base_byte_range],
buffer
.text_for_range(
Point::new(hunk.buffer_range.start, 0)
..Point::new(hunk.buffer_range.end, 0),
Point::new(hunk.associated_range.start, 0)
..Point::new(hunk.associated_range.end, 0),
)
.collect::<String>(),
)

View File

@ -930,8 +930,17 @@ impl Buffer {
/// against the buffer text.
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
self.diff_base = diff_base;
self.git_diff_recalc(cx);
cx.emit(Event::DiffBaseChanged);
if let Some(recalc_task) = self.git_diff_recalc(cx) {
cx.spawn(|buffer, mut cx| async move {
recalc_task.await;
buffer
.update(&mut cx, |_, cx| {
cx.emit(Event::DiffBaseChanged);
})
.ok();
})
.detach();
}
}
/// Recomputes the Git diff status.

View File

@ -3186,19 +3186,21 @@ impl MultiBufferSnapshot {
.map(move |hunk| {
let start = multibuffer_start.row
+ hunk
.buffer_range
.associated_range
.start
.saturating_sub(excerpt_start_point.row);
let end = multibuffer_start.row
+ hunk
.buffer_range
.associated_range
.end
.min(excerpt_end_point.row + 1)
.saturating_sub(excerpt_start_point.row);
DiffHunk {
buffer_range: start..end,
associated_range: start..end,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
buffer_id: hunk.buffer_id,
}
});
@ -3215,52 +3217,65 @@ impl MultiBufferSnapshot {
) -> impl Iterator<Item = DiffHunk<u32>> + '_ {
let mut cursor = self.excerpts.cursor::<Point>();
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
cursor.seek(&Point::new(row_range.start, 0), Bias::Left, &());
std::iter::from_fn(move || {
let excerpt = cursor.item()?;
let multibuffer_start = *cursor.start();
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
if multibuffer_start.row >= row_range.end {
return None;
}
let mut buffer_start = excerpt.range.context.start;
let mut buffer_end = excerpt.range.context.end;
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
if row_range.start > multibuffer_start.row {
let buffer_start_point =
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
let excerpt_rows = match multibuffer_start.row.cmp(&row_range.end) {
cmp::Ordering::Less => {
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
if row_range.end < multibuffer_end.row {
let buffer_end_point =
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
}
if row_range.start > multibuffer_start.row {
let buffer_start_point = excerpt_start_point
+ Point::new(row_range.start - multibuffer_start.row, 0);
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
if row_range.end < multibuffer_end.row {
let buffer_end_point = excerpt_start_point
+ Point::new(row_range.end - multibuffer_start.row, 0);
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
}
excerpt_start_point.row..excerpt_end_point.row
}
cmp::Ordering::Equal if row_range.end == 0 => {
buffer_end = buffer_start;
0..0
}
cmp::Ordering::Greater | cmp::Ordering::Equal => return None,
};
let buffer_hunks = excerpt
.buffer
.git_diff_hunks_intersecting_range(buffer_start..buffer_end)
.map(move |hunk| {
let start = multibuffer_start.row
+ hunk
.buffer_range
.start
.saturating_sub(excerpt_start_point.row);
let end = multibuffer_start.row
+ hunk
.buffer_range
.end
.min(excerpt_end_point.row + 1)
.saturating_sub(excerpt_start_point.row);
let buffer_range = if excerpt_rows.start == 0 && excerpt_rows.end == 0 {
0..1
} else {
let start = multibuffer_start.row
+ hunk
.associated_range
.start
.saturating_sub(excerpt_rows.start);
let end = multibuffer_start.row
+ hunk
.associated_range
.end
.min(excerpt_rows.end + 1)
.saturating_sub(excerpt_rows.start);
start..end
};
DiffHunk {
buffer_range: start..end,
associated_range: buffer_range,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
buffer_id: hunk.buffer_id,
}
});

View File

@ -4427,7 +4427,6 @@ impl Project {
project_transaction.0.extend(new.0);
}
// TODO kb here too:
if let Some(command) = action.lsp_action.command {
project.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server