Detailed changes
@@ -118,7 +118,8 @@
"stop_at_soft_wraps": true
}
],
- "ctrl-;": "editor::ToggleLineNumbers"
+ "ctrl-;": "editor::ToggleLineNumbers",
+ "ctrl-alt-z": "editor::RevertSelectedHunks"
}
},
{
@@ -153,7 +153,8 @@
}
],
"ctrl-cmd-space": "editor::ShowCharacterPalette",
- "cmd-;": "editor::ToggleLineNumbers"
+ "cmd-;": "editor::ToggleLineNumbers",
+ "cmd-alt-z": "editor::RevertSelectedHunks"
}
},
{
@@ -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() {
@@ -210,6 +210,7 @@ gpui::actions!(
PageDown,
PageUp,
Paste,
+ RevertSelectedHunks,
Redo,
RedoSelection,
Rename,
@@ -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())
}
@@ -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 «Rˇ»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);
+}
@@ -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)
@@ -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()
@@ -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())
@@ -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>(),
)
@@ -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.
@@ -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,
}
});
@@ -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