editor: Hide hover links when mouse cursor is not visible (#50424)

Karthik Nishanth created

When I am in the middle of editing, pressing Ctrl would
counter-intuitively highlight links even when the mouse cursor is
hidden. This change considers the state of the mouse cursor before
painting links on hover.

Before: Modifier pressed, cursor hidden, link visible

<img width="506" height="518" alt="image"
src="https://github.com/user-attachments/assets/82a59e83-e3cb-490f-b292-148686ec569d"
/>

After: Modifier pressed, cursor hidden (red dot indicates current cursor
position)

<img width="408" height="298" alt="image"
src="https://github.com/user-attachments/assets/c19ed83c-4778-4890-97b9-5155cdcf658b"
/>

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- Fixed spurious link highlighting when mouse cursor is hidden

Fixes #50776

Change summary

crates/editor/src/hover_links.rs | 80 +++++++++++++++++++++++++++++++++
1 file changed, 78 insertions(+), 2 deletions(-)

Detailed changes

crates/editor/src/hover_links.rs 🔗

@@ -119,7 +119,7 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx);
-        if !hovered_link_modifier || self.has_pending_selection() {
+        if !hovered_link_modifier || self.has_pending_selection() || self.mouse_cursor_hidden {
             self.hide_hovered_link(cx);
             return;
         }
@@ -782,7 +782,7 @@ fn surrounding_filename(
 mod tests {
     use super::*;
     use crate::{
-        DisplayPoint,
+        DisplayPoint, HideMouseCursorOrigin,
         display_map::ToDisplayPoint,
         editor_tests::init_test,
         inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
@@ -1362,6 +1362,82 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_hover_preconditions(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        macro_rules! assert_no_highlight {
+            ($cx:expr) => {
+                // No highlight
+                $cx.update_editor(|editor, window, cx| {
+                    assert!(
+                        editor
+                            .snapshot(window, cx)
+                            .text_highlight_ranges(HighlightKey::HoveredLinkState)
+                            .unwrap_or_default()
+                            .1
+                            .is_empty()
+                    );
+                });
+            };
+        }
+
+        // No link
+        cx.set_state(indoc! {"
+            Let's test a [complex](https://zed.dev/channel/) caseˇ.
+        "});
+        assert_no_highlight!(cx);
+
+        // No modifier
+        let screen_coord = cx.pixel_position(indoc! {"
+            Let's test a [complex](https://zed.dev/channel/ˇ) case.
+            "});
+        cx.simulate_mouse_move(screen_coord, None, Modifiers::none());
+        assert_no_highlight!(cx);
+
+        // Modifier active
+        let screen_coord = cx.pixel_position(indoc! {"
+            Let's test a [complex](https://zed.dev/channeˇl/) case.
+            "});
+        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+        cx.assert_editor_text_highlights(
+            HighlightKey::HoveredLinkState,
+            indoc! {"
+            Let's test a [complex](«https://zed.dev/channel/ˇ») case.
+        "},
+        );
+
+        // Cursor hidden with secondary key
+        let screen_coord = cx.pixel_position(indoc! {"
+            Let's test a [complex](https://zed.dev/ˇchannel/) case.
+            "});
+        cx.simulate_mouse_move(screen_coord, None, Modifiers::none());
+        cx.update_editor(|editor, _, cx| {
+            editor.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
+        });
+        cx.simulate_modifiers_change(Modifiers::secondary_key());
+        assert_no_highlight!(cx);
+
+        // Cursor active again
+        let screen_coord = cx.pixel_position(indoc! {"
+            Let's test a [complex](https://ˇzed.dev/channel/) case.
+            "});
+        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+        cx.assert_editor_text_highlights(
+            HighlightKey::HoveredLinkState,
+            indoc! {"
+            Let's test a [complex](«https://zed.dev/channel/ˇ») case.
+        "},
+        );
+    }
+
     #[gpui::test]
     async fn test_urls_at_beginning_of_buffer(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});