@@ -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::<lsp::request::Completion, _, _>(|_, _| 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::<lsp::request::Completion, _, _>(|_, _| 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::<lsp::request::Completion, _, _>(|_, _| 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| {