Fix the completions being too slow (#19013)

Kirill Bulatov and Antonio Scandurra created

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

Release Notes:

- Fixed completion items inserted with a delay
([#19005](https://github.com/zed-industries/zed/issues/19005))

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>

Change summary

crates/collab/src/tests/editor_tests.rs           | 84 +++++-----------
crates/copilot/src/copilot_completion_provider.rs | 10 -
crates/editor/src/debounced_delay.rs              | 22 +---
crates/editor/src/editor.rs                       | 47 +-------
crates/editor/src/editor_tests.rs                 | 18 +--
crates/editor/src/hover_popover.rs                | 17 +--
crates/editor/src/test/editor_test_context.rs     |  1 
crates/lsp/src/lsp.rs                             |  3 
crates/vim/src/normal/repeat.rs                   |  6 
9 files changed, 64 insertions(+), 144 deletions(-)

Detailed changes

crates/collab/src/tests/editor_tests.rs 🔗

@@ -379,75 +379,51 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
         .next()
         .await
         .unwrap();
+    cx_a.executor().finish_waiting();
+
     // Open the buffer on the host.
     let buffer_a = project_a
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
+    cx_a.executor().run_until_parked();
 
     buffer_a.read_with(cx_a, |buffer, _| {
         assert_eq!(buffer.text(), "fn main() { a. }")
     });
 
+    // Confirm a completion on the guest.
+    editor_b.update(cx_b, |editor, cx| {
+        assert!(editor.context_menu_visible());
+        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
+        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
+    });
+
     // Return a resolved completion from the host's language server.
     // The resolved completion has an additional text edit.
     fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
         |params, _| async move {
-            Ok(match params.label.as_str() {
-                "first_method(…)" => lsp::CompletionItem {
-                    label: "first_method(…)".into(),
-                    detail: Some("fn(&mut self, B) -> C".into()),
-                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                        new_text: "first_method($1)".to_string(),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 14),
-                            lsp::Position::new(0, 14),
-                        ),
-                    })),
-                    additional_text_edits: Some(vec![lsp::TextEdit {
-                        new_text: "use d::SomeTrait;\n".to_string(),
-                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
-                    }]),
-                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-                    ..Default::default()
-                },
-                "second_method(…)" => lsp::CompletionItem {
-                    label: "second_method(…)".into(),
-                    detail: Some("fn(&mut self, C) -> D<E>".into()),
-                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                        new_text: "second_method()".to_string(),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 14),
-                            lsp::Position::new(0, 14),
-                        ),
-                    })),
-                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-                    additional_text_edits: Some(vec![lsp::TextEdit {
-                        new_text: "use d::SomeTrait;\n".to_string(),
-                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
-                    }]),
-                    ..Default::default()
-                },
-                _ => panic!("unexpected completion label: {:?}", params.label),
+            assert_eq!(params.label, "first_method(…)");
+            Ok(lsp::CompletionItem {
+                label: "first_method(…)".into(),
+                detail: Some("fn(&mut self, B) -> C".into()),
+                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                    new_text: "first_method($1)".to_string(),
+                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+                })),
+                additional_text_edits: Some(vec![lsp::TextEdit {
+                    new_text: "use d::SomeTrait;\n".to_string(),
+                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+                }]),
+                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                ..Default::default()
             })
         },
     );
-    cx_a.executor().finish_waiting();
-    cx_a.executor().run_until_parked();
 
-    // Confirm a completion on the guest.
-    editor_b
-        .update(cx_b, |editor, cx| {
-            assert!(editor.context_menu_visible());
-            editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
-        })
-        .unwrap()
-        .await
-        .unwrap();
+    // The additional edit is applied.
     cx_a.executor().run_until_parked();
-    cx_b.executor().run_until_parked();
 
-    // The additional edit is applied.
     buffer_a.read_with(cx_a, |buffer, _| {
         assert_eq!(
             buffer.text(),
@@ -540,15 +516,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
     cx_b.executor().run_until_parked();
 
     // When accepting the completion, the snippet is insert.
-    editor_b
-        .update(cx_b, |editor, cx| {
-            assert!(editor.context_menu_visible());
-            editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
-        })
-        .unwrap()
-        .await
-        .unwrap();
     editor_b.update(cx_b, |editor, cx| {
+        assert!(editor.context_menu_visible());
+        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
         assert_eq!(
             editor.text(cx),
             "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"

crates/copilot/src/copilot_completion_provider.rs 🔗

@@ -363,12 +363,10 @@ mod tests {
 
             // Confirming a completion inserts it and hides the context menu, without showing
             // the copilot suggestion afterwards.
-            editor.confirm_completion(&Default::default(), cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-        cx.update_editor(|editor, cx| {
+            editor
+                .confirm_completion(&Default::default(), cx)
+                .unwrap()
+                .detach();
             assert!(!editor.context_menu_visible());
             assert!(!editor.has_active_inline_completion(cx));
             assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");

crates/editor/src/debounced_delay.rs 🔗

@@ -1,4 +1,4 @@
-use std::{ops::ControlFlow, time::Duration};
+use std::time::Duration;
 
 use futures::{channel::oneshot, FutureExt};
 use gpui::{Task, ViewContext};
@@ -7,7 +7,7 @@ use crate::Editor;
 
 pub struct DebouncedDelay {
     task: Option<Task<()>>,
-    cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>,
+    cancel_channel: Option<oneshot::Sender<()>>,
 }
 
 impl DebouncedDelay {
@@ -23,22 +23,17 @@ impl DebouncedDelay {
         F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
     {
         if let Some(channel) = self.cancel_channel.take() {
-            channel.send(ControlFlow::Break(())).ok();
+            _ = channel.send(());
         }
 
-        let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>();
+        let (sender, mut receiver) = oneshot::channel::<()>();
         self.cancel_channel = Some(sender);
 
         drop(self.task.take());
         self.task = Some(cx.spawn(move |model, mut cx| async move {
             let mut timer = cx.background_executor().timer(delay).fuse();
             futures::select_biased! {
-                interrupt = receiver => {
-                    match interrupt {
-                        Ok(ControlFlow::Break(())) | Err(_) => return,
-                        Ok(ControlFlow::Continue(())) => {},
-                    }
-                }
+                _ = receiver => return,
                 _ = timer => {}
             }
 
@@ -47,11 +42,4 @@ impl DebouncedDelay {
             }
         }));
     }
-
-    pub fn start_now(&mut self) -> Option<Task<()>> {
-        if let Some(channel) = self.cancel_channel.take() {
-            channel.send(ControlFlow::Continue(())).ok();
-        }
-        self.task.take()
-    }
 }

crates/editor/src/editor.rs 🔗

@@ -4427,49 +4427,16 @@ impl Editor {
         &mut self,
         item_ix: Option<usize>,
         intent: CompletionIntent,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Task<anyhow::Result<()>>> {
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
+        use language::ToOffset as _;
+
         let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
             menu
         } else {
             return None;
         };
 
-        let mut resolve_task_store = completions_menu
-            .selected_completion_documentation_resolve_debounce
-            .lock();
-        let selected_completion_resolve = resolve_task_store.start_now();
-        let menu_pre_resolve = self
-            .completion_documentation_pre_resolve_debounce
-            .start_now();
-        drop(resolve_task_store);
-
-        Some(cx.spawn(|editor, mut cx| async move {
-            match (selected_completion_resolve, menu_pre_resolve) {
-                (None, None) => {}
-                (Some(resolve), None) | (None, Some(resolve)) => resolve.await,
-                (Some(resolve_1), Some(resolve_2)) => {
-                    futures::join!(resolve_1, resolve_2);
-                }
-            }
-            if let Some(apply_edits_task) = editor.update(&mut cx, |editor, cx| {
-                editor.apply_resolved_completion(completions_menu, item_ix, intent, cx)
-            })? {
-                apply_edits_task.await?;
-            }
-            Ok(())
-        }))
-    }
-
-    fn apply_resolved_completion(
-        &mut self,
-        completions_menu: CompletionsMenu,
-        item_ix: Option<usize>,
-        intent: CompletionIntent,
-        cx: &mut ViewContext<'_, Editor>,
-    ) -> Option<Task<anyhow::Result<Option<language::Transaction>>>> {
-        use language::ToOffset as _;
-
         let mat = completions_menu
             .matches
             .get(item_ix.unwrap_or(completions_menu.selected_item))?;
@@ -4628,7 +4595,11 @@ impl Editor {
             // so we should automatically call signature_help
             self.show_signature_help(&ShowSignatureHelp, cx);
         }
-        Some(apply_edits)
+
+        Some(cx.foreground_executor().spawn(async move {
+            apply_edits.await?;
+            Ok(())
+        }))
     }
 
     pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {

crates/editor/src/editor_tests.rs 🔗

@@ -7996,7 +7996,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
             .unwrap()
     });
     cx.assert_editor_state(indoc! {"
-        one.ˇ
+        one.second_completionˇ
         two
         three
     "});
@@ -8029,9 +8029,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
     cx.assert_editor_state(indoc! {"
         one.second_completionˇ
         two
-        thoverlapping additional editree
-
-        additional edit"});
+        three
+        additional edit
+    "});
 
     cx.set_state(indoc! {"
         one.second_completion
@@ -8091,8 +8091,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
     });
     cx.assert_editor_state(indoc! {"
         one.second_completion
-        two siˇ
-        three siˇ
+        two sixth_completionˇ
+        three sixth_completionˇ
         additional edit
     "});
 
@@ -8133,11 +8133,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
             .confirm_completion(&ConfirmCompletion::default(), cx)
             .unwrap()
     });
-    cx.assert_editor_state("editor.cloˇ");
+    cx.assert_editor_state("editor.closeˇ");
     handle_resolve_completion_request(&mut cx, None).await;
     apply_additional_edits.await.unwrap();
-    cx.assert_editor_state(indoc! {"
-    editor.closeˇ"});
 }
 
 #[gpui::test]
@@ -10142,7 +10140,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
             .confirm_completion(&ConfirmCompletion::default(), cx)
             .unwrap()
     });
-    cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
+    cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
 
     cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
         let task_completion_item = completion_item.clone();

crates/editor/src/hover_popover.rs 🔗

@@ -821,7 +821,7 @@ mod tests {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
                 completion_provider: Some(lsp::CompletionOptions {
                     trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-                    resolve_provider: Some(false),
+                    resolve_provider: Some(true),
                     ..Default::default()
                 }),
                 ..Default::default()
@@ -913,15 +913,12 @@ mod tests {
         assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
 
         //apply a completion and check it was successfully applied
-        let () = cx
-            .update_editor(|editor, cx| {
-                editor.context_menu_next(&Default::default(), cx);
-                editor
-                    .confirm_completion(&ConfirmCompletion::default(), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
+        let _apply_additional_edits = cx.update_editor(|editor, cx| {
+            editor.context_menu_next(&Default::default(), cx);
+            editor
+                .confirm_completion(&ConfirmCompletion::default(), cx)
+                .unwrap()
+        });
         cx.assert_editor_state(indoc! {"
             one.second_completionˇ
             two

crates/editor/src/test/editor_test_context.rs 🔗

@@ -13,7 +13,6 @@ use itertools::Itertools;
 use language::{Buffer, BufferSnapshot, LanguageRegistry};
 use multi_buffer::{ExcerptRange, ToPoint};
 use parking_lot::RwLock;
-use pretty_assertions::assert_eq;
 use project::{FakeFs, Project};
 use std::{
     any::TypeId,

crates/lsp/src/lsp.rs 🔗

@@ -631,7 +631,8 @@ impl LanguageServer {
                                     "filterText".to_string(),
                                     "labelDetails".to_string(),
                                     "tags".to_string(),
-                                    "textEdit".to_string(),
+                                    // NB: Do not have this resolved, otherwise Zed becomes slow to complete things
+                                    // "textEdit".to_string(),
                                 ],
                             }),
                             insert_replace_support: Some(true),

crates/vim/src/normal/repeat.rs 🔗

@@ -387,7 +387,7 @@ mod test {
             lsp::ServerCapabilities {
                 completion_provider: Some(lsp::CompletionOptions {
                     trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-                    resolve_provider: Some(false),
+                    resolve_provider: Some(true),
                     ..Default::default()
                 }),
                 ..Default::default()
@@ -432,9 +432,7 @@ mod test {
         request.next().await;
         cx.condition(|editor, _| editor.context_menu_visible())
             .await;
-        cx.simulate_keystrokes("down enter");
-        cx.run_until_parked();
-        cx.simulate_keystrokes("! escape");
+        cx.simulate_keystrokes("down enter ! escape");
 
         cx.assert_state(
             indoc! {"