project: Add tests for more cases in LSP completions (#27650)

Smit Barmase created

This PR adds two more cases to existing LSP completion cases.

- When text_edit exists:  (New test)
   1. we use text_edit, over insert_text and label

- When edit range exists (and text_edit is None):  (New test)
   1. insert_text is used over label if exists
   2. label is used otherwise

- When not edit range exists (and text_edit is None):  (Existing test)
   1. insert_text is used over label if exists
   2. label is used otherwise

Release Notes:

- N/A

Change summary

crates/project/src/project_tests.rs | 206 +++++++++++++++++++++++++++++++
1 file changed, 206 insertions(+)

Detailed changes

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::<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| {