From 497b6de85f11a6afeaa3593ce26d22e49e141b91 Mon Sep 17 00:00:00 2001 From: daydalek <90121301+daydalek@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:46:19 +0800 Subject: [PATCH] editor: Add configurable hover delay (#53504) 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> --- 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(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2fd6120ba0d79add35903117e17a43caa02ef619..68a9f2324912db7c1724c81cefd60ecbc41bf4b1 100644 --- a/assets/settings/default.json +++ b/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 diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 67b56a161f4d92985339d725b553c4baeec05bca..548053da7d794de83d99afdfddb098e4cfb2b18e 100644 --- a/crates/editor/src/editor_settings.rs +++ b/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(), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 55350a9c679a10ea8597ae8c923c33af34d71360..730adec4f9b6b13ea14fc00c447b37bf77156b94 100644 --- a/crates/editor/src/hover_popover.rs +++ b/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::(|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::(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::(|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::(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::(|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::(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::(|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::(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::(|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" + ); + }); + } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 6e37c22afe0087c54c5574e17275218d8468ae05..a1b3b8c0ae23f58bdbe915a151e0825ab085a866 100644 --- a/crates/settings/src/settings_store.rs +++ b/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] diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 40565fc4616d3b71c61729743d36b8479c3e590f..c83e56577373aa9834f76b3c32488a069844d249 100644 --- a/crates/settings/src/vscode_import.rs +++ b/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, diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index 00a0549d6b8b1ded71069a5ece36ded5d1a69d0e..60c2686c084ba428992dfc82a9c18b6c24860a66 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -57,6 +57,17 @@ pub struct EditorSettingsContent { /// /// Default: 300 pub hover_popover_delay: Option, + /// 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, + /// 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, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index a14a831452a423baf5f75ec2698ee86c34ae042d..f3f44a754035f1b4531f8a0b987e26981c8963df 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/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, + }), ] } diff --git a/docs/src/migrate/vs-code.md b/docs/src/migrate/vs-code.md index 820158c73ffc1ec2f869ad88e34fea4697e4fbec..b2f3049fce10b0dc0593e9f477c89e674b8f566d 100644 --- a/docs/src/migrate/vs-code.md +++ b/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` | diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index b2b5a76a3a21411b1444268c592e24186ad29797..6eada231df3eafe44b86242aa75ba00a286a7be4 100644 --- a/docs/src/reference/all-settings.md +++ b/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.