Start on hover popover

Antonio Scandurra created

Change summary

crates/editor2/src/editor.rs        |    2 
crates/editor2/src/element.rs       |  198 +-
crates/editor2/src/hover_popover.rs | 1792 +++++++++++++++---------------
3 files changed, 987 insertions(+), 1,005 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -530,8 +530,6 @@ pub fn init(cx: &mut AppContext) {
     // cx.register_action_type(Editor::context_menu_next);
     // cx.register_action_type(Editor::context_menu_last);
 
-    hover_popover::init(cx);
-
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
     workspace::register_deserializable_item::<Editor>(cx);

crates/editor2/src/element.rs 🔗

@@ -5,7 +5,9 @@ use crate::{
     },
     editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
-    hover_popover::hover_at,
+    hover_popover::{
+        self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+    },
     link_go_to_definition::{
         go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
         update_inlay_link_and_hover_points, GoToDefinitionTrigger,
@@ -257,6 +259,7 @@ impl EditorElement {
         // on_action(cx, Editor::open_excerpts); todo!()
         register_action(view, cx, Editor::toggle_soft_wrap);
         register_action(view, cx, Editor::toggle_inlay_hints);
+        register_action(view, cx, hover_popover::hover);
         register_action(view, cx, Editor::reveal_in_finder);
         register_action(view, cx, Editor::copy_path);
         register_action(view, cx, Editor::copy_relative_path);
@@ -1024,8 +1027,8 @@ impl EditorElement {
                     }
                 });
 
-                if let Some((position, mut context_menu)) = layout.context_menu.take() {
-                    cx.with_z_index(1, |cx| {
+                cx.with_z_index(1, |cx| {
+                    if let Some((position, mut context_menu)) = layout.context_menu.take() {
                         let available_space =
                             size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                         let context_menu_size = context_menu.measure(available_space, cx);
@@ -1053,80 +1056,72 @@ impl EditorElement {
                         }
 
                         context_menu.draw(list_origin, available_space, cx);
-                    })
-                }
+                    }
 
-                // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
-                //     cx.scene().push_stacking_context(None, None);
-
-                //     // This is safe because we check on layout whether the required row is available
-                //     let hovered_row_layout =
-                //         &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
-
-                //     // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
-                //     // height. This is the size we will use to decide whether to render popovers above or below
-                //     // the hovered line.
-                //     let first_size = hover_popovers[0].size();
-                //     let height_to_reserve = first_size.y
-                //         + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
-
-                //     // Compute Hovered Point
-                //     let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
-                //     let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
-                //     let hovered_point = content_origin + point(x, y);
-
-                //     if hovered_point.y - height_to_reserve > 0.0 {
-                //         // There is enough space above. Render popovers above the hovered point
-                //         let mut current_y = hovered_point.y;
-                //         for hover_popover in hover_popovers {
-                //             let size = hover_popover.size();
-                //             let mut popover_origin = point(hovered_point.x, current_y - size.y);
-
-                //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
-                //             if x_out_of_bounds < 0.0 {
-                //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
-                //             }
-
-                //             hover_popover.paint(
-                //                 popover_origin,
-                //                 Bounds::<Pixels>::from_points(
-                //                     gpui::Point::<Pixels>::zero(),
-                //                     point(f32::MAX, f32::MAX),
-                //                 ), // Let content bleed outside of editor
-                //                 editor,
-                //                 cx,
-                //             );
-
-                //             current_y = popover_origin.y - HOVER_POPOVER_GAP;
-                //         }
-                //     } else {
-                //         // There is not enough space above. Render popovers below the hovered point
-                //         let mut current_y = hovered_point.y + layout.position_map.line_height;
-                //         for hover_popover in hover_popovers {
-                //             let size = hover_popover.size();
-                //             let mut popover_origin = point(hovered_point.x, current_y);
-
-                //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
-                //             if x_out_of_bounds < 0.0 {
-                //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
-                //             }
-
-                //             hover_popover.paint(
-                //                 popover_origin,
-                //                 Bounds::<Pixels>::from_points(
-                //                     gpui::Point::<Pixels>::zero(),
-                //                     point(f32::MAX, f32::MAX),
-                //                 ), // Let content bleed outside of editor
-                //                 editor,
-                //                 cx,
-                //             );
-
-                //             current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP;
-                //         }
-                //     }
-
-                //     cx.scene().pop_stacking_context();
-                // }
+                    if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
+                        let available_space =
+                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                        // cx.scene().push_stacking_context(None, None);
+
+                        // This is safe because we check on layout whether the required row is available
+                        let hovered_row_layout = &layout.position_map.line_layouts
+                            [(position.row() - start_row) as usize]
+                            .line;
+
+                        // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+                        // height. This is the size we will use to decide whether to render popovers above or below
+                        // the hovered line.
+                        let first_size = hover_popovers[0].measure(available_space, cx);
+                        let height_to_reserve = first_size.height
+                            + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
+
+                        // Compute Hovered Point
+                        let x = hovered_row_layout.x_for_index(position.column() as usize)
+                            - layout.position_map.scroll_position.x;
+                        let y = position.row() as f32 * layout.position_map.line_height
+                            - layout.position_map.scroll_position.y;
+                        let hovered_point = content_origin + point(x, y);
+
+                        if hovered_point.y - height_to_reserve > Pixels::ZERO {
+                            // There is enough space above. Render popovers above the hovered point
+                            let mut current_y = hovered_point.y;
+                            for mut hover_popover in hover_popovers {
+                                let size = hover_popover.measure(available_space, cx);
+                                let mut popover_origin =
+                                    point(hovered_point.x, current_y - size.height);
+
+                                let x_out_of_bounds =
+                                    text_bounds.upper_right().x - (popover_origin.x + size.width);
+                                if x_out_of_bounds < Pixels::ZERO {
+                                    popover_origin.x = popover_origin.x + x_out_of_bounds;
+                                }
+
+                                hover_popover.draw(popover_origin, available_space, cx);
+
+                                current_y = popover_origin.y - HOVER_POPOVER_GAP;
+                            }
+                        } else {
+                            // There is not enough space above. Render popovers below the hovered point
+                            let mut current_y = hovered_point.y + layout.position_map.line_height;
+                            for mut hover_popover in hover_popovers {
+                                let size = hover_popover.measure(available_space, cx);
+                                let mut popover_origin = point(hovered_point.x, current_y);
+
+                                let x_out_of_bounds =
+                                    text_bounds.upper_right().x - (popover_origin.x + size.width);
+                                if x_out_of_bounds < Pixels::ZERO {
+                                    popover_origin.x = popover_origin.x + x_out_of_bounds;
+                                }
+
+                                hover_popover.draw(popover_origin, available_space, cx);
+
+                                current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
+                            }
+                        }
+
+                        // cx.scene().pop_stacking_context();
+                    }
+                })
             },
         )
     }
@@ -1992,15 +1987,23 @@ impl EditorElement {
             }
 
             let visible_rows = start_row..start_row + line_layouts.len() as u32;
-            // todo!("hover")
-            // let mut hover = editor.hover_state.render(
-            //     &snapshot,
-            //     &style,
-            //     visible_rows,
-            //     editor.workspace.as_ref().map(|(w, _)| w.clone()),
-            //     cx,
-            // );
-            // let mode = editor.mode;
+            let max_size = size(
+                (120. * em_width) // Default size
+                    .min(bounds.size.width / 2.) // Shrink to half of the editor width
+                    .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
+                (16. * line_height) // Default size
+                    .min(bounds.size.height / 2.) // Shrink to half of the editor height
+                    .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
+            );
+
+            let mut hover = editor.hover_state.render(
+                &snapshot,
+                &style,
+                visible_rows,
+                max_size,
+                editor.workspace.as_ref().map(|(w, _)| w.clone()),
+                cx,
+            );
 
             let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
                 editor.render_fold_indicators(
@@ -2013,27 +2016,6 @@ impl EditorElement {
                 )
             });
 
-            // todo!("hover popovers")
-            // if let Some((_, hover_popovers)) = hover.as_mut() {
-            //     for hover_popover in hover_popovers.iter_mut() {
-            //         hover_popover.layout(
-            //             SizeConstraint {
-            //                 min: gpui::Point::<Pixels>::zero(),
-            //                 max: point(
-            //                     (120. * em_width) // Default size
-            //                         .min(size.x / 2.) // Shrink to half of the editor width
-            //                         .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
-            //                     (16. * line_height) // Default size
-            //                         .min(size.y / 2.) // Shrink to half of the editor height
-            //                         .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
-            //                 ),
-            //             },
-            //             editor,
-            //             cx,
-            //         );
-            //     }
-            // }
-
             let invisible_symbol_font_size = font_size / 2.;
             let tab_invisible = cx
                 .text_system()
@@ -2102,7 +2084,7 @@ impl EditorElement {
                 fold_indicators,
                 tab_invisible,
                 space_invisible,
-                // hover_popovers: hover,
+                hover_popovers: hover,
             }
         })
     }
@@ -3287,7 +3269,7 @@ pub struct LayoutState {
     max_row: u32,
     context_menu: Option<(DisplayPoint, AnyElement)>,
     code_actions_indicator: Option<CodeActionsIndicator>,
-    // hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
+    hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
     fold_indicators: Vec<Option<IconButton>>,
     tab_invisible: ShapedLine,
     space_invisible: ShapedLine,

crates/editor2/src/hover_popover.rs 🔗

@@ -1,11 +1,14 @@
 use crate::{
-    display_map::InlayOffset,
+    display_map::{InlayOffset, ToDisplayPoint},
     link_go_to_definition::{InlayHighlight, RangeInEditor},
     Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
     ExcerptId, RangeToAnchorExt,
 };
 use futures::FutureExt;
-use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView};
+use gpui::{
+    actions, div, px, AnyElement, AppContext, InteractiveElement, IntoElement, Model, MouseButton,
+    ParentElement, Pixels, Size, StatefulInteractiveElement, Styled, Task, ViewContext, WeakView,
+};
 use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
 use settings::Settings;
@@ -17,22 +20,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
 
 pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
-pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
-pub const HOVER_POPOVER_GAP: f32 = 10.;
+pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
+pub const HOVER_POPOVER_GAP: Pixels = px(10.);
 
-// actions!(editor, [Hover]);
+actions!(Hover);
 
-pub fn init(cx: &mut AppContext) {
-    // cx.add_action(hover);
+/// Bindable action which uses the most recent selection head to trigger a hover
+pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
+    let head = editor.selections.newest_display(cx).head();
+    show_hover(editor, head, true, cx);
 }
 
-// todo!()
-// /// Bindable action which uses the most recent selection head to trigger a hover
-// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
-//     let head = editor.selections.newest_display(cx).head();
-//     show_hover(editor, head, true, cx);
-// }
-
 /// The internal hover action dispatches between `show_hover` or `hide_hover`
 /// depending on whether a point to hover over is provided.
 pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
@@ -74,64 +72,63 @@ pub fn find_hovered_hint_part(
 }
 
 pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
-    todo!()
-    // if EditorSettings::get_global(cx).hover_popover_enabled {
-    //     if editor.pending_rename.is_some() {
-    //         return;
-    //     }
-
-    //     let Some(project) = editor.project.clone() else {
-    //         return;
-    //     };
-
-    //     if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
-    //         if let RangeInEditor::Inlay(range) = symbol_range {
-    //             if range == &inlay_hover.range {
-    //                 // Hover triggered from same location as last time. Don't show again.
-    //                 return;
-    //             }
-    //         }
-    //         hide_hover(editor, cx);
-    //     }
-
-    //     let task = cx.spawn(|this, mut cx| {
-    //         async move {
-    //             cx.background_executor()
-    //                 .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
-    //                 .await;
-    //             this.update(&mut cx, |this, _| {
-    //                 this.hover_state.diagnostic_popover = None;
-    //             })?;
-
-    //             let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
-    //             let blocks = vec![inlay_hover.tooltip];
-    //             let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
-
-    //             let hover_popover = InfoPopover {
-    //                 project: project.clone(),
-    //                 symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
-    //                 blocks,
-    //                 parsed_content,
-    //             };
-
-    //             this.update(&mut cx, |this, cx| {
-    //                 // Highlight the selected symbol using a background highlight
-    //                 this.highlight_inlay_background::<HoverState>(
-    //                     vec![inlay_hover.range],
-    //                     |theme| theme.editor.hover_popover.highlight,
-    //                     cx,
-    //                 );
-    //                 this.hover_state.info_popover = Some(hover_popover);
-    //                 cx.notify();
-    //             })?;
-
-    //             anyhow::Ok(())
-    //         }
-    //         .log_err()
-    //     });
-
-    //     editor.hover_state.info_task = Some(task);
-    // }
+    if EditorSettings::get_global(cx).hover_popover_enabled {
+        if editor.pending_rename.is_some() {
+            return;
+        }
+
+        let Some(project) = editor.project.clone() else {
+            return;
+        };
+
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            if let RangeInEditor::Inlay(range) = symbol_range {
+                if range == &inlay_hover.range {
+                    // Hover triggered from same location as last time. Don't show again.
+                    return;
+                }
+            }
+            hide_hover(editor, cx);
+        }
+
+        let task = cx.spawn(|this, mut cx| {
+            async move {
+                cx.background_executor()
+                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
+                    .await;
+                this.update(&mut cx, |this, _| {
+                    this.hover_state.diagnostic_popover = None;
+                })?;
+
+                let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
+                let blocks = vec![inlay_hover.tooltip];
+                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
+
+                let hover_popover = InfoPopover {
+                    project: project.clone(),
+                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
+                    blocks,
+                    parsed_content,
+                };
+
+                this.update(&mut cx, |this, cx| {
+                    // Highlight the selected symbol using a background highlight
+                    this.highlight_inlay_background::<HoverState>(
+                        vec![inlay_hover.range],
+                        |theme| gpui::red(), // todo!("use a proper background here")
+                        cx,
+                    );
+                    this.hover_state.info_popover = Some(hover_popover);
+                    cx.notify();
+                })?;
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        });
+
+        editor.hover_state.info_task = Some(task);
+    }
 }
 
 /// Hides the type information popup.
@@ -420,43 +417,42 @@ impl HoverState {
         snapshot: &EditorSnapshot,
         style: &EditorStyle,
         visible_rows: Range<u32>,
+        max_size: Size<Pixels>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
-        todo!("old version below")
+        // If there is a diagnostic, position the popovers based on that.
+        // Otherwise use the start of the hover range
+        let anchor = self
+            .diagnostic_popover
+            .as_ref()
+            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
+            .or_else(|| {
+                self.info_popover
+                    .as_ref()
+                    .map(|info_popover| match &info_popover.symbol_range {
+                        RangeInEditor::Text(range) => &range.start,
+                        RangeInEditor::Inlay(range) => &range.inlay_position,
+                    })
+            })?;
+        let point = anchor.to_display_point(&snapshot.display_snapshot);
+
+        // Don't render if the relevant point isn't on screen
+        if !self.visible() || !visible_rows.contains(&point.row()) {
+            return None;
+        }
+
+        let mut elements = Vec::new();
+
+        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
+            elements.push(diagnostic_popover.render(style, max_size, cx));
+        }
+        if let Some(info_popover) = self.info_popover.as_mut() {
+            elements.push(info_popover.render(style, max_size, workspace, cx));
+        }
+
+        Some((point, elements))
     }
-    //     // If there is a diagnostic, position the popovers based on that.
-    //     // Otherwise use the start of the hover range
-    //     let anchor = self
-    //         .diagnostic_popover
-    //         .as_ref()
-    //         .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
-    //         .or_else(|| {
-    //             self.info_popover
-    //                 .as_ref()
-    //                 .map(|info_popover| match &info_popover.symbol_range {
-    //                     RangeInEditor::Text(range) => &range.start,
-    //                     RangeInEditor::Inlay(range) => &range.inlay_position,
-    //                 })
-    //         })?;
-    //     let point = anchor.to_display_point(&snapshot.display_snapshot);
-
-    //     // Don't render if the relevant point isn't on screen
-    //     if !self.visible() || !visible_rows.contains(&point.row()) {
-    //         return None;
-    //     }
-
-    //     let mut elements = Vec::new();
-
-    //     if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
-    //         elements.push(diagnostic_popover.render(style, cx));
-    //     }
-    //     if let Some(info_popover) = self.info_popover.as_mut() {
-    //         elements.push(info_popover.render(style, workspace, cx));
-    //     }
-
-    //     Some((point, elements))
-    // }
 }
 
 #[derive(Debug, Clone)]
@@ -467,35 +463,36 @@ pub struct InfoPopover {
     parsed_content: ParsedMarkdown,
 }
 
-// impl InfoPopover {
-//     pub fn render(
-//         &mut self,
-//         style: &EditorStyle,
-//         workspace: Option<WeakView<Workspace>>,
-//         cx: &mut ViewContext<Editor>,
-//     ) -> AnyElement<Editor> {
-//         MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
-//             Flex::column()
-//                 .scrollable::<HoverBlock>(0, None, cx)
-//                 .with_child(crate::render_parsed_markdown::<HoverBlock>(
-//                     &self.parsed_content,
-//                     style,
-//                     workspace,
-//                     cx,
-//                 ))
-//                 .contained()
-//                 .with_style(style.hover_popover.container)
-//         })
-//         .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
-//         .with_cursor_style(CursorStyle::Arrow)
-//         .with_padding(Padding {
-//             bottom: HOVER_POPOVER_GAP,
-//             top: HOVER_POPOVER_GAP,
-//             ..Default::default()
-//         })
-//         .into_any()
-//     }
-// }
+impl InfoPopover {
+    pub fn render(
+        &mut self,
+        style: &EditorStyle,
+        max_size: Size<Pixels>,
+        workspace: Option<WeakView<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement {
+        div()
+            .id("info_popover")
+            .overflow_y_scroll()
+            .bg(gpui::red())
+            .max_w(max_size.width)
+            .max_h(max_size.height)
+            // Prevent a mouse move on the popover from being propagated to the editor,
+            // because that would dismiss the popover.
+            .on_mouse_move(|_, cx| cx.stop_propagation())
+            // Prevent a mouse down on the popover from being propagated to the editor,
+            // because that would move the cursor.
+            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+            .child(crate::render_parsed_markdown(
+                "content",
+                &self.parsed_content,
+                style,
+                workspace,
+                cx,
+            ))
+            .into_any_element()
+    }
+}
 
 #[derive(Debug, Clone)]
 pub struct DiagnosticPopover {
@@ -504,7 +501,12 @@ pub struct DiagnosticPopover {
 }
 
 impl DiagnosticPopover {
-    pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement {
+    pub fn render(
+        &self,
+        style: &EditorStyle,
+        max_size: Size<Pixels>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement {
         todo!()
         // enum PrimaryDiagnostic {}
 
@@ -567,763 +569,763 @@ impl DiagnosticPopover {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         editor_tests::init_test,
-//         element::PointForPosition,
-//         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
-//         link_go_to_definition::update_inlay_link_and_hover_points,
-//         test::editor_lsp_test_context::EditorLspTestContext,
-//         InlayId,
-//     };
-//     use collections::BTreeSet;
-//     use gpui::fonts::{HighlightStyle, Underline, Weight};
-//     use indoc::indoc;
-//     use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
-//     use lsp::LanguageServerId;
-//     use project::{HoverBlock, HoverBlockKind};
-//     use smol::stream::StreamExt;
-//     use unindent::Unindent;
-//     use util::test::marked_text_ranges;
-
-//     #[gpui::test]
-//     async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Basic hover delays and then pops without moving the mouse
-//         cx.set_state(indoc! {"
-//             fn ˇtest() { println!(); }
-//         "});
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { printˇln!(); }
-//         "});
-
-//         cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
-//         assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
-
-//         // After delay, hover should be visible.
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             fn test() { «println!»(); }
-//         "});
-//         let mut requests =
-//             cx.handle_request::<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.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         requests.next().await;
-
-//         cx.editor(|editor, _| {
-//             assert!(editor.hover_state.visible());
-//             assert_eq!(
-//                 editor.hover_state.info_popover.clone().unwrap().blocks,
-//                 vec![HoverBlock {
-//                     text: "some basic docs".to_string(),
-//                     kind: HoverBlockKind::Markdown,
-//                 },]
-//             )
-//         });
-
-//         // Mouse moved with no hover response dismisses
-//         let hover_point = cx.display_point(indoc! {"
-//             fn teˇst() { println!(); }
-//         "});
-//         let mut request = cx
-//             .lsp
-//             .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
-//         cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         request.next().await;
-//         cx.editor(|editor, _| {
-//             assert!(!editor.hover_state.visible());
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with keyboard has no delay
-//         cx.set_state(indoc! {"
-//             fˇn test() { println!(); }
-//         "});
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             «fn» test() { println!(); }
-//         "});
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//             Ok(Some(lsp::Hover {
-//                 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                     kind: lsp::MarkupKind::Markdown,
-//                     value: "some other basic docs".to_string(),
-//                 }),
-//                 range: Some(symbol_range),
-//             }))
-//         })
-//         .next()
-//         .await;
-
-//         cx.condition(|editor, _| editor.hover_state.visible()).await;
-//         cx.editor(|editor, _| {
-//             assert_eq!(
-//                 editor.hover_state.info_popover.clone().unwrap().blocks,
-//                 vec![HoverBlock {
-//                     text: "some other basic docs".to_string(),
-//                     kind: HoverBlockKind::Markdown,
-//                 }]
-//             )
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with keyboard has no delay
-//         cx.set_state(indoc! {"
-//             fˇn test() { println!(); }
-//         "});
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             «fn» test() { println!(); }
-//         "});
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//             Ok(Some(lsp::Hover {
-//                 contents: lsp::HoverContents::Array(vec![
-//                     lsp::MarkedString::String("regular text for hover to show".to_string()),
-//                     lsp::MarkedString::String("".to_string()),
-//                     lsp::MarkedString::LanguageString(lsp::LanguageString {
-//                         language: "Rust".to_string(),
-//                         value: "".to_string(),
-//                     }),
-//                 ]),
-//                 range: Some(symbol_range),
-//             }))
-//         })
-//         .next()
-//         .await;
-
-//         cx.condition(|editor, _| editor.hover_state.visible()).await;
-//         cx.editor(|editor, _| {
-//             assert_eq!(
-//                 editor.hover_state.info_popover.clone().unwrap().blocks,
-//                 vec![HoverBlock {
-//                     text: "regular text for hover to show".to_string(),
-//                     kind: HoverBlockKind::Markdown,
-//                 }],
-//                 "No empty string hovers should be shown"
-//             );
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with keyboard has no delay
-//         cx.set_state(indoc! {"
-//             fˇn test() { println!(); }
-//         "});
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             «fn» test() { println!(); }
-//         "});
-
-//         let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
-//         let markdown_string = format!("\n```rust\n{code_str}```");
-
-//         let closure_markdown_string = markdown_string.clone();
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
-//             let future_markdown_string = closure_markdown_string.clone();
-//             async move {
-//                 Ok(Some(lsp::Hover {
-//                     contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                         kind: lsp::MarkupKind::Markdown,
-//                         value: future_markdown_string,
-//                     }),
-//                     range: Some(symbol_range),
-//                 }))
-//             }
-//         })
-//         .next()
-//         .await;
-
-//         cx.condition(|editor, _| editor.hover_state.visible()).await;
-//         cx.editor(|editor, _| {
-//             let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
-//             assert_eq!(
-//                 blocks,
-//                 vec![HoverBlock {
-//                     text: markdown_string,
-//                     kind: HoverBlockKind::Markdown,
-//                 }],
-//             );
-
-//             let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
-//             assert_eq!(
-//                 rendered.text,
-//                 code_str.trim(),
-//                 "Should not have extra line breaks at end of rendered hover"
-//             );
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with just diagnostic, pops DiagnosticPopover immediately and then
-//         // info popover once request completes
-//         cx.set_state(indoc! {"
-//             fn teˇst() { println!(); }
-//         "});
-
-//         // Send diagnostic to client
-//         let range = cx.text_anchor_range(indoc! {"
-//             fn «test»() { println!(); }
-//         "});
-//         cx.update_buffer(|buffer, cx| {
-//             let snapshot = buffer.text_snapshot();
-//             let set = DiagnosticSet::from_sorted_entries(
-//                 vec![DiagnosticEntry {
-//                     range,
-//                     diagnostic: Diagnostic {
-//                         message: "A test diagnostic message.".to_string(),
-//                         ..Default::default()
-//                     },
-//                 }],
-//                 &snapshot,
-//             );
-//             buffer.update_diagnostics(LanguageServerId(0), set, cx);
-//         });
-
-//         // Hover pops diagnostic immediately
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         cx.foreground().run_until_parked();
-
-//         cx.editor(|Editor { hover_state, .. }, _| {
-//             assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
-//         });
-
-//         // Info Popover shows after request responded to
-//         let range = cx.lsp_range(indoc! {"
-//             fn «test»() { println!(); }
-//         "});
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//             Ok(Some(lsp::Hover {
-//                 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                     kind: lsp::MarkupKind::Markdown,
-//                     value: "some new docs".to_string(),
-//                 }),
-//                 range: Some(range),
-//             }))
-//         });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-
-//         cx.foreground().run_until_parked();
-//         cx.editor(|Editor { hover_state, .. }, _| {
-//             hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
-//         });
-//     }
-
-//     #[gpui::test]
-//     fn test_render_blocks(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         cx.add_window(|cx| {
-//             let editor = Editor::single_line(None, cx);
-//             let style = editor.style(cx);
-
-//             struct Row {
-//                 blocks: Vec<HoverBlock>,
-//                 expected_marked_text: String,
-//                 expected_styles: Vec<HighlightStyle>,
-//             }
-
-//             let rows = &[
-//                 // Strong emphasis
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "one **two** three".to_string(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "one «two» three".to_string(),
-//                     expected_styles: vec![HighlightStyle {
-//                         weight: Some(Weight::BOLD),
-//                         ..Default::default()
-//                     }],
-//                 },
-//                 // Links
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "one [two](https://the-url) three".to_string(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "one «two» three".to_string(),
-//                     expected_styles: vec![HighlightStyle {
-//                         underline: Some(Underline {
-//                             thickness: 1.0.into(),
-//                             ..Default::default()
-//                         }),
-//                         ..Default::default()
-//                     }],
-//                 },
-//                 // Lists
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "
-//                             lists:
-//                             * one
-//                                 - a
-//                                 - b
-//                             * two
-//                                 - [c](https://the-url)
-//                                 - d"
-//                         .unindent(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "
-//                         lists:
-//                         - one
-//                           - a
-//                           - b
-//                         - two
-//                           - «c»
-//                           - d"
-//                     .unindent(),
-//                     expected_styles: vec![HighlightStyle {
-//                         underline: Some(Underline {
-//                             thickness: 1.0.into(),
-//                             ..Default::default()
-//                         }),
-//                         ..Default::default()
-//                     }],
-//                 },
-//                 // Multi-paragraph list items
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "
-//                             * one two
-//                               three
-
-//                             * four five
-//                                 * six seven
-//                                   eight
-
-//                                   nine
-//                                 * ten
-//                             * six"
-//                             .unindent(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "
-//                         - one two three
-//                         - four five
-//                           - six seven eight
-
-//                             nine
-//                           - ten
-//                         - six"
-//                         .unindent(),
-//                     expected_styles: vec![HighlightStyle {
-//                         underline: Some(Underline {
-//                             thickness: 1.0.into(),
-//                             ..Default::default()
-//                         }),
-//                         ..Default::default()
-//                     }],
-//                 },
-//             ];
-
-//             for Row {
-//                 blocks,
-//                 expected_marked_text,
-//                 expected_styles,
-//             } in &rows[0..]
-//             {
-//                 let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
-
-//                 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
-//                 let expected_highlights = ranges
-//                     .into_iter()
-//                     .zip(expected_styles.iter().cloned())
-//                     .collect::<Vec<_>>();
-//                 assert_eq!(
-//                     rendered.text, expected_text,
-//                     "wrong text for input {blocks:?}"
-//                 );
-
-//                 let rendered_highlights: Vec<_> = rendered
-//                     .highlights
-//                     .iter()
-//                     .filter_map(|(range, highlight)| {
-//                         let highlight = highlight.to_highlight_style(&style.syntax)?;
-//                         Some((range.clone(), highlight))
-//                     })
-//                     .collect();
-
-//                 assert_eq!(
-//                     rendered_highlights, expected_highlights,
-//                     "wrong highlights for input {blocks:?}"
-//                 );
-//             }
-
-//             editor
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |settings| {
-//             settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                 enabled: true,
-//                 show_type_hints: true,
-//                 show_parameter_hints: true,
-//                 show_other_hints: true,
-//             })
-//         });
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 inlay_hint_provider: Some(lsp::OneOf::Right(
-//                     lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
-//                         resolve_provider: Some(true),
-//                         ..Default::default()
-//                     }),
-//                 )),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         cx.set_state(indoc! {"
-//             struct TestStruct;
-
-//             // ==================
-
-//             struct TestNewType<T>(T);
-
-//             fn main() {
-//                 let variableˇ = TestNewType(TestStruct);
-//             }
-//         "});
-
-//         let hint_start_offset = cx.ranges(indoc! {"
-//             struct TestStruct;
-
-//             // ==================
-
-//             struct TestNewType<T>(T);
-
-//             fn main() {
-//                 let variableˇ = TestNewType(TestStruct);
-//             }
-//         "})[0]
-//             .start;
-//         let hint_position = cx.to_lsp(hint_start_offset);
-//         let new_type_target_range = cx.lsp_range(indoc! {"
-//             struct TestStruct;
-
-//             // ==================
-
-//             struct «TestNewType»<T>(T);
-
-//             fn main() {
-//                 let variable = TestNewType(TestStruct);
-//             }
-//         "});
-//         let struct_target_range = cx.lsp_range(indoc! {"
-//             struct «TestStruct»;
-
-//             // ==================
-
-//             struct TestNewType<T>(T);
-
-//             fn main() {
-//                 let variable = TestNewType(TestStruct);
-//             }
-//         "});
-
-//         let uri = cx.buffer_lsp_url.clone();
-//         let new_type_label = "TestNewType";
-//         let struct_label = "TestStruct";
-//         let entire_hint_label = ": TestNewType<TestStruct>";
-//         let closure_uri = uri.clone();
-//         cx.lsp
-//             .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-//                 let task_uri = closure_uri.clone();
-//                 async move {
-//                     assert_eq!(params.text_document.uri, task_uri);
-//                     Ok(Some(vec![lsp::InlayHint {
-//                         position: hint_position,
-//                         label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
-//                             value: entire_hint_label.to_string(),
-//                             ..Default::default()
-//                         }]),
-//                         kind: Some(lsp::InlayHintKind::TYPE),
-//                         text_edits: None,
-//                         tooltip: None,
-//                         padding_left: Some(false),
-//                         padding_right: Some(false),
-//                         data: None,
-//                     }]))
-//                 }
-//             })
-//             .next()
-//             .await;
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             let expected_layers = vec![entire_hint_label.to_string()];
-//             assert_eq!(expected_layers, cached_hint_labels(editor));
-//             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-//         });
-
-//         let inlay_range = cx
-//             .ranges(indoc! {"
-//                 struct TestStruct;
-
-//                 // ==================
-
-//                 struct TestNewType<T>(T);
-
-//                 fn main() {
-//                     let variable« »= TestNewType(TestStruct);
-//                 }
-//         "})
-//             .get(0)
-//             .cloned()
-//             .unwrap();
-//         let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let previous_valid = inlay_range.start.to_display_point(&snapshot);
-//             let next_valid = inlay_range.end.to_display_point(&snapshot);
-//             assert_eq!(previous_valid.row(), next_valid.row());
-//             assert!(previous_valid.column() < next_valid.column());
-//             let exact_unclipped = DisplayPoint::new(
-//                 previous_valid.row(),
-//                 previous_valid.column()
-//                     + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
-//                         as u32,
-//             );
-//             PointForPosition {
-//                 previous_valid,
-//                 next_valid,
-//                 exact_unclipped,
-//                 column_overshoot_after_line_end: 0,
-//             }
-//         });
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 new_type_hint_part_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-
-//         let resolve_closure_uri = uri.clone();
-//         cx.lsp
-//             .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
-//                 move |mut hint_to_resolve, _| {
-//                     let mut resolved_hint_positions = BTreeSet::new();
-//                     let task_uri = resolve_closure_uri.clone();
-//                     async move {
-//                         let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
-//                         assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
-
-//                         // `: TestNewType<TestStruct>`
-//                         hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
-//                             lsp::InlayHintLabelPart {
-//                                 value: ": ".to_string(),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: new_type_label.to_string(),
-//                                 location: Some(lsp::Location {
-//                                     uri: task_uri.clone(),
-//                                     range: new_type_target_range,
-//                                 }),
-//                                 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
-//                                     "A tooltip for `{new_type_label}`"
-//                                 ))),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: "<".to_string(),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: struct_label.to_string(),
-//                                 location: Some(lsp::Location {
-//                                     uri: task_uri,
-//                                     range: struct_target_range,
-//                                 }),
-//                                 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
-//                                     lsp::MarkupContent {
-//                                         kind: lsp::MarkupKind::Markdown,
-//                                         value: format!("A tooltip for `{struct_label}`"),
-//                                     },
-//                                 )),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: ">".to_string(),
-//                                 ..Default::default()
-//                             },
-//                         ]);
-
-//                         Ok(hint_to_resolve)
-//                     }
-//                 },
-//             )
-//             .next()
-//             .await;
-//         cx.foreground().run_until_parked();
-
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 new_type_hint_part_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             let hover_state = &editor.hover_state;
-//             assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
-//             let popover = hover_state.info_popover.as_ref().unwrap();
-//             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-//             assert_eq!(
-//                 popover.symbol_range,
-//                 RangeInEditor::Inlay(InlayHighlight {
-//                     inlay: InlayId::Hint(0),
-//                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-//                     range: ": ".len()..": ".len() + new_type_label.len(),
-//                 }),
-//                 "Popover range should match the new type label part"
-//             );
-//             assert_eq!(
-//                 popover.parsed_content.text,
-//                 format!("A tooltip for `{new_type_label}`"),
-//                 "Rendered text should not anyhow alter backticks"
-//             );
-//         });
-
-//         let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let previous_valid = inlay_range.start.to_display_point(&snapshot);
-//             let next_valid = inlay_range.end.to_display_point(&snapshot);
-//             assert_eq!(previous_valid.row(), next_valid.row());
-//             assert!(previous_valid.column() < next_valid.column());
-//             let exact_unclipped = DisplayPoint::new(
-//                 previous_valid.row(),
-//                 previous_valid.column()
-//                     + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
-//                         as u32,
-//             );
-//             PointForPosition {
-//                 previous_valid,
-//                 next_valid,
-//                 exact_unclipped,
-//                 column_overshoot_after_line_end: 0,
-//             }
-//         });
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 struct_hint_part_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             let hover_state = &editor.hover_state;
-//             assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
-//             let popover = hover_state.info_popover.as_ref().unwrap();
-//             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-//             assert_eq!(
-//                 popover.symbol_range,
-//                 RangeInEditor::Inlay(InlayHighlight {
-//                     inlay: InlayId::Hint(0),
-//                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-//                     range: ": ".len() + new_type_label.len() + "<".len()
-//                         ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
-//                 }),
-//                 "Popover range should match the struct label part"
-//             );
-//             assert_eq!(
-//                 popover.parsed_content.text,
-//                 format!("A tooltip for {struct_label}"),
-//                 "Rendered markdown element should remove backticks from text"
-//             );
-//         });
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        editor_tests::init_test,
+        element::PointForPosition,
+        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        link_go_to_definition::update_inlay_link_and_hover_points,
+        test::editor_lsp_test_context::EditorLspTestContext,
+        InlayId,
+    };
+    use collections::BTreeSet;
+    use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
+    use indoc::indoc;
+    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
+    use lsp::LanguageServerId;
+    use project::{HoverBlock, HoverBlockKind};
+    use smol::stream::StreamExt;
+    use unindent::Unindent;
+    use util::test::marked_text_ranges;
+
+    #[gpui::test]
+    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Basic hover delays and then pops without moving the mouse
+        cx.set_state(indoc! {"
+            fn ˇtest() { println!(); }
+        "});
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { printˇln!(); }
+        "});
+
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
+
+        // After delay, hover should be visible.
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { «println!»(); }
+        "});
+        let mut requests =
+            cx.handle_request::<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.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        requests.next().await;
+
+        cx.editor(|editor, _| {
+            assert!(editor.hover_state.visible());
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "some basic docs".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                },]
+            )
+        });
+
+        // Mouse moved with no hover response dismisses
+        let hover_point = cx.display_point(indoc! {"
+            fn teˇst() { println!(); }
+        "});
+        let mut request = cx
+            .lsp
+            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        request.next().await;
+        cx.editor(|editor, _| {
+            assert!(!editor.hover_state.visible());
+        });
+    }
+
+    #[gpui::test]
+    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            «fn» test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: "some other basic docs".to_string(),
+                }),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "some other basic docs".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }]
+            )
+        });
+    }
+
+    #[gpui::test]
+    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            «fn» test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Array(vec![
+                    lsp::MarkedString::String("regular text for hover to show".to_string()),
+                    lsp::MarkedString::String("".to_string()),
+                    lsp::MarkedString::LanguageString(lsp::LanguageString {
+                        language: "Rust".to_string(),
+                        value: "".to_string(),
+                    }),
+                ]),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "regular text for hover to show".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }],
+                "No empty string hovers should be shown"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            «fn» test() { println!(); }
+        "});
+
+        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
+        let markdown_string = format!("\n```rust\n{code_str}```");
+
+        let closure_markdown_string = markdown_string.clone();
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
+            let future_markdown_string = closure_markdown_string.clone();
+            async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: future_markdown_string,
+                    }),
+                    range: Some(symbol_range),
+                }))
+            }
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
+            assert_eq!(
+                blocks,
+                vec![HoverBlock {
+                    text: markdown_string,
+                    kind: HoverBlockKind::Markdown,
+                }],
+            );
+
+            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+            assert_eq!(
+                rendered.text,
+                code_str.trim(),
+                "Should not have extra line breaks at end of rendered hover"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
+        // info popover once request completes
+        cx.set_state(indoc! {"
+            fn teˇst() { println!(); }
+        "});
+
+        // Send diagnostic to client
+        let range = cx.text_anchor_range(indoc! {"
+            fn «test»() { println!(); }
+        "});
+        cx.update_buffer(|buffer, cx| {
+            let snapshot = buffer.text_snapshot();
+            let set = DiagnosticSet::from_sorted_entries(
+                vec![DiagnosticEntry {
+                    range,
+                    diagnostic: Diagnostic {
+                        message: "A test diagnostic message.".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                &snapshot,
+            );
+            buffer.update_diagnostics(LanguageServerId(0), set, cx);
+        });
+
+        // Hover pops diagnostic immediately
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        cx.background_executor.run_until_parked();
+
+        cx.editor(|Editor { hover_state, .. }, _| {
+            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
+        });
+
+        // Info Popover shows after request responded to
+        let range = cx.lsp_range(indoc! {"
+            fn «test»() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: "some new docs".to_string(),
+                }),
+                range: Some(range),
+            }))
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+
+        cx.background_executor.run_until_parked();
+        cx.editor(|Editor { hover_state, .. }, _| {
+            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
+        });
+    }
+
+    #[gpui::test]
+    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        cx.add_window(|cx| {
+            let editor = Editor::single_line(cx);
+            let style = editor.style.clone().unwrap();
+
+            struct Row {
+                blocks: Vec<HoverBlock>,
+                expected_marked_text: String,
+                expected_styles: Vec<HighlightStyle>,
+            }
+
+            let rows = &[
+                // Strong emphasis
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "one **two** three".to_string(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "one «two» three".to_string(),
+                    expected_styles: vec![HighlightStyle {
+                        font_weight: Some(FontWeight::BOLD),
+                        ..Default::default()
+                    }],
+                },
+                // Links
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "one [two](https://the-url) three".to_string(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "one «two» three".to_string(),
+                    expected_styles: vec![HighlightStyle {
+                        underline: Some(UnderlineStyle {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    }],
+                },
+                // Lists
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "
+                            lists:
+                            * one
+                                - a
+                                - b
+                            * two
+                                - [c](https://the-url)
+                                - d"
+                        .unindent(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "
+                        lists:
+                        - one
+                          - a
+                          - b
+                        - two
+                          - «c»
+                          - d"
+                    .unindent(),
+                    expected_styles: vec![HighlightStyle {
+                        underline: Some(UnderlineStyle {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    }],
+                },
+                // Multi-paragraph list items
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "
+                            * one two
+                              three
+
+                            * four five
+                                * six seven
+                                  eight
+
+                                  nine
+                                * ten
+                            * six"
+                            .unindent(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "
+                        - one two three
+                        - four five
+                          - six seven eight
+
+                            nine
+                          - ten
+                        - six"
+                        .unindent(),
+                    expected_styles: vec![HighlightStyle {
+                        underline: Some(UnderlineStyle {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    }],
+                },
+            ];
+
+            for Row {
+                blocks,
+                expected_marked_text,
+                expected_styles,
+            } in &rows[0..]
+            {
+                let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+
+                let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
+                let expected_highlights = ranges
+                    .into_iter()
+                    .zip(expected_styles.iter().cloned())
+                    .collect::<Vec<_>>();
+                assert_eq!(
+                    rendered.text, expected_text,
+                    "wrong text for input {blocks:?}"
+                );
+
+                let rendered_highlights: Vec<_> = rendered
+                    .highlights
+                    .iter()
+                    .filter_map(|(range, highlight)| {
+                        let highlight = highlight.to_highlight_style(&style.syntax)?;
+                        Some((range.clone(), highlight))
+                    })
+                    .collect();
+
+                assert_eq!(
+                    rendered_highlights, expected_highlights,
+                    "wrong highlights for input {blocks:?}"
+                );
+            }
+
+            editor
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Right(
+                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
+                        resolve_provider: Some(true),
+                        ..Default::default()
+                    }),
+                )),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableˇ = TestNewType(TestStruct);
+            }
+        "});
+
+        let hint_start_offset = cx.ranges(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableˇ = TestNewType(TestStruct);
+            }
+        "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let new_type_target_range = cx.lsp_range(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct «TestNewType»<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+        let struct_target_range = cx.lsp_range(indoc! {"
+            struct «TestStruct»;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+
+        let uri = cx.buffer_lsp_url.clone();
+        let new_type_label = "TestNewType";
+        let struct_label = "TestStruct";
+        let entire_hint_label = ": TestNewType<TestStruct>";
+        let closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_uri = closure_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, task_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: entire_hint_label.to_string(),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![entire_hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+                struct TestStruct;
+
+                // ==================
+
+                struct TestNewType<T>(T);
+
+                fn main() {
+                    let variable« »= TestNewType(TestStruct);
+                }
+        "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+
+        let resolve_closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
+                move |mut hint_to_resolve, _| {
+                    let mut resolved_hint_positions = BTreeSet::new();
+                    let task_uri = resolve_closure_uri.clone();
+                    async move {
+                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
+                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
+
+                        // `: TestNewType<TestStruct>`
+                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
+                            lsp::InlayHintLabelPart {
+                                value: ": ".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: new_type_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri.clone(),
+                                    range: new_type_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
+                                    "A tooltip for `{new_type_label}`"
+                                ))),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: "<".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: struct_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri,
+                                    range: struct_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                    lsp::MarkupContent {
+                                        kind: lsp::MarkupKind::Markdown,
+                                        value: format!("A tooltip for `{struct_label}`"),
+                                    },
+                                )),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: ">".to_string(),
+                                ..Default::default()
+                            },
+                        ]);
+
+                        Ok(hint_to_resolve)
+                    }
+                },
+            )
+            .next()
+            .await;
+        cx.background_executor.run_until_parked();
+
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let hover_state = &editor.hover_state;
+            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+            let popover = hover_state.info_popover.as_ref().unwrap();
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            assert_eq!(
+                popover.symbol_range,
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    range: ": ".len()..": ".len() + new_type_label.len(),
+                }),
+                "Popover range should match the new type label part"
+            );
+            assert_eq!(
+                popover.parsed_content.text,
+                format!("A tooltip for `{new_type_label}`"),
+                "Rendered text should not anyhow alter backticks"
+            );
+        });
+
+        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                struct_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let hover_state = &editor.hover_state;
+            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+            let popover = hover_state.info_popover.as_ref().unwrap();
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            assert_eq!(
+                popover.symbol_range,
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    range: ": ".len() + new_type_label.len() + "<".len()
+                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
+                }),
+                "Popover range should match the struct label part"
+            );
+            assert_eq!(
+                popover.parsed_content.text,
+                format!("A tooltip for {struct_label}"),
+                "Rendered markdown element should remove backticks from text"
+            );
+        });
+    }
+}