diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index d5abc60002..caa59c11c5 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2776,6 +2776,210 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { } } +#[gpui::test] +async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "a.ts": "", + }), + ) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); + + let (buffer, _handle) = project + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + // When text_edit exists, it takes precedence over insert_text and label + let text = "let a = obj.fqn"; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx) + }); + + fake_server + .set_request_handler::(|_, _| async { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "labelText".into(), + insert_text: Some("insertText".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, text.len() as u32 - 3), + lsp::Position::new(0, text.len() as u32), + ), + new_text: "textEditText".into(), + })), + ..Default::default() + }, + ]))) + }) + .next() + .await; + + let completions = completions.await.unwrap().unwrap(); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "textEditText"); + assert_eq!( + completions[0].old_range.to_offset(&snapshot), + text.len() - 3..text.len() + ); +} + +#[gpui::test] +async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "a.ts": "", + }), + ) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); + + let (buffer, _handle) = project + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + let text = "let a = obj.fqn"; + + // Test 1: When text_edit is None but insert_text exists with default edit_range + { + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx) + }); + + fake_server + .set_request_handler::(|_, _| async { + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: false, + item_defaults: Some(lsp::CompletionListItemDefaults { + edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range( + lsp::Range::new( + lsp::Position::new(0, text.len() as u32 - 3), + lsp::Position::new(0, text.len() as u32), + ), + )), + ..Default::default() + }), + items: vec![lsp::CompletionItem { + label: "labelText".into(), + insert_text: Some("insertText".into()), + text_edit: None, + ..Default::default() + }], + }))) + }) + .next() + .await; + + let completions = completions.await.unwrap().unwrap(); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "insertText"); + assert_eq!( + completions[0].old_range.to_offset(&snapshot), + text.len() - 3..text.len() + ); + } + + // Test 2: When both text_edit and insert_text are None with default edit_range + { + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx) + }); + + fake_server + .set_request_handler::(|_, _| async { + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: false, + item_defaults: Some(lsp::CompletionListItemDefaults { + edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range( + lsp::Range::new( + lsp::Position::new(0, text.len() as u32 - 3), + lsp::Position::new(0, text.len() as u32), + ), + )), + ..Default::default() + }), + items: vec![lsp::CompletionItem { + label: "labelText".into(), + insert_text: None, + text_edit: None, + ..Default::default() + }], + }))) + }) + .next() + .await; + + let completions = completions.await.unwrap().unwrap(); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "labelText"); + assert_eq!( + completions[0].old_range.to_offset(&snapshot), + text.len() - 3..text.len() + ); + } +} + #[gpui::test] async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -2816,6 +3020,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { let fake_server = fake_language_servers.next().await.unwrap(); + // Test 1: When text_edit is None but insert_text exists (no edit_range in defaults) let text = "let a = b.fqn"; buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); let completions = project.update(cx, |project, cx| { @@ -2843,6 +3048,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { text.len() - 3..text.len() ); + // Test 2: When both text_edit and insert_text are None (no edit_range in defaults) let text = "let a = \"atoms/cmp\""; buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); let completions = project.update(cx, |project, cx| {