Merge branch 'main' into completions-styles

Conrad Irwin created

Change summary

crates/auto_update2/src/update_notification.rs |    9 
crates/command_palette2/src/command_palette.rs |    9 
crates/editor2/src/editor.rs                   |    2 
crates/editor2/src/element.rs                  |  201 -
crates/editor2/src/hover_popover.rs            | 1873 +++++++++----------
crates/file_finder2/src/file_finder.rs         |   10 
crates/go_to_line2/src/go_to_line.rs           |   13 
crates/gpui2/src/app/async_context.rs          |    8 
crates/gpui2/src/app/test_context.rs           |    2 
crates/gpui2/src/elements/overlay.rs           |    8 
crates/gpui2/src/window.rs                     |   28 
crates/ui2/src/components/context_menu.rs      |   16 
crates/workspace/src/workspace.rs              |   20 
crates/workspace2/src/notifications.rs         |  133 
crates/workspace2/src/workspace2.rs            |   71 
15 files changed, 1,233 insertions(+), 1,170 deletions(-)

Detailed changes

crates/auto_update2/src/update_notification.rs 🔗

@@ -1,12 +1,13 @@
-use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
+use gpui::{
+    div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext,
+};
 use menu::Cancel;
-use workspace::notifications::NotificationEvent;
 
 pub struct UpdateNotification {
     _version: SemanticVersion,
 }
 
-impl EventEmitter<NotificationEvent> for UpdateNotification {}
+impl EventEmitter<DismissEvent> for UpdateNotification {}
 
 impl Render for UpdateNotification {
     type Element = Div;
@@ -82,6 +83,6 @@ impl UpdateNotification {
     }
 
     pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(NotificationEvent::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 }

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,8 +1,9 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
-    Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+    actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext,
+    WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::{
@@ -68,7 +69,7 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<Manager> for CommandPalette {}
+impl EventEmitter<DismissEvent> for CommandPalette {}
 
 impl FocusableView for CommandPalette {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
             .log_err();
     }
 

crates/editor2/src/editor.rs 🔗

@@ -531,8 +531,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);
@@ -1052,81 +1055,74 @@ impl EditorElement {
                             list_origin.y -= layout.position_map.line_height + list_height;
                         }
 
-                        context_menu.draw(list_origin, available_space, cx);
-                    })
-                }
+                        cx.break_content_mask(|cx| {
+                            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);
+
+                        // 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;
+                                }
+
+                                cx.break_content_mask(|cx| {
+                                    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;
+                            }
+                        }
+                    }
+                })
             },
         )
     }
@@ -1992,15 +1988,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 +2017,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 +2085,7 @@ impl EditorElement {
                 fold_indicators,
                 tab_invisible,
                 space_invisible,
-                // hover_popovers: hover,
+                hover_popovers: hover,
             }
         })
     }
@@ -3287,7 +3270,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,15 +1,21 @@
 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, CursorStyle, InteractiveElement, IntoElement, Model,
+    MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,
+    Task, ViewContext, WeakView,
+};
 use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
+use lsp::DiagnosticSeverity;
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
 use settings::Settings;
 use std::{ops::Range, sync::Arc, time::Duration};
+use ui::Tooltip;
 use util::TryFutureExt;
 use workspace::Workspace;
 
@@ -17,22 +23,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 +75,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| theme.element_hover, // 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 +420,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 +466,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,57 +504,40 @@ pub struct DiagnosticPopover {
 }
 
 impl DiagnosticPopover {
-    pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement {
-        todo!()
-        // enum PrimaryDiagnostic {}
-
-        // let mut text_style = style.hover_popover.prose.clone();
-        // text_style.font_size = style.text.font_size;
-        // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
-
-        // let text = match &self.local_diagnostic.diagnostic.source {
-        //     Some(source) => Text::new(
-        //         format!("{source}: {}", self.local_diagnostic.diagnostic.message),
-        //         text_style,
-        //     )
-        //     .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
-
-        //     None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
-        // };
-
-        // let container_style = match self.local_diagnostic.diagnostic.severity {
-        //     DiagnosticSeverity::HINT => style.hover_popover.info_container,
-        //     DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
-        //     DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
-        //     DiagnosticSeverity::ERROR => style.hover_popover.error_container,
-        //     _ => style.hover_popover.container,
-        // };
-
-        // let tooltip_style = theme::current(cx).tooltip.clone();
-
-        // MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
-        //     text.with_soft_wrap(true)
-        //         .contained()
-        //         .with_style(container_style)
-        // })
-        // .with_padding(Padding {
-        //     top: HOVER_POPOVER_GAP,
-        //     bottom: HOVER_POPOVER_GAP,
-        //     ..Default::default()
-        // })
-        // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
-        // .on_click(MouseButton::Left, |_, this, cx| {
-        //     this.go_to_diagnostic(&Default::default(), cx)
-        // })
-        // .with_cursor_style(CursorStyle::PointingHand)
-        // .with_tooltip::<PrimaryDiagnostic>(
-        //     0,
-        //     "Go To Diagnostic".to_string(),
-        //     Some(Box::new(crate::GoToDiagnostic)),
-        //     tooltip_style,
-        //     cx,
-        // )
-        // .into_any()
+    pub fn render(
+        &self,
+        style: &EditorStyle,
+        max_size: Size<Pixels>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement {
+        let text = match &self.local_diagnostic.diagnostic.source {
+            Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
+            None => self.local_diagnostic.diagnostic.message.clone(),
+        };
+
+        let container_bg = crate::diagnostic_style(
+            self.local_diagnostic.diagnostic.severity,
+            true,
+            &style.diagnostic_style,
+        );
+
+        div()
+            .id("diagnostic")
+            .overflow_y_scroll()
+            .bg(container_bg)
+            .max_w(max_size.width)
+            .max_h(max_size.height)
+            .cursor(CursorStyle::PointingHand)
+            .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
+            // 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())
+            .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
+            .child(SharedString::from(text))
+            .into_any_element()
     }
 
     pub fn activation_info(&self) -> (usize, Anchor) {
@@ -567,763 +550,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, |_| {});
+
+        let editor = cx.add_window(|cx| Editor::single_line(cx));
+        editor
+            .update(cx, |editor, 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:?}"
+                    );
+                }
+            })
+            .unwrap();
+    }
+
+    #[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"
+            );
+        });
+    }
+}

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,8 +2,8 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    IntoElement, Manager, Model, ParentElement, Render, Styled, Task, View, ViewContext,
+    actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext,
     VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
@@ -111,7 +111,7 @@ impl FileFinder {
     }
 }
 
-impl EventEmitter<Manager> for FileFinder {}
+impl EventEmitter<DismissEvent> for FileFinder {}
 impl FocusableView for FileFinder {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
         self.picker.focus_handle(cx)
@@ -690,7 +690,7 @@ impl PickerDelegate for FileFinderDelegate {
                         }
                     }
                     finder
-                        .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
+                        .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
                         .ok()?;
 
                     Some(())
@@ -702,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
             .log_err();
     }
 

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,7 +1,8 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
-    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+    WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
@@ -28,7 +29,7 @@ impl FocusableView for GoToLine {
         self.active_editor.focus_handle(cx)
     }
 }
-impl EventEmitter<Manager> for GoToLine {}
+impl EventEmitter<DismissEvent> for GoToLine {}
 
 impl GoToLine {
     fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
@@ -88,7 +89,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
+            editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss),
             editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +124,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +141,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 }
 

crates/gpui2/src/app/async_context.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
-    ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
-    VisualContext, WindowContext, WindowHandle,
+    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
+    FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle,
 };
 use anyhow::{anyhow, Context as _};
 use derive_more::{Deref, DerefMut};
@@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext {
         V: crate::ManagedView,
     {
         self.window.update(self, |_, cx| {
-            view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
         })
     }
 }

crates/gpui2/src/app/test_context.rs 🔗

@@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
     {
         self.window
             .update(self.cx, |_, cx| {
-                view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
+                view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
             })
             .unwrap()
     }

crates/gpui2/src/elements/overlay.rs 🔗

@@ -145,9 +145,11 @@ impl Element for Overlay {
         }
 
         cx.with_element_offset(desired.origin - bounds.origin, |cx| {
-            for child in self.children {
-                child.paint(cx);
-            }
+            cx.break_content_mask(|cx| {
+                for child in self.children {
+                    child.paint(cx);
+                }
+            })
         })
     }
 }

crates/gpui2/src/window.rs 🔗

@@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render {
 
 /// ManagedView is a view (like a Modal, Popover, Menu, etc.)
 /// where the lifecycle of the view is handled by another view.
-pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
+pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
 
-impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
+impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
 
-pub enum Manager {
+pub enum DismissEvent {
     Dismiss,
 }
 
@@ -1663,7 +1663,7 @@ impl VisualContext for WindowContext<'_> {
     where
         V: ManagedView,
     {
-        self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
+        self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
     }
 }
 
@@ -1752,6 +1752,24 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         }
     }
 
+    /// Invoke the given function with the content mask reset to that
+    /// of the window.
+    fn break_content_mask<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
+        let mask = ContentMask {
+            bounds: Bounds {
+                origin: Point::default(),
+                size: self.window().viewport_size,
+            },
+        };
+        self.window_mut()
+            .current_frame
+            .content_mask_stack
+            .push(mask);
+        let result = f(self);
+        self.window_mut().current_frame.content_mask_stack.pop();
+        result
+    }
+
     /// Update the global element offset relative to the current offset. This is used to implement
     /// scrolling.
     fn with_element_offset<R>(
@@ -2349,7 +2367,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     where
         V: ManagedView,
     {
-        self.defer(|_, cx| cx.emit(Manager::Dismiss))
+        self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
     }
 
     pub fn listener<E>(

crates/ui2/src/components/context_menu.rs 🔗

@@ -4,9 +4,9 @@ use std::rc::Rc;
 use crate::{prelude::*, v_stack, Label, List};
 use crate::{ListItem, ListSeparator, ListSubHeader};
 use gpui::{
-    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase,
-    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, Manager,
-    MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
+    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
+    DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
+    ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
 };
 
 pub enum ContextMenuItem {
@@ -26,7 +26,7 @@ impl FocusableView for ContextMenu {
     }
 }
 
-impl EventEmitter<Manager> for ContextMenu {}
+impl EventEmitter<DismissEvent> for ContextMenu {}
 
 impl ContextMenu {
     pub fn build(
@@ -74,11 +74,11 @@ impl ContextMenu {
 
     pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
         // todo!()
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 
     pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 }
 
@@ -111,7 +111,7 @@ impl Render for ContextMenu {
                         }
                         ContextMenuItem::Entry(entry, callback) => {
                             let callback = callback.clone();
-                            let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss));
+                            let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
 
                             ListItem::new(entry.clone())
                                 .child(Label::new(entry.clone()))
@@ -265,7 +265,7 @@ impl<M: ManagedView> Element for MenuHandle<M> {
                 let new_menu = (builder)(cx);
                 let menu2 = menu.clone();
                 cx.subscribe(&new_menu, move |modal, e, cx| match e {
-                    &Manager::Dismiss => {
+                    &DismissEvent::Dismiss => {
                         *menu2.borrow_mut() = None;
                         cx.notify();
                     }

crates/workspace/src/workspace.rs 🔗

@@ -63,7 +63,7 @@ use crate::{
 };
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
 use lazy_static::lazy_static;
-use notifications::{NotificationHandle, NotifyResultExt};
+use notifications::{simple_message_notification, NotificationHandle, NotifyResultExt};
 pub use pane::*;
 pub use pane_group::*;
 use persistence::{model::SerializedItem, DB};
@@ -776,7 +776,23 @@ impl Workspace {
             }),
         ];
 
-        cx.defer(|this, cx| this.update_window_title(cx));
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+
+            this.show_notification(0, cx, |cx| {
+                cx.add_view(|_cx| {
+                    simple_message_notification::MessageNotification::new(format!(
+                        "Error: what happens if this message is very very very very very long "
+                    ))
+                    .with_click_message("Click here because!")
+                })
+            });
+            this.show_notification(1, cx, |cx| {
+                cx.add_view(|_cx| {
+                    simple_message_notification::MessageNotification::new(format!("Nope"))
+                })
+            });
+        });
         Workspace {
             weak_self: weak_handle.clone(),
             modal: None,

crates/workspace2/src/notifications.rs 🔗

@@ -1,6 +1,9 @@
 use crate::{Toast, Workspace};
 use collections::HashMap;
-use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext};
+use gpui::{
+    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
+    View, ViewContext, VisualContext,
+};
 use std::{any::TypeId, ops::DerefMut};
 
 pub fn init(cx: &mut AppContext) {
@@ -9,13 +12,9 @@ pub fn init(cx: &mut AppContext) {
     // simple_message_notification::init(cx);
 }
 
-pub enum NotificationEvent {
-    Dismiss,
-}
-
-pub trait Notification: EventEmitter<NotificationEvent> + Render {}
+pub trait Notification: EventEmitter<DismissEvent> + Render {}
 
-impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
+impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
 
 pub trait NotificationHandle: Send {
     fn id(&self) -> EntityId;
@@ -107,8 +106,8 @@ impl Workspace {
             let notification = build_notification(cx);
             cx.subscribe(
                 &notification,
-                move |this, handle, event: &NotificationEvent, cx| match event {
-                    NotificationEvent::Dismiss => {
+                move |this, handle, event: &DismissEvent, cx| match event {
+                    DismissEvent::Dismiss => {
                         this.dismiss_notification_internal(type_id, id, cx);
                     }
                 },
@@ -120,6 +119,17 @@ impl Workspace {
         }
     }
 
+    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
+    where
+        E: std::fmt::Debug,
+    {
+        self.show_notification(0, cx, |cx| {
+            cx.build_view(|_cx| {
+                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
+            })
+        });
+    }
+
     pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
         let type_id = TypeId::of::<V>();
 
@@ -166,13 +176,14 @@ impl Workspace {
 }
 
 pub mod simple_message_notification {
-    use super::NotificationEvent;
-    use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
+    use gpui::{
+        div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
+        ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
+        ViewContext,
+    };
     use serde::Deserialize;
     use std::{borrow::Cow, sync::Arc};
-
-    // todo!()
-    // actions!(message_notifications, [CancelMessageNotification]);
+    use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
 
     #[derive(Clone, Default, Deserialize, PartialEq)]
     pub struct OsOpen(pub Cow<'static, str>);
@@ -197,22 +208,22 @@ pub mod simple_message_notification {
     //     }
 
     enum NotificationMessage {
-        Text(Cow<'static, str>),
+        Text(SharedString),
         Element(fn(TextStyle, &AppContext) -> AnyElement),
     }
 
     pub struct MessageNotification {
         message: NotificationMessage,
         on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
-        click_message: Option<Cow<'static, str>>,
+        click_message: Option<SharedString>,
     }
 
-    impl EventEmitter<NotificationMessage> for MessageNotification {}
+    impl EventEmitter<DismissEvent> for MessageNotification {}
 
     impl MessageNotification {
         pub fn new<S>(message: S) -> MessageNotification
         where
-            S: Into<Cow<'static, str>>,
+            S: Into<SharedString>,
         {
             Self {
                 message: NotificationMessage::Text(message.into()),
@@ -221,19 +232,20 @@ pub mod simple_message_notification {
             }
         }
 
-        pub fn new_element(
-            message: fn(TextStyle, &AppContext) -> AnyElement,
-        ) -> MessageNotification {
-            Self {
-                message: NotificationMessage::Element(message),
-                on_click: None,
-                click_message: None,
-            }
-        }
+        // not needed I think (only for the "new panel" toast, which is outdated now)
+        // pub fn new_element(
+        //     message: fn(TextStyle, &AppContext) -> AnyElement,
+        // ) -> MessageNotification {
+        //     Self {
+        //         message: NotificationMessage::Element(message),
+        //         on_click: None,
+        //         click_message: None,
+        //     }
+        // }
 
         pub fn with_click_message<S>(mut self, message: S) -> Self
         where
-            S: Into<Cow<'static, str>>,
+            S: Into<SharedString>,
         {
             self.click_message = Some(message.into());
             self
@@ -247,17 +259,43 @@ pub mod simple_message_notification {
             self
         }
 
-        // todo!()
-        // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
-        //     cx.emit(MessageNotificationEvent::Dismiss);
-        // }
+        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+            cx.emit(DismissEvent::Dismiss);
+        }
     }
 
     impl Render for MessageNotification {
         type Element = Div;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            todo!()
+            v_stack()
+                .elevation_3(cx)
+                .p_4()
+                .child(
+                    h_stack()
+                        .justify_between()
+                        .child(div().max_w_80().child(match &self.message {
+                            NotificationMessage::Text(text) => Label::new(text.clone()),
+                            NotificationMessage::Element(element) => {
+                                todo!()
+                            }
+                        }))
+                        .child(
+                            div()
+                                .id("cancel")
+                                .child(IconElement::new(Icon::Close))
+                                .cursor_pointer()
+                                .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
+                        ),
+                )
+                .children(self.click_message.iter().map(|message| {
+                    Button::new(message.clone()).on_click(cx.listener(|this, _, cx| {
+                        if let Some(on_click) = this.on_click.as_ref() {
+                            (on_click)(cx)
+                        };
+                        this.dismiss(cx)
+                    }))
+                }))
         }
     }
     // todo!()
@@ -359,8 +397,6 @@ pub mod simple_message_notification {
     //                 .into_any()
     //         }
     //     }
-
-    impl EventEmitter<NotificationEvent> for MessageNotification {}
 }
 
 pub trait NotifyResultExt {
@@ -371,6 +407,8 @@ pub trait NotifyResultExt {
         workspace: &mut Workspace,
         cx: &mut ViewContext<Workspace>,
     ) -> Option<Self::Ok>;
+
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
 }
 
 impl<T, E> NotifyResultExt for Result<T, E>
@@ -384,14 +422,23 @@ where
             Ok(value) => Some(value),
             Err(err) => {
                 log::error!("TODO {err:?}");
-                // todo!()
-                // workspace.show_notification(0, cx, |cx| {
-                //     cx.add_view(|_cx| {
-                //         simple_message_notification::MessageNotification::new(format!(
-                //             "Error: {err:?}",
-                //         ))
-                //     })
-                // });
+                workspace.show_error(&err, cx);
+                None
+            }
+        }
+    }
+
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
+        match self {
+            Ok(value) => Some(value),
+            Err(err) => {
+                log::error!("TODO {err:?}");
+                cx.update(|view, cx| {
+                    if let Ok(workspace) = view.downcast::<Workspace>() {
+                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
+                    }
+                })
+                .ok();
                 None
             }
         }

crates/workspace2/src/workspace2.rs 🔗

@@ -683,7 +683,21 @@ impl Workspace {
             }),
         ];
 
-        cx.defer(|this, cx| this.update_window_title(cx));
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+            // todo! @nate - these are useful for testing notifications
+            // this.show_error(
+            //     &anyhow::anyhow!("what happens if this message is very very very very very long"),
+            //     cx,
+            // );
+
+            // this.show_notification(1, cx, |cx| {
+            //     cx.build_view(|_cx| {
+            //         simple_message_notification::MessageNotification::new(format!("Error:"))
+            //             .with_click_message("click here because!")
+            //     })
+            // });
+        });
         Workspace {
             window_self: window_handle,
             weak_self: weak_handle.clone(),
@@ -2566,32 +2580,31 @@ impl Workspace {
     //         }
     //     }
 
-    //     fn render_notifications(
-    //         &self,
-    //         theme: &theme::Workspace,
-    //         cx: &AppContext,
-    //     ) -> Option<AnyElement<Workspace>> {
-    //         if self.notifications.is_empty() {
-    //             None
-    //         } else {
-    //             Some(
-    //                 Flex::column()
-    //                     .with_children(self.notifications.iter().map(|(_, _, notification)| {
-    //                         ChildView::new(notification.as_any(), cx)
-    //                             .contained()
-    //                             .with_style(theme.notification)
-    //                     }))
-    //                     .constrained()
-    //                     .with_width(theme.notifications.width)
-    //                     .contained()
-    //                     .with_style(theme.notifications.container)
-    //                     .aligned()
-    //                     .bottom()
-    //                     .right()
-    //                     .into_any(),
-    //             )
-    //         }
-    //     }
+    fn render_notifications(&self, cx: &ViewContext<Self>) -> Option<Div> {
+        if self.notifications.is_empty() {
+            None
+        } else {
+            Some(
+                div()
+                    .absolute()
+                    .z_index(100)
+                    .right_3()
+                    .bottom_3()
+                    .w_96()
+                    .h_full()
+                    .flex()
+                    .flex_col()
+                    .justify_end()
+                    .gap_2()
+                    .children(self.notifications.iter().map(|(_, _, notification)| {
+                        div()
+                            .on_any_mouse_down(|_, cx| cx.stop_propagation())
+                            .on_any_mouse_up(|_, cx| cx.stop_propagation())
+                            .child(notification.to_any())
+                    })),
+            )
+        }
+    }
 
     //     // RPC handlers
 
@@ -3653,7 +3666,6 @@ impl Render for Workspace {
             .bg(cx.theme().colors().background)
             .children(self.titlebar_item.clone())
             .child(
-                // todo! should this be a component a view?
                 div()
                     .id("workspace")
                     .relative()
@@ -3703,7 +3715,8 @@ impl Render for Workspace {
                                     .overflow_hidden()
                                     .child(self.right_dock.clone()),
                             ),
-                    ),
+                    )
+                    .children(self.render_notifications(cx)),
             )
             .child(self.status_bar.clone())
     }