Use info popovers instead of diagnostic for invisible char hover (#22701)

Michael Sloan created

This will allow the diagnostic popover to be displayed even if hovering
an invisible char.

Beyond that, it solves a rare `DiagnosticPopover` corner case:

* Supports moving the selection to a diagnostic when `GoToDiagnostic` is
done while hovering, based on `group_id`

* Provides Diagnostic values with `group_id: 0` providing information on
hover about invisible characters.

So, `GoToDiagnostic` would navigate to the very first error produced by
a language server. Really not a big deal of course.

Release Notes:

- N/A

Change summary

crates/editor/src/hover_popover.rs | 127 ++++++++++++++++---------------
1 file changed, 64 insertions(+), 63 deletions(-)

Detailed changes

crates/editor/src/hover_popover.rs 🔗

@@ -11,11 +11,11 @@ use gpui::{
     StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
 };
 use itertools::Itertools;
-use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};
+use language::{DiagnosticEntry, Language, LanguageRegistry};
 use lsp::DiagnosticSeverity;
 use markdown::{Markdown, MarkdownStyle};
 use multi_buffer::ToOffset;
-use project::{HoverBlock, InlayHintLabelPart};
+use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
 use settings::Settings;
 use std::rc::Rc;
 use std::{borrow::Cow, cell::RefCell};
@@ -263,50 +263,14 @@ fn show_hover(
                 delay.await;
             }
 
-            let local_diagnostic = if let Some(invisible) = snapshot
+            let local_diagnostic = snapshot
                 .buffer_snapshot
-                .chars_at(anchor)
-                .next()
-                .filter(|&c| is_invisible(c))
-            {
-                let after = snapshot.buffer_snapshot.anchor_after(
-                    anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
-                );
-                Some(DiagnosticEntry {
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: format!("Unicode character U+{:02X}", invisible as u32),
-                        ..Default::default()
-                    },
-                    range: anchor..after,
-                })
-            } else if let Some(invisible) = snapshot
-                .buffer_snapshot
-                .reversed_chars_at(anchor)
-                .next()
-                .filter(|&c| is_invisible(c))
-            {
-                let before = snapshot.buffer_snapshot.anchor_before(
-                    anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
-                );
-                Some(DiagnosticEntry {
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: format!("Unicode character U+{:02X}", invisible as u32),
-                        ..Default::default()
-                    },
-                    range: before..anchor,
-                })
-            } else {
-                snapshot
-                    .buffer_snapshot
-                    .diagnostics_in_range(anchor..anchor, false)
-                    // Find the entry with the most specific range
-                    .min_by_key(|entry| {
-                        let range = entry.range.to_offset(&snapshot.buffer_snapshot);
-                        range.end - range.start
-                    })
-            };
+                .diagnostics_in_range(anchor..anchor, false)
+                // Find the entry with the most specific range
+                .min_by_key(|entry| {
+                    let range = entry.range.to_offset(&snapshot.buffer_snapshot);
+                    range.end - range.start
+                });
 
             let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
                 let text = match local_diagnostic.diagnostic.source {
@@ -389,6 +353,31 @@ fn show_hover(
                 this.hover_state.diagnostic_popover = diagnostic_popover;
             })?;
 
+            let invisible_char = if let Some(invisible) = snapshot
+                .buffer_snapshot
+                .chars_at(anchor)
+                .next()
+                .filter(|&c| is_invisible(c))
+            {
+                let after = snapshot.buffer_snapshot.anchor_after(
+                    anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
+                );
+                Some((invisible, anchor..after))
+            } else if let Some(invisible) = snapshot
+                .buffer_snapshot
+                .reversed_chars_at(anchor)
+                .next()
+                .filter(|&c| is_invisible(c))
+            {
+                let before = snapshot.buffer_snapshot.anchor_before(
+                    anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
+                );
+
+                Some((invisible, before..anchor))
+            } else {
+                None
+            };
+
             let hovers_response = if let Some(hover_request) = hover_request {
                 hover_request.await
             } else {
@@ -396,8 +385,26 @@ fn show_hover(
             };
             let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
             let mut hover_highlights = Vec::with_capacity(hovers_response.len());
-            let mut info_popovers = Vec::with_capacity(hovers_response.len());
-            let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
+            let mut info_popovers = Vec::with_capacity(
+                hovers_response.len() + if invisible_char.is_some() { 1 } else { 0 },
+            );
+
+            if let Some((invisible, range)) = invisible_char {
+                let blocks = vec![HoverBlock {
+                    text: format!("Unicode character U+{:02X}", invisible as u32),
+                    kind: HoverBlockKind::PlainText,
+                }];
+                let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
+                let scroll_handle = ScrollHandle::new();
+                info_popovers.push(InfoPopover {
+                    symbol_range: RangeInEditor::Text(range),
+                    parsed_content,
+                    scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+                    scroll_handle,
+                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
+                    anchor: Some(anchor),
+                })
+            }
 
             for hover_result in hovers_response {
                 // Create symbol range of anchors for highlighting and filtering of future requests.
@@ -410,7 +417,6 @@ fn show_hover(
                         let end = snapshot
                             .buffer_snapshot
                             .anchor_in_excerpt(excerpt_id, range.end)?;
-
                         Some(start..end)
                     })
                     .or_else(|| {
@@ -428,21 +434,15 @@ fn show_hover(
                 let parsed_content =
                     parse_blocks(&blocks, &language_registry, language, &mut cx).await;
                 let scroll_handle = ScrollHandle::new();
-                info_popover_tasks.push((
-                    range.clone(),
-                    InfoPopover {
-                        symbol_range: RangeInEditor::Text(range),
-                        parsed_content,
-                        scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
-                        scroll_handle,
-                        keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
-                        anchor: Some(anchor),
-                    },
-                ));
-            }
-            for (highlight_range, info_popover) in info_popover_tasks {
-                hover_highlights.push(highlight_range);
-                info_popovers.push(info_popover);
+                hover_highlights.push(range.clone());
+                info_popovers.push(InfoPopover {
+                    symbol_range: RangeInEditor::Text(range),
+                    parsed_content,
+                    scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+                    scroll_handle,
+                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
+                    anchor: Some(anchor),
+                });
             }
 
             this.update(&mut cx, |editor, cx| {
@@ -732,6 +732,7 @@ impl InfoPopover {
         cx.notify();
         self.scroll_handle.set_offset(current);
     }
+
     fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Editor>) -> Stateful<Div> {
         div()
             .occlude()