editor: Fix completions menu flashes on every keystroke in TSX files with emmet (#38320)

Smit Barmase created

Closes https://github.com/zed-industries/zed/issues/37774

Bug in https://github.com/zed-industries/zed/pull/32927

Instead of using trigger characters to clear cached completions items,
now we check if the query is empty to clear it. Turns out Emmet defines
whole [alphanumeric as trigger
characters](https://github.com/olrtg/emmet-language-server/blob/279be108725fb391c167690b697ce154fd32657b/index.ts#L116)
which causes flickering.

Clear on trigger characters was introduced to get rid of cached
completions like in the case of "Parent.Foo.Bar", where "." is one of
the trigger characters. This works still since "." is not part of
`completion_query_characters` and hence we use it as a boundary while
building the current query. i.e in this case, the query would be empty
after typing ".", clearing cached completions.

Release Notes:

- Fixed issue where completions menu flashed on every keystroke in TSX
files with emmet extension installed.

Change summary

crates/editor/src/editor.rs | 61 +++++++++++++++++++-------------------
1 file changed, 30 insertions(+), 31 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -5488,6 +5488,18 @@ impl Editor {
 
         drop(multibuffer_snapshot);
 
+        // Hide the current completions menu when query is empty. Without this, cached
+        // completions from before the trigger char may be reused (#32774).
+        if query.is_none() {
+            let menu_is_open = matches!(
+                self.context_menu.borrow().as_ref(),
+                Some(CodeContextMenu::Completions(_))
+            );
+            if menu_is_open {
+                self.hide_context_menu(window, cx);
+            }
+        }
+
         let mut ignore_word_threshold = false;
         let provider = match requested_source {
             Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),
@@ -5509,37 +5521,6 @@ impl Editor {
             .as_ref()
             .is_none_or(|provider| provider.filter_completions());
 
-        let trigger_kind = match trigger {
-            Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
-                CompletionTriggerKind::TRIGGER_CHARACTER
-            }
-            _ => CompletionTriggerKind::INVOKED,
-        };
-        let completion_context = CompletionContext {
-            trigger_character: trigger.and_then(|trigger| {
-                if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
-                    Some(String::from(trigger))
-                } else {
-                    None
-                }
-            }),
-            trigger_kind,
-        };
-
-        // Hide the current completions menu when a trigger char is typed. Without this, cached
-        // completions from before the trigger char may be reused (#32774). Snippet choices could
-        // involve trigger chars, so this is skipped in that case.
-        if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER && self.snippet_stack.is_empty()
-        {
-            let menu_is_open = matches!(
-                self.context_menu.borrow().as_ref(),
-                Some(CodeContextMenu::Completions(_))
-            );
-            if menu_is_open {
-                self.hide_context_menu(window, cx);
-            }
-        }
-
         if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
             if filter_completions {
                 menu.filter(query.clone(), provider.clone(), window, cx);
@@ -5570,11 +5551,29 @@ impl Editor {
             }
         };
 
+        let trigger_kind = match trigger {
+            Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
+                CompletionTriggerKind::TRIGGER_CHARACTER
+            }
+            _ => CompletionTriggerKind::INVOKED,
+        };
+        let completion_context = CompletionContext {
+            trigger_character: trigger.and_then(|trigger| {
+                if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
+                    Some(String::from(trigger))
+                } else {
+                    None
+                }
+            }),
+            trigger_kind,
+        };
+
         let Anchor {
             excerpt_id: buffer_excerpt_id,
             text_anchor: buffer_position,
             ..
         } = buffer_position;
+
         let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) =
             buffer_snapshot.surrounding_word(buffer_position, false)
         {