editor: Immediately cancel popover task if the hover ends before the popover is shown (#53168)

Tim Vermeulen and Lukas Wirth created

Hover popovers aren't immediately dismissed when the hover ends, but
this delay mechanism is currently causing popovers to sometimes still
briefly appear when the hover lasted too short to have triggered it.
Fixed by immediately cancelling the popover task when the hover ends
before the popover is displayed.

Most easily reproduced when setting the hover popover delay to a value
lower than the default 300ms (but it still happens sometimes with the
default value), because the popover hide delay is also 300ms. Here's it
with `"hover_popover_delay": 200`:


https://github.com/user-attachments/assets/6415d112-d8e0-4a87-9a79-a7ab559f20f2

After the fix:


https://github.com/user-attachments/assets/34782389-de4c-4a25-bd6e-4858b55028de

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed hover popovers sometimes briefly appearing after the hover
already ended.

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>

Change summary

crates/editor/src/hover_popover.rs | 60 ++++++++++++++++++++++++++++++++
1 file changed, 60 insertions(+)

Detailed changes

crates/editor/src/hover_popover.rs 🔗

@@ -62,6 +62,8 @@ pub fn hover_at(
             editor.hover_state.hiding_delay_task = None;
             editor.hover_state.closest_mouse_distance = None;
             show_hover(editor, anchor, false, window, cx);
+        } else if !editor.hover_state.visible() {
+            editor.hover_state.info_task = None;
         } else {
             let settings = EditorSettings::get_global(cx);
             if !settings.hover_popover_sticky {
@@ -1501,6 +1503,64 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_mouse_hover_cancelled_before_delay(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            fn ˇtest() { println!(); }
+        "});
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { printˇln!(); }
+        "});
+
+        cx.update_editor(|editor, window, cx| {
+            let snapshot = editor.snapshot(window, cx);
+            let anchor = snapshot
+                .buffer_snapshot()
+                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
+            hover_at(editor, Some(anchor), None, window, cx);
+            hover_at(editor, None, None, window, cx);
+        });
+
+        let request_count = Arc::new(AtomicUsize::new(0));
+        cx.set_request_handler::<lsp::request::HoverRequest, _, _>({
+            let request_count = request_count.clone();
+            move |_, _, _| {
+                let request_count = request_count.clone();
+                async move {
+                    request_count.fetch_add(1, atomic::Ordering::Release);
+                    Ok(Some(lsp::Hover {
+                        contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                            kind: lsp::MarkupKind::Markdown,
+                            value: "some basic docs".to_string(),
+                        }),
+                        range: None,
+                    }))
+                }
+            }
+        });
+
+        cx.background_executor
+            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
+        cx.background_executor.run_until_parked();
+        cx.run_until_parked();
+
+        assert_eq!(request_count.load(atomic::Ordering::Acquire), 0);
+        cx.editor(|editor, _, _| {
+            assert!(!editor.hover_state.visible());
+        });
+    }
+
     #[gpui::test]
     async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});