editor: Fix function completion expansion in string contexts and call expressions (#30351)

Smit Barmase created

Closes #27582

Now, when accepting function completion, it doesn't expand with
parentheses and arguments in the following cases:
1. If it's in a string (like `type Foo = MyClass["sayHello"]` instead of
`type Foo = MyClass["sayHello(name)"]`)
2. If it's in a call expression (like `useRef<HTMLDivElement>(null)`
over `useRef(initialValue)<HTMLDivElement>(null)`)

This is a follow-up to https://github.com/zed-industries/zed/pull/30312,
more like cleaner version of it.

Release Notes:

- Fixed an issue where accepting a method as an object string in
JavaScript would incorrectly expand. E.g. `MyClass["sayHello(name)"]`
instead of `MyClass["sayHello"]`.

Change summary

crates/editor/src/editor.rs                   | 34 ++++++++------------
crates/language/src/language.rs               | 14 ++++++++
crates/languages/src/tsx/config.toml          |  4 ++
crates/languages/src/tsx/overrides.scm        |  2 +
crates/languages/src/typescript/config.toml   |  4 ++
crates/languages/src/typescript/overrides.scm |  2 +
6 files changed, 39 insertions(+), 21 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -5051,27 +5051,19 @@ impl Editor {
         let snippet;
         let new_text;
         if completion.is_snippet() {
-            // lsp returns function definition with placeholders in "new_text"
-            // when configured from language server, even when renaming a function
-            //
-            // in such cases, we use the label instead
-            // https://github.com/zed-industries/zed/issues/29982
-            let snippet_source = completion
-                .label()
-                .filter(|label| {
-                    completion.kind() == Some(CompletionItemKind::FUNCTION)
-                        && label != &completion.new_text
-                })
-                .and_then(|label| {
-                    let cursor_offset = newest_anchor.head().to_offset(&snapshot);
-                    let next_char_is_not_whitespace = snapshot
-                        .chars_at(cursor_offset)
-                        .next()
-                        .map_or(true, |ch| !ch.is_whitespace());
-                    next_char_is_not_whitespace.then_some(label)
-                })
-                .unwrap_or(completion.new_text.clone());
-
+            let mut snippet_source = completion.new_text.clone();
+            if let Some(scope) = snapshot.language_scope_at(newest_anchor.head()) {
+                if scope.prefers_label_for_snippet_in_completion() {
+                    if let Some(label) = completion.label() {
+                        if matches!(
+                            completion.kind(),
+                            Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD)
+                        ) {
+                            snippet_source = label;
+                        }
+                    }
+                }
+            }
             snippet = Some(Snippet::parse(&snippet_source).log_err()?);
             new_text = snippet.as_ref().unwrap().text.clone();
         } else {

crates/language/src/language.rs 🔗

@@ -828,6 +828,8 @@ pub struct LanguageConfigOverride {
     pub completion_query_characters: Override<HashSet<char>>,
     #[serde(default)]
     pub opt_into_language_servers: Vec<LanguageServerName>,
+    #[serde(default)]
+    pub prefer_label_for_snippet: Option<bool>,
 }
 
 #[derive(Clone, Deserialize, Debug, Serialize, JsonSchema)]
@@ -1788,6 +1790,18 @@ impl LanguageScope {
         )
     }
 
+    /// Returns whether to prefer snippet `label` over `new_text` to replace text when
+    /// completion is accepted.
+    ///
+    /// In cases like when cursor is in string or renaming existing function,
+    /// you don't want to expand function signature instead just want function name
+    /// to replace existing one.
+    pub fn prefers_label_for_snippet_in_completion(&self) -> bool {
+        self.config_override()
+            .and_then(|o| o.prefer_label_for_snippet)
+            .unwrap_or(false)
+    }
+
     /// Returns a list of bracket pairs for a given language with an additional
     /// piece of information about whether the particular bracket pair is currently active for a given language.
     pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {

crates/languages/src/tsx/config.toml 🔗

@@ -34,3 +34,7 @@ opt_into_language_servers = ["emmet-language-server"]
 [overrides.string]
 completion_query_characters = ["-", "."]
 opt_into_language_servers = ["tailwindcss-language-server"]
+prefer_label_for_snippet = true
+
+[overrides.call_expression]
+prefer_label_for_snippet = true

crates/languages/src/typescript/config.toml 🔗

@@ -21,3 +21,7 @@ debuggers = ["JavaScript"]
 
 [overrides.string]
 completion_query_characters = ["."]
+prefer_label_for_snippet = true
+
+[overrides.call_expression]
+prefer_label_for_snippet = true