Fix diagnostic popups flickering when moving cursor in the boundaries of the symbol (#14870)

Stanislav Alekseev created

This PR just uses ranges returned by an LSP to work, the subsequent PR
would focus on trying to fall back onto tree-sitter in case of info
hovers. I'm also unsure if I'm supposed to use `local_diagnostic` or
`primary_diagnostic` when both are available
Release Notes:

- Fix diagnostic popups flickering when moving cursor in the boundaries
of the symbol

Before:


https://github.com/user-attachments/assets/4905a7e5-c333-453b-b204-264b3ef79586

After:


https://github.com/user-attachments/assets/c742c424-fb20-450d-8848-baaf1937dd47

Change summary

crates/editor/src/hover_popover.rs | 55 ++++++++++++++++++++++---------
1 file changed, 39 insertions(+), 16 deletions(-)

Detailed changes

crates/editor/src/hover_popover.rs 🔗

@@ -213,22 +213,8 @@ fn show_hover(
     };
 
     if !ignore_timeout {
-        if editor
-            .hover_state
-            .info_popovers
-            .iter()
-            .any(|InfoPopover { symbol_range, .. }| {
-                symbol_range
-                    .as_text_range()
-                    .map(|range| {
-                        let hover_range = range.to_offset(&snapshot.buffer_snapshot);
-                        let offset = anchor.to_offset(&snapshot.buffer_snapshot);
-                        // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
-                        // use an inclusive range here to check if we should dismiss the popover
-                        (hover_range.start..=hover_range.end).contains(&offset)
-                    })
-                    .unwrap_or(false)
-            })
+        if same_info_hover(editor, &snapshot, anchor)
+            || same_diagnostic_hover(editor, &snapshot, anchor)
         {
             // Hover triggered from same location as last time. Don't show again.
             return;
@@ -375,6 +361,43 @@ fn show_hover(
     editor.hover_state.info_task = Some(task);
 }
 
+fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
+    editor
+        .hover_state
+        .info_popovers
+        .iter()
+        .any(|InfoPopover { symbol_range, .. }| {
+            symbol_range
+                .as_text_range()
+                .map(|range| {
+                    let hover_range = range.to_offset(&snapshot.buffer_snapshot);
+                    let offset = anchor.to_offset(&snapshot.buffer_snapshot);
+                    // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
+                    // use an inclusive range here to check if we should dismiss the popover
+                    (hover_range.start..=hover_range.end).contains(&offset)
+                })
+                .unwrap_or(false)
+        })
+}
+
+fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
+    editor
+        .hover_state
+        .diagnostic_popover
+        .as_ref()
+        .map(|diagnostic| {
+            let hover_range = diagnostic
+                .local_diagnostic
+                .range
+                .to_offset(&snapshot.buffer_snapshot);
+            let offset = anchor.to_offset(&snapshot.buffer_snapshot);
+
+            // Here we do basically the same as in `same_info_hover`, see comment there for an explanation
+            (hover_range.start..=hover_range.end).contains(&offset)
+        })
+        .unwrap_or(false)
+}
+
 async fn parse_blocks(
     blocks: &[HoverBlock],
     language_registry: &Arc<LanguageRegistry>,