editor: Add configurable hover delay (#53504)

daydalek and Copilot created

follow up #47471 

As described in #47471, we introduced a direction-aware strategy to
improve user experience when interacting with hover popovers. In this
follow-up, we are adding `hover_popover_sticky` and
`hover_popover_hiding_delay` to control whether the feature introduced
in 47471 enabled, and to let users configure the delay to balance
responsiveness . Also `hover_popover_sticky` can now be imported from
`editor.hover.sticky`, as well as `hover_popover_hiding_delay` from
`editor.hover.hidingDelay` in VSCode.


Also this PR adds several tests:
- `test_hover_popover_cancel_hide_on_rehover`: when the cursor returns
to the hover after leaving once within the hiding delay, the hover
should persist while canceling the existing hiding timer.
- `test_hover_popover_enabled_false_ignores_sticky` : when
`hover_popover_enabled` is false, the `hover_popover_sticky` and
`hover_popover_hiding_delay` have no effect(since no hover is shown).
- `test_hover_popover_sticky_delay_restarts_when_mouse_gets_closer`:
when mouse gets closer to hover popover, we expect the timer to reset
and the hover remains visible.
- `test_hover_popover_hiding_delay`: check if the delay(in test, that's
500ms) works.
- `test_hover_popover_sticky_disabled`: when hover_popover_sticky is
false, the hover popover disappears immediately after the cursor leaving
the codes.
- VSCode import test in `settings_store.rs`

Release Notes:

- Added `hover_popover_sticky` and `hover_popover_hiding_delay` settings
to balance responsiveness of hover popovers.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Change summary

assets/settings/default.json          |   7 
crates/editor/src/editor_settings.rs  |   4 
crates/editor/src/hover_popover.rs    | 447 ++++++++++++++++++++++++++++
crates/settings/src/settings_store.rs |  22 +
crates/settings/src/vscode_import.rs  |   2 
crates/settings_content/src/editor.rs |  11 
crates/settings_ui/src/page_data.rs   |  31 +
docs/src/migrate/vs-code.md           |   2 
docs/src/reference/all-settings.md    |  20 +
9 files changed, 543 insertions(+), 3 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -123,6 +123,13 @@
   // Time to wait in milliseconds before showing the informational hover box.
   // This delay also applies to auto signature help when `auto_signature_help` is enabled.
   "hover_popover_delay": 300,
+  // Whether the hover popover sticks when the mouse moves toward it,
+  // allowing interaction with its contents before it disappears.
+  "hover_popover_sticky": true,
+  // Time to wait in milliseconds before hiding the hover popover
+  // after the mouse moves away from the hover target.
+  // Only applies when `hover_popover_sticky` is enabled.
+  "hover_popover_hiding_delay": 300,
   // Whether to confirm before quitting Zed.
   "confirm_quit": false,
   // Whether to restore last closed project when fresh Zed instance is opened

crates/editor/src/editor_settings.rs 🔗

@@ -24,6 +24,8 @@ pub struct EditorSettings {
     pub lsp_highlight_debounce: DelayMs,
     pub hover_popover_enabled: bool,
     pub hover_popover_delay: DelayMs,
+    pub hover_popover_sticky: bool,
+    pub hover_popover_hiding_delay: DelayMs,
     pub toolbar: Toolbar,
     pub scrollbar: Scrollbar,
     pub minimap: Minimap,
@@ -205,6 +207,8 @@ impl Settings for EditorSettings {
             lsp_highlight_debounce: editor.lsp_highlight_debounce.unwrap(),
             hover_popover_enabled: editor.hover_popover_enabled.unwrap(),
             hover_popover_delay: editor.hover_popover_delay.unwrap(),
+            hover_popover_sticky: editor.hover_popover_sticky.unwrap(),
+            hover_popover_hiding_delay: editor.hover_popover_hiding_delay.unwrap(),
             toolbar: Toolbar {
                 breadcrumbs: toolbar.breadcrumbs.unwrap(),
                 quick_actions: toolbar.quick_actions.unwrap(),

crates/editor/src/hover_popover.rs 🔗

@@ -63,6 +63,12 @@ pub fn hover_at(
             editor.hover_state.closest_mouse_distance = None;
             show_hover(editor, anchor, false, window, cx);
         } else {
+            let settings = EditorSettings::get_global(cx);
+            if !settings.hover_popover_sticky {
+                hide_hover(editor, cx);
+                return;
+            }
+
             let mut getting_closer = false;
             if let Some(mouse_position) = mouse_position {
                 getting_closer = editor.hover_state.is_mouse_getting_closer(mouse_position);
@@ -73,8 +79,8 @@ pub fn hover_at(
                 return;
             }
 
-            // If we are moving closer, or if no timer is running at all, start/restart the 300ms timer.
-            let delay = Duration::from_millis(300u64);
+            // If we are moving closer, or if no timer is running at all, start/restart the timer.
+            let delay = Duration::from_millis(settings.hover_popover_hiding_delay.0);
             let task = cx.spawn(async move |this, cx| {
                 cx.background_executor().timer(delay).await;
                 this.update(cx, |editor, cx| {
@@ -1201,6 +1207,7 @@ mod tests {
     use markdown::parser::MarkdownEvent;
     use project::InlayId;
     use settings::InlayHintSettingsContent;
+    use settings::{DelayMs, SettingsStore};
     use smol::stream::StreamExt;
     use std::sync::atomic;
     use std::sync::atomic::AtomicUsize;
@@ -2149,4 +2156,440 @@ mod tests {
             InlayOffset(MultiBufferOffset(104))..InlayOffset(MultiBufferOffset(108))
         );
     }
+
+    #[gpui::test]
+    async fn test_hover_popover_hiding_delay(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let custom_delay_ms = 500u64;
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|settings, cx| {
+                settings.update_user_settings(cx, |settings| {
+                    settings.editor.hover_popover_sticky = Some(true);
+                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
+                });
+            });
+        });
+
+        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!(); }
+        "});
+
+        // Trigger hover on a symbol
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { printˇln!(); }
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { «println!»(); }
+        "});
+        let mut requests =
+            cx.set_request_handler::<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.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)
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
+        requests.next().await;
+
+        // Hover should be visible
+        cx.editor(|editor, _, _| {
+            assert!(editor.hover_state.visible());
+        });
+
+        // Move mouse away (hover_at with None anchor triggers the hiding delay)
+        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
+
+        // Popover should still be visible before the custom hiding delay expires
+        cx.background_executor
+            .advance_clock(Duration::from_millis(custom_delay_ms - 100));
+        cx.editor(|editor, _, _| {
+            assert!(
+                editor.hover_state.visible(),
+                "Popover should remain visible before the hiding delay expires"
+            );
+        });
+
+        // After the full custom delay, the popover should be hidden
+        cx.background_executor
+            .advance_clock(Duration::from_millis(200));
+        cx.editor(|editor, _, _| {
+            assert!(
+                !editor.hover_state.visible(),
+                "Popover should be hidden after the hiding delay expires"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_popover_sticky_disabled(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|settings, cx| {
+                settings.update_user_settings(cx, |settings| {
+                    settings.editor.hover_popover_sticky = Some(false);
+                });
+            });
+        });
+
+        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!(); }
+        "});
+
+        // Trigger hover on a symbol
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { printˇln!(); }
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { «println!»(); }
+        "});
+        let mut requests =
+            cx.set_request_handler::<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.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)
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
+        requests.next().await;
+
+        // Hover should be visible
+        cx.editor(|editor, _, _| {
+            assert!(editor.hover_state.visible());
+        });
+
+        // Move mouse away — with sticky disabled, hide immediately
+        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
+
+        // Popover should be hidden immediately without any delay
+        cx.editor(|editor, _, _| {
+            assert!(
+                !editor.hover_state.visible(),
+                "Popover should be hidden immediately when sticky is disabled"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_popover_hiding_delay_restarts_when_mouse_gets_closer(
+        cx: &mut gpui::TestAppContext,
+    ) {
+        init_test(cx, |_| {});
+
+        let custom_delay_ms = 600u64;
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|settings, cx| {
+                settings.update_user_settings(cx, |settings| {
+                    settings.editor.hover_popover_sticky = Some(true);
+                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
+                });
+            });
+        });
+
+        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!(); }
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { «println!»(); }
+        "});
+        let mut requests =
+            cx.set_request_handler::<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.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)
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
+        requests.next().await;
+
+        cx.editor(|editor, _, _| {
+            assert!(editor.hover_state.visible());
+        });
+
+        cx.update_editor(|editor, _, _| {
+            let popover = editor.hover_state.info_popovers.first().unwrap();
+            popover.last_bounds.set(Some(Bounds {
+                origin: gpui::Point {
+                    x: px(100.0),
+                    y: px(100.0),
+                },
+                size: Size {
+                    width: px(100.0),
+                    height: px(60.0),
+                },
+            }));
+        });
+
+        let far_point = gpui::Point {
+            x: px(260.0),
+            y: px(130.0),
+        };
+        cx.update_editor(|editor, window, cx| hover_at(editor, None, Some(far_point), window, cx));
+
+        cx.background_executor
+            .advance_clock(Duration::from_millis(400));
+        cx.background_executor.run_until_parked();
+
+        let closer_point = gpui::Point {
+            x: px(220.0),
+            y: px(130.0),
+        };
+        cx.update_editor(|editor, window, cx| {
+            hover_at(editor, None, Some(closer_point), window, cx)
+        });
+
+        cx.background_executor
+            .advance_clock(Duration::from_millis(250));
+        cx.background_executor.run_until_parked();
+
+        cx.editor(|editor, _, _| {
+            assert!(
+                editor.hover_state.visible(),
+                "Popover should remain visible because moving closer restarts the hiding timer"
+            );
+        });
+
+        cx.background_executor
+            .advance_clock(Duration::from_millis(350));
+        cx.background_executor.run_until_parked();
+
+        cx.editor(|editor, _, _| {
+            assert!(
+                !editor.hover_state.visible(),
+                "Popover should hide after the restarted hiding timer expires"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_popover_cancel_hide_on_rehover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let custom_delay_ms = 500u64;
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|settings, cx| {
+                settings.update_user_settings(cx, |settings| {
+                    settings.editor.hover_popover_sticky = Some(true);
+                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
+                });
+            });
+        });
+
+        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!(); }
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { «println!»(); }
+        "});
+        let mut requests =
+            cx.set_request_handler::<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.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)
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
+        requests.next().await;
+
+        cx.editor(|editor, _, _| {
+            assert!(editor.hover_state.visible());
+        });
+
+        // Move mouse away — starts the 500ms hide timer
+        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
+
+        cx.background_executor
+            .advance_clock(Duration::from_millis(300));
+        cx.background_executor.run_until_parked();
+        cx.editor(|editor, _, _| {
+            assert!(
+                editor.hover_state.visible(),
+                "Popover should still be visible before hiding delay expires"
+            );
+        });
+
+        // Move back to the symbol — should cancel the hiding timer
+        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)
+        });
+
+        // Advance past the original deadline — popover should still be visible
+        // because re-hovering cleared the hiding_delay_task
+        cx.background_executor
+            .advance_clock(Duration::from_millis(300));
+        cx.background_executor.run_until_parked();
+        cx.editor(|editor, _, _| {
+            assert!(
+                editor.hover_state.visible(),
+                "Popover should remain visible after re-hovering the symbol"
+            );
+            assert!(
+                editor.hover_state.hiding_delay_task.is_none(),
+                "Hiding delay task should have been cleared by re-hover"
+            );
+        });
+
+        // Move away again — starts a fresh 500ms timer
+        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
+
+        cx.background_executor
+            .advance_clock(Duration::from_millis(custom_delay_ms + 100));
+        cx.background_executor.run_until_parked();
+        cx.editor(|editor, _, _| {
+            assert!(
+                !editor.hover_state.visible(),
+                "Popover should hide after the new hiding timer expires"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_popover_enabled_false_ignores_sticky(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|settings, cx| {
+                settings.update_user_settings(cx, |settings| {
+                    settings.editor.hover_popover_enabled = Some(false);
+                    settings.editor.hover_popover_sticky = Some(true);
+                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(500));
+                });
+            });
+        });
+
+        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!(); }
+        "});
+
+        // Trigger hover_at — should be gated by hover_popover_enabled=false
+        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)
+        });
+
+        // No need to advance clock or wait for LSP — the gate should prevent any work
+        cx.editor(|editor, _, _| {
+            assert!(
+                !editor.hover_state.visible(),
+                "Popover should not appear when hover_popover_enabled is false"
+            );
+            assert!(
+                editor.hover_state.info_task.is_none(),
+                "No hover info task should be scheduled when hover is disabled"
+            );
+            assert!(
+                editor.hover_state.triggered_from.is_none(),
+                "No hover trigger should be recorded when hover is disabled"
+            );
+        });
+    }
 }

crates/settings/src/settings_store.rs 🔗

@@ -2071,6 +2071,28 @@ mod tests {
             .unindent(),
             cx,
         );
+
+        // hover sticky settings
+        check_vscode_import(
+            &mut store,
+            r#"{
+            }
+            "#
+            .unindent(),
+            r#"{
+              "editor.hover.sticky": false,
+              "editor.hover.hidingDelay": 500
+            }"#
+            .to_owned(),
+            r#"{
+              "base_keymap": "VSCode",
+              "hover_popover_hiding_delay": 500,
+              "hover_popover_sticky": false
+            }
+            "#
+            .unindent(),
+            cx,
+        );
     }
 
     #[track_caller]

crates/settings/src/vscode_import.rs 🔗

@@ -267,6 +267,8 @@ impl VsCodeSettings {
             horizontal_scroll_margin: None,
             hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
             hover_popover_enabled: self.read_bool("editor.hover.enabled"),
+            hover_popover_sticky: self.read_bool("editor.hover.sticky"),
+            hover_popover_hiding_delay: self.read_u64("editor.hover.hidingDelay").map(Into::into),
             inline_code_actions: None,
             jupyter: None,
             lsp_document_colors: None,

crates/settings_content/src/editor.rs 🔗

@@ -57,6 +57,17 @@ pub struct EditorSettingsContent {
     ///
     /// Default: 300
     pub hover_popover_delay: Option<DelayMs>,
+    /// Whether the hover popover sticks when the mouse moves toward it,
+    /// allowing interaction with its contents before it disappears.
+    ///
+    /// Default: true
+    pub hover_popover_sticky: Option<bool>,
+    /// Time to wait in milliseconds before hiding the hover popover
+    /// after the mouse moves away from the hover target.
+    /// Only applies when `hover_popover_sticky` is enabled.
+    ///
+    /// Default: 300
+    pub hover_popover_hiding_delay: Option<DelayMs>,
     /// Toolbar related settings
     pub toolbar: Option<ToolbarContent>,
     /// Scrollbar related settings

crates/settings_ui/src/page_data.rs 🔗

@@ -1770,7 +1770,7 @@ fn editor_page() -> SettingsPage {
         ]
     }
 
-    fn hover_popover_section() -> [SettingsPageItem; 3] {
+    fn hover_popover_section() -> [SettingsPageItem; 5] {
         [
             SettingsPageItem::SectionHeader("Hover Popover"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -1800,6 +1800,35 @@ fn editor_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Sticky",
+                description: "Whether the hover popover sticks when the mouse moves toward it, allowing interaction with its contents.",
+                field: Box::new(SettingField {
+                    json_path: Some("hover_popover_sticky"),
+                    pick: |settings_content| settings_content.editor.hover_popover_sticky.as_ref(),
+                    write: |settings_content, value| {
+                        settings_content.editor.hover_popover_sticky = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
+            // todo(settings ui): add units to this number input
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Hiding Delay",
+                description: "Time to wait in milliseconds before hiding the hover popover after the mouse moves away.",
+                field: Box::new(SettingField {
+                    json_path: Some("hover_popover_hiding_delay"),
+                    pick: |settings_content| {
+                        settings_content.editor.hover_popover_hiding_delay.as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content.editor.hover_popover_hiding_delay = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
         ]
     }
 

docs/src/migrate/vs-code.md 🔗

@@ -59,6 +59,8 @@ The following VS Code settings are automatically imported when you use **Import
 | `editor.cursorSurroundingLines`             | `vertical_scroll_margin`                       |
 | `editor.hover.enabled`                      | `hover_popover_enabled`                        |
 | `editor.hover.delay`                        | `hover_popover_delay`                          |
+| `editor.hover.sticky`                       | `hover_popover_sticky`                         |
+| `editor.hover.hidingDelay`                  | `hover_popover_hiding_delay`                   |
 | `editor.parameterHints.enabled`             | `auto_signature_help`                          |
 | `editor.multiCursorModifier`                | `multi_cursor_modifier`                        |
 | `editor.selectionHighlight`                 | `selection_highlight`                          |

docs/src/reference/all-settings.md 🔗

@@ -2465,6 +2465,26 @@ Example:
 
 `integer` values representing milliseconds
 
+## Hover Popover Sticky
+
+- Description: Whether the hover popover sticks when the mouse moves toward it, allowing interaction with its contents before it disappears.
+- Setting: `hover_popover_sticky`
+- Default: `true`
+
+**Options**
+
+`boolean` values
+
+## Hover Popover Hiding Delay
+
+- Description: Time to wait in milliseconds before hiding the hover popover after the mouse moves away from the hover target. Only applies when `hover_popover_sticky` is enabled.
+- Setting: `hover_popover_hiding_delay`
+- Default: `300`
+
+**Options**
+
+`integer` values representing milliseconds
+
 ## Icon Theme
 
 - Description: The icon theme setting can be specified in two forms - either as the name of an icon theme or as an object containing the `mode`, `dark`, and `light` icon themes for files/folders inside Zed.