Allow information popovers while showing autocomplete suggestions (#12157)

Ephram created

Allows information popovers to display while autocomplete suggestions
are also being displayed.

Before: 


https://github.com/zed-industries/zed/assets/50590465/a61f0b4e-1521-42de-84fd-c5a3586b74ab

After:


https://github.com/zed-industries/zed/assets/50590465/ad2daae7-aebc-4c71-ad4c-f9d2deb6d0d0

Release Notes:
- Allow hover and completion popovers to appear at the same time ([12152](https://github.com/zed-industries/zed/issues/12152))

Change summary

crates/editor/src/element.rs       |   6 
crates/editor/src/hover_popover.rs | 173 +++++++++++++++++++++++++++++++
2 files changed, 175 insertions(+), 4 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -4614,11 +4614,11 @@ impl Element for EditorElement {
 
                     let gutter_settings = EditorSettings::get_global(cx).gutter;
 
-                    let mut context_menu_visible = false;
+                    let mut _context_menu_visible = false;
                     let mut code_actions_indicator = None;
                     if let Some(newest_selection_head) = newest_selection_head {
                         if (start_row..end_row).contains(&newest_selection_head.row()) {
-                            context_menu_visible = self.layout_context_menu(
+                            _context_menu_visible = self.layout_context_menu(
                                 line_height,
                                 &hitbox,
                                 &text_hitbox,
@@ -4671,7 +4671,7 @@ impl Element for EditorElement {
                         cx,
                     );
 
-                    if !context_menu_visible && !cx.has_active_drag() {
+                    if !cx.has_active_drag() {
                         self.layout_hover_popovers(
                             &snapshot,
                             &hitbox,

crates/editor/src/hover_popover.rs 🔗

@@ -612,7 +612,8 @@ impl DiagnosticPopover {
 mod tests {
     use super::*;
     use crate::{
-        editor_tests::init_test,
+        actions::ConfirmCompletion,
+        editor_tests::{handle_completion_request, init_test},
         hover_links::update_inlay_link_and_hover_points,
         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
         test::editor_lsp_test_context::EditorLspTestContext,
@@ -625,10 +626,180 @@ mod tests {
     use lsp::LanguageServerId;
     use project::{HoverBlock, HoverBlockKind};
     use smol::stream::StreamExt;
+    use std::sync::atomic;
+    use std::sync::atomic::AtomicUsize;
     use text::Bias;
     use unindent::Unindent;
     use util::test::marked_text_ranges;
 
+    #[gpui::test]
+    async fn test_mouse_hover_info_popover_with_autocomplete_popover(
+        cx: &mut gpui::TestAppContext,
+    ) {
+        init_test(cx, |_| {});
+        const HOVER_DELAY_MILLIS: u64 = 350;
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                    resolve_provider: Some(true),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+        let counter = Arc::new(AtomicUsize::new(0));
+        // Basic hover delays and then pops without moving the mouse
+        cx.set_state(indoc! {"
+                oneˇ
+                two
+                three
+                fn test() { println!(); }
+            "});
+
+        //prompt autocompletion menu
+        cx.simulate_keystroke(".");
+        handle_completion_request(
+            &mut cx,
+            indoc! {"
+                        one.|<>
+                        two
+                        three
+                    "},
+            vec!["first_completion", "second_completion"],
+            counter.clone(),
+        )
+        .await;
+        cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
+            .await;
+        assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
+
+        let hover_point = cx.display_point(indoc! {"
+                one.
+                two
+                three
+                fn test() { printˇln!(); }
+            "});
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let anchor = snapshot
+                .buffer_snapshot
+                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
+            hover_at(editor, Some(anchor), cx)
+        });
+        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
+
+        // After delay, hover should be visible.
+        let symbol_range = cx.lsp_range(indoc! {"
+                one.
+                two
+                three
+                fn test() { «println!»(); }
+            "});
+        let mut requests =
+            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: "some basic docs".to_string(),
+                    }),
+                    range: Some(symbol_range),
+                }))
+            });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        requests.next().await;
+
+        cx.editor(|editor, _| {
+            assert!(editor.hover_state.visible());
+            assert_eq!(
+                editor.hover_state.info_popovers.len(),
+                1,
+                "Expected exactly one hover but got: {:?}",
+                editor.hover_state.info_popovers
+            );
+            let rendered = editor
+                .hover_state
+                .info_popovers
+                .first()
+                .cloned()
+                .unwrap()
+                .parsed_content;
+            assert_eq!(rendered.text, "some basic docs".to_string())
+        });
+
+        // check that the completion menu is still visible and that there still has only been 1 completion request
+        cx.editor(|editor, _| assert!(editor.context_menu_visible()));
+        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+
+        //apply a completion and check it was successfully applied
+        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
+            three
+            fn test() { println!(); }
+        "});
+
+        // check that the completion menu is no longer visible and that there still has only been 1 completion request
+        cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
+        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
+
+        //verify the information popover is still visible and unchanged
+        cx.editor(|editor, _| {
+            assert!(editor.hover_state.visible());
+            assert_eq!(
+                editor.hover_state.info_popovers.len(),
+                1,
+                "Expected exactly one hover but got: {:?}",
+                editor.hover_state.info_popovers
+            );
+            let rendered = editor
+                .hover_state
+                .info_popovers
+                .first()
+                .cloned()
+                .unwrap()
+                .parsed_content;
+            assert_eq!(rendered.text, "some basic docs".to_string())
+        });
+
+        // Mouse moved with no hover response dismisses
+        let hover_point = cx.display_point(indoc! {"
+                one.second_completionˇ
+                two
+                three
+                fn teˇst() { println!(); }
+            "});
+        let mut request = cx
+            .lsp
+            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let anchor = snapshot
+                .buffer_snapshot
+                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
+            hover_at(editor, Some(anchor), cx)
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        request.next().await;
+
+        // verify that the information popover is no longer visible
+        cx.editor(|editor, _| {
+            assert!(!editor.hover_state.visible());
+        });
+    }
+
     #[gpui::test]
     async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});