diff --git a/crates/editor/src/completions.rs b/crates/editor/src/completions.rs index 2be7f28c5b..72bb0af3e7 100644 --- a/crates/editor/src/completions.rs +++ b/crates/editor/src/completions.rs @@ -975,6 +975,11 @@ impl Editor { self.show_signature_help(&ShowSignatureHelp, window, cx); } + // After the code completion is finished, we should finalize the last transaction. + // This ensure vim/helix not group the edits together. + self.buffer + .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); + Some(cx.spawn_in(window, async move |editor, cx| { let additional_edits_tx = apply_edits.await?; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 8b5570125c..99eda3d6c4 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1402,6 +1402,204 @@ async fn test_undo(cx: &mut gpui::TestAppContext) { 3"}); } +#[perf] +#[gpui::test] +async fn test_lsp_completions_undo(cx: &mut gpui::TestAppContext) { + use editor::test::editor_lsp_test_context::EditorLspTestContext; + VimTestContext::init(cx); + let mut cx = VimTestContext::new_with_lsp( + EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + signature_help_provider: Some(lsp::SignatureHelpOptions::default()), + ..Default::default() + }, + cx, + ) + .await, + true, + ); + + cx.set_state("fn main() { let a = ˇ }", Mode::Normal); + cx.simulate_keystroke("i"); + cx.set_state("fn main() { let a = ˇ }", Mode::Insert); + + cx.simulate_keystroke(";"); + cx.assert_state("fn main() { let a = ;ˇ }", Mode::Insert); + + cx.simulate_keystroke("escape"); + cx.assert_state("fn main() { let a = ˇ; }", Mode::Normal); + + cx.simulate_keystroke("i"); + cx.simulate_keystroke("2"); + cx.assert_state("fn main() { let a = 2ˇ; }", Mode::Insert); + cx.simulate_keystroke("."); + + let completion_item = lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 22, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "completion".to_string(), + })), + additional_text_edits: None, + ..Default::default() + }; + + let closure_completion_item = completion_item.clone(); + let mut request = cx.set_request_handler::(move |_, _, _| { + let task_completion_item = closure_completion_item.clone(); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + task_completion_item, + ]))) + } + }); + + request.next().await; + + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + let _ = cx.update_editor(|editor, window, cx| { + editor + .confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx) + .unwrap() + }); + + cx.assert_editor_state("fn main() { let a = 2.completionˇ; }"); + + cx.simulate_keystrokes("escape u"); + + cx.assert_editor_state("fn main() { let a = 2.ˇ; }"); +} + +#[perf] +#[gpui::test] +async fn test_lsp_completions_with_additional_edits_undo(cx: &mut gpui::TestAppContext) { + use editor::test::editor_lsp_test_context::EditorLspTestContext; + VimTestContext::init(cx); + let mut cx = VimTestContext::new_with_lsp( + EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await, + true, + ); + + cx.set_state("fn main() { let a = ˇ }", Mode::Normal); + cx.simulate_keystroke("i"); + cx.set_state("fn main() { let a = ˇ }", Mode::Insert); + + cx.simulate_keystroke("2"); + cx.simulate_keystroke(";"); + cx.assert_state("fn main() { let a = 2;ˇ }", Mode::Insert); + + cx.simulate_keystroke("escape"); + cx.assert_state("fn main() { let a = 2ˇ; }", Mode::Normal); + + cx.simulate_keystroke("i"); + cx.assert_state("fn main() { let a = 2ˇ; }", Mode::Insert); + + cx.simulate_keystroke("."); + let completion_item = lsp::CompletionItem { + label: "some".into(), + kind: Some(lsp::CompletionItemKind::SNIPPET), + detail: Some("Wrap the expression in an `Option::Some`".to_string()), + documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "```rust\nSome(2)\n```".to_string(), + })), + deprecated: Some(false), + sort_text: Some("fffffff2".to_string()), + filter_text: Some("some".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 22, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "Some(2)".to_string(), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "".to_string(), + }]), + ..Default::default() + }; + + let closure_completion_item = completion_item.clone(); + let mut request = cx.set_request_handler::(move |_, _, _| { + let task_completion_item = closure_completion_item.clone(); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + task_completion_item, + ]))) + } + }); + + request.next().await; + + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = cx.update_editor(|editor, window, cx| { + editor + .confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx) + .unwrap() + }); + cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }"); + + cx.set_request_handler::(move |_, _, _| { + let task_completion_item = completion_item.clone(); + async move { Ok(task_completion_item) } + }) + .next() + .await + .unwrap(); + + apply_additional_edits.await.unwrap(); + cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }"); + + cx.simulate_keystrokes("escape u"); + + cx.assert_editor_state("fn main() { let a = 2.ˇ; }"); +} + #[perf] #[gpui::test] async fn test_mouse_selection(cx: &mut TestAppContext) {