diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index de674fc25b..936170349a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14175,12 +14175,28 @@ impl Editor { } }; + let transaction_id_prev = buffer.read_with(cx, |b, cx| b.last_transaction_id(cx)); + let selections_prev = transaction_id_prev + .and_then(|transaction_id_prev| { + // default to selections as they were after the last edit, if we have them, + // instead of how they are now. + // This will make it so that editing, moving somewhere else, formatting, then undoing the format + // will take you back to where you made the last edit, instead of staying where you scrolled + self.selection_history + .transaction(transaction_id_prev) + .map(|t| t.0.clone()) + }) + .unwrap_or_else(|| { + log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); + self.selections.disjoint_anchors() + }); + let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| { project.format(buffers, target, true, trigger, cx) }); - cx.spawn_in(window, async move |_, cx| { + cx.spawn_in(window, async move |editor, cx| { let transaction = futures::select_biased! { transaction = format.log_err().fuse() => transaction, () = timeout => { @@ -14200,6 +14216,19 @@ impl Editor { }) .ok(); + if let Some(transaction_id_now) = + buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))? + { + let has_new_transaction = transaction_id_prev != Some(transaction_id_now); + if has_new_transaction { + _ = editor.update(cx, |editor, _| { + editor + .selection_history + .insert_transaction(transaction_id_now, selections_prev); + }); + } + } + Ok(()) }) } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b0034d9df1..fcacecc01e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5873,6 +5873,83 @@ async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + line 1 + line 2 + linˇe 3 + line 4 + line 5 + "}); + + // Make an edit + cx.update_editor(|editor, window, cx| { + editor.handle_input("X", window, cx); + }); + + // Move cursor to a different position + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]); + }); + }); + + cx.assert_editor_state(indoc! {" + line 1 + line 2 + linXe 3 + line 4 + liˇne 5 + "}); + + cx.lsp + .set_request_handler::(move |_, _| async move { + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + "PREFIX ".to_string(), + )])) + }); + + cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx)) + .unwrap() + .await + .unwrap(); + + cx.assert_editor_state(indoc! {" + PREFIX line 1 + line 2 + linXe 3 + line 4 + liˇne 5 + "}); + + // Undo formatting + cx.update_editor(|editor, window, cx| { + editor.undo(&Default::default(), window, cx); + }); + + // Verify cursor moved back to position after edit + cx.assert_editor_state(indoc! {" + line 1 + line 2 + linXˇe 3 + line 4 + line 5 + "}); +} + #[gpui::test] async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index cda6161406..18c17b3b02 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1118,9 +1118,16 @@ impl MultiBuffer { self.history.start_transaction(now) } - pub fn last_transaction_id(&self) -> Option { - let last_transaction = self.history.undo_stack.last()?; - return Some(last_transaction.id); + pub fn last_transaction_id(&self, cx: &App) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.read_with(cx, |b, _| { + b.peek_undo_stack() + .map(|history_entry| history_entry.transaction_id()) + }); + } else { + let last_transaction = self.history.undo_stack.last()?; + return Some(last_transaction.id); + } } pub fn end_transaction(&mut self, cx: &mut Context) -> Option {