diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3a9a40328d..3320359440 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -530,8 +530,6 @@ pub fn init(cx: &mut AppContext) { // cx.register_action_type(Editor::context_menu_next); // cx.register_action_type(Editor::context_menu_last); - hover_popover::init(cx); - workspace::register_project_item::(cx); workspace::register_followable_item::(cx); workspace::register_deserializable_item::(cx); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index e591dd84cf..56f4ecd2be 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -5,7 +5,9 @@ use crate::{ }, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, - hover_popover::hover_at, + hover_popover::{ + self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, @@ -257,6 +259,7 @@ impl EditorElement { // on_action(cx, Editor::open_excerpts); todo!() register_action(view, cx, Editor::toggle_soft_wrap); register_action(view, cx, Editor::toggle_inlay_hints); + register_action(view, cx, hover_popover::hover); register_action(view, cx, Editor::reveal_in_finder); register_action(view, cx, Editor::copy_path); register_action(view, cx, Editor::copy_relative_path); @@ -1024,8 +1027,8 @@ impl EditorElement { } }); - if let Some((position, mut context_menu)) = layout.context_menu.take() { - cx.with_z_index(1, |cx| { + cx.with_z_index(1, |cx| { + if let Some((position, mut context_menu)) = layout.context_menu.take() { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); let context_menu_size = context_menu.measure(available_space, cx); @@ -1053,80 +1056,72 @@ impl EditorElement { } context_menu.draw(list_origin, available_space, cx); - }) - } + } - // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - // cx.scene().push_stacking_context(None, None); + if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + // cx.scene().push_stacking_context(None, None); - // // This is safe because we check on layout whether the required row is available - // let hovered_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; + // 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; + // 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) - scroll_left; - // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; - // let hovered_point = content_origin + point(x, y); + // 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 > 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); + 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 = 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); - // } + 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.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // 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 hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y); + 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 = 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); - // } + 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.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); + hover_popover.draw(popover_origin, available_space, cx); - // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; - // } - // } + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; + } + } - // cx.scene().pop_stacking_context(); - // } + // cx.scene().pop_stacking_context(); + } + }) }, ) } @@ -1992,15 +1987,23 @@ impl EditorElement { } let visible_rows = start_row..start_row + line_layouts.len() as u32; - // todo!("hover") - // let mut hover = editor.hover_state.render( - // &snapshot, - // &style, - // visible_rows, - // editor.workspace.as_ref().map(|(w, _)| w.clone()), - // cx, - // ); - // let mode = editor.mode; + let max_size = size( + (120. * em_width) // Default size + .min(bounds.size.width / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(bounds.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { editor.render_fold_indicators( @@ -2013,27 +2016,6 @@ impl EditorElement { ) }); - // todo!("hover popovers") - // if let Some((_, hover_popovers)) = hover.as_mut() { - // for hover_popover in hover_popovers.iter_mut() { - // hover_popover.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point( - // (120. * em_width) // Default size - // .min(size.x / 2.) // Shrink to half of the editor width - // .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters - // (16. * line_height) // Default size - // .min(size.y / 2.) // Shrink to half of the editor height - // .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines - // ), - // }, - // editor, - // cx, - // ); - // } - // } - let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx .text_system() @@ -2102,7 +2084,7 @@ impl EditorElement { fold_indicators, tab_invisible, space_invisible, - // hover_popovers: hover, + hover_popovers: hover, } }) } @@ -3287,7 +3269,7 @@ pub struct LayoutState { max_row: u32, context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, - // hover_popovers: Option<(DisplayPoint, Vec)>, + hover_popovers: Option<(DisplayPoint, Vec)>, fold_indicators: Vec>, tab_invisible: ShapedLine, space_invisible: ShapedLine, diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 07d108cd65..948b391ecf 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -1,11 +1,14 @@ use crate::{ - display_map::InlayOffset, + display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; -use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView}; +use gpui::{ + actions, div, px, AnyElement, AppContext, InteractiveElement, IntoElement, Model, MouseButton, + ParentElement, Pixels, Size, StatefulInteractiveElement, Styled, Task, ViewContext, WeakView, +}; use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; @@ -17,22 +20,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; -pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; -pub const HOVER_POPOVER_GAP: f32 = 10.; +pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); +pub const HOVER_POPOVER_GAP: Pixels = px(10.); -// actions!(editor, [Hover]); +actions!(Hover); -pub fn init(cx: &mut AppContext) { - // cx.add_action(hover); +/// Bindable action which uses the most recent selection head to trigger a hover +pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { + 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) { -// 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, cx: &mut ViewContext) { @@ -74,64 +72,63 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - todo!() - // if EditorSettings::get_global(cx).hover_popover_enabled { - // if editor.pending_rename.is_some() { - // return; - // } + if EditorSettings::get_global(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } - // let Some(project) = editor.project.clone() else { - // 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); - // } + 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 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 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, - // }; + 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::( - // vec![inlay_hover.range], - // |theme| theme.editor.hover_popover.highlight, - // cx, - // ); - // this.hover_state.info_popover = Some(hover_popover); - // cx.notify(); - // })?; + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| gpui::red(), // todo!("use a proper background here") + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; - // anyhow::Ok(()) - // } - // .log_err() - // }); + anyhow::Ok(()) + } + .log_err() + }); - // editor.hover_state.info_task = Some(task); - // } + editor.hover_state.info_task = Some(task); + } } /// Hides the type information popup. @@ -420,43 +417,42 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + max_size: Size, workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec)> { - todo!("old version below") + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| match &info_popover.symbol_range { + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, + }) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, max_size, cx)); + } + if let Some(info_popover) = self.info_popover.as_mut() { + elements.push(info_popover.render(style, max_size, workspace, cx)); + } + + Some((point, elements)) } - // // If there is a diagnostic, position the popovers based on that. - // // Otherwise use the start of the hover range - // let anchor = self - // .diagnostic_popover - // .as_ref() - // .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) - // .or_else(|| { - // self.info_popover - // .as_ref() - // .map(|info_popover| match &info_popover.symbol_range { - // RangeInEditor::Text(range) => &range.start, - // RangeInEditor::Inlay(range) => &range.inlay_position, - // }) - // })?; - // let point = anchor.to_display_point(&snapshot.display_snapshot); - - // // Don't render if the relevant point isn't on screen - // if !self.visible() || !visible_rows.contains(&point.row()) { - // return None; - // } - - // let mut elements = Vec::new(); - - // if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { - // elements.push(diagnostic_popover.render(style, cx)); - // } - // if let Some(info_popover) = self.info_popover.as_mut() { - // elements.push(info_popover.render(style, workspace, cx)); - // } - - // Some((point, elements)) - // } } #[derive(Debug, Clone)] @@ -467,35 +463,36 @@ pub struct InfoPopover { parsed_content: ParsedMarkdown, } -// impl InfoPopover { -// pub fn render( -// &mut self, -// style: &EditorStyle, -// workspace: Option>, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(0, cx, |_, cx| { -// Flex::column() -// .scrollable::(0, None, cx) -// .with_child(crate::render_parsed_markdown::( -// &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, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + div() + .id("info_popover") + .overflow_y_scroll() + .bg(gpui::red()) + .max_w(max_size.width) + .max_h(max_size.height) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .child(crate::render_parsed_markdown( + "content", + &self.parsed_content, + style, + workspace, + cx, + )) + .into_any_element() + } +} #[derive(Debug, Clone)] pub struct DiagnosticPopover { @@ -504,7 +501,12 @@ pub struct DiagnosticPopover { } impl DiagnosticPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { + pub fn render( + &self, + style: &EditorStyle, + max_size: Size, + cx: &mut ViewContext, + ) -> AnyElement { todo!() // enum PrimaryDiagnostic {} @@ -567,763 +569,763 @@ impl DiagnosticPopover { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// editor_tests::init_test, -// element::PointForPosition, -// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, -// link_go_to_definition::update_inlay_link_and_hover_points, -// test::editor_lsp_test_context::EditorLspTestContext, -// InlayId, -// }; -// use collections::BTreeSet; -// use gpui::fonts::{HighlightStyle, Underline, Weight}; -// use indoc::indoc; -// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; -// use lsp::LanguageServerId; -// use project::{HoverBlock, HoverBlockKind}; -// use smol::stream::StreamExt; -// use unindent::Unindent; -// use util::test::marked_text_ranges; - -// #[gpui::test] -// async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Basic hover delays and then pops without moving the mouse -// cx.set_state(indoc! {" -// fn ˇtest() { println!(); } -// "}); -// let hover_point = cx.display_point(indoc! {" -// fn test() { printˇln!(); } -// "}); - -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// assert!(!cx.editor(|editor, _| editor.hover_state.visible())); - -// // After delay, hover should be visible. -// let symbol_range = cx.lsp_range(indoc! {" -// fn test() { «println!»(); } -// "}); -// let mut requests = -// cx.handle_request::(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::(|_, _| 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::(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::(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::(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::(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, -// expected_marked_text: String, -// expected_styles: Vec, -// } - -// 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::>(); -// 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); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "}); - -// let hint_start_offset = cx.ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(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); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); -// let struct_target_range = cx.lsp_range(indoc! {" -// struct «TestStruct»; - -// // ================== - -// struct TestNewType(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"; -// let closure_uri = uri.clone(); -// cx.lsp -// .handle_request::(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); - -// 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::( -// 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` -// 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::(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::(|_, _| 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::(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::(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::(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::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some new docs".to_string(), + }), + range: Some(range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + + cx.background_executor.run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }); + } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let editor = Editor::single_line(cx); + let style = editor.style.clone().unwrap(); + + struct Row { + blocks: Vec, + expected_marked_text: String, + expected_styles: Vec, + } + + 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::>(); + 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); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(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); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(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"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(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); + + 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::( + 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` + 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" + ); + }); + } +}