hover_popover.rs

   1use crate::{
   2    display_map::{InlayOffset, ToDisplayPoint},
   3    link_go_to_definition::{InlayHighlight, RangeInEditor},
   4    Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
   5    ExcerptId, RangeToAnchorExt,
   6};
   7use futures::FutureExt;
   8use gpui::{
   9    actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model,
  10    MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,
  11    Task, ViewContext, WeakView,
  12};
  13use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
  14use lsp::DiagnosticSeverity;
  15use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
  16use settings::Settings;
  17use std::{ops::Range, sync::Arc, time::Duration};
  18use ui::{StyledExt, Tooltip};
  19use util::TryFutureExt;
  20use workspace::Workspace;
  21
  22pub const HOVER_DELAY_MILLIS: u64 = 350;
  23pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
  24
  25pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
  26pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
  27pub const HOVER_POPOVER_GAP: Pixels = px(10.);
  28
  29actions!(editor, [Hover]);
  30
  31/// Bindable action which uses the most recent selection head to trigger a hover
  32pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
  33    let head = editor.selections.newest_display(cx).head();
  34    show_hover(editor, head, true, cx);
  35}
  36
  37/// The internal hover action dispatches between `show_hover` or `hide_hover`
  38/// depending on whether a point to hover over is provided.
  39pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
  40    if EditorSettings::get_global(cx).hover_popover_enabled {
  41        if let Some(point) = point {
  42            show_hover(editor, point, false, cx);
  43        } else {
  44            hide_hover(editor, cx);
  45        }
  46    }
  47}
  48
  49pub struct InlayHover {
  50    pub excerpt: ExcerptId,
  51    pub range: InlayHighlight,
  52    pub tooltip: HoverBlock,
  53}
  54
  55pub fn find_hovered_hint_part(
  56    label_parts: Vec<InlayHintLabelPart>,
  57    hint_start: InlayOffset,
  58    hovered_offset: InlayOffset,
  59) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
  60    if hovered_offset >= hint_start {
  61        let mut hovered_character = (hovered_offset - hint_start).0;
  62        let mut part_start = hint_start;
  63        for part in label_parts {
  64            let part_len = part.value.chars().count();
  65            if hovered_character > part_len {
  66                hovered_character -= part_len;
  67                part_start.0 += part_len;
  68            } else {
  69                let part_end = InlayOffset(part_start.0 + part_len);
  70                return Some((part, part_start..part_end));
  71            }
  72        }
  73    }
  74    None
  75}
  76
  77pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
  78    if EditorSettings::get_global(cx).hover_popover_enabled {
  79        if editor.pending_rename.is_some() {
  80            return;
  81        }
  82
  83        let Some(project) = editor.project.clone() else {
  84            return;
  85        };
  86
  87        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
  88            if let RangeInEditor::Inlay(range) = symbol_range {
  89                if range == &inlay_hover.range {
  90                    // Hover triggered from same location as last time. Don't show again.
  91                    return;
  92                }
  93            }
  94            hide_hover(editor, cx);
  95        }
  96
  97        let task = cx.spawn(|this, mut cx| {
  98            async move {
  99                cx.background_executor()
 100                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
 101                    .await;
 102                this.update(&mut cx, |this, _| {
 103                    this.hover_state.diagnostic_popover = None;
 104                })?;
 105
 106                let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
 107                let blocks = vec![inlay_hover.tooltip];
 108                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
 109
 110                let hover_popover = InfoPopover {
 111                    project: project.clone(),
 112                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
 113                    blocks,
 114                    parsed_content,
 115                };
 116
 117                this.update(&mut cx, |this, cx| {
 118                    // Highlight the selected symbol using a background highlight
 119                    this.highlight_inlay_background::<HoverState>(
 120                        vec![inlay_hover.range],
 121                        |theme| theme.element_hover, // todo!("use a proper background here")
 122                        cx,
 123                    );
 124                    this.hover_state.info_popover = Some(hover_popover);
 125                    cx.notify();
 126                })?;
 127
 128                anyhow::Ok(())
 129            }
 130            .log_err()
 131        });
 132
 133        editor.hover_state.info_task = Some(task);
 134    }
 135}
 136
 137/// Hides the type information popup.
 138/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 139/// selections changed.
 140pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
 141    let did_hide = editor.hover_state.info_popover.take().is_some()
 142        | editor.hover_state.diagnostic_popover.take().is_some();
 143
 144    editor.hover_state.info_task = None;
 145    editor.hover_state.triggered_from = None;
 146
 147    editor.clear_background_highlights::<HoverState>(cx);
 148
 149    if did_hide {
 150        cx.notify();
 151    }
 152
 153    did_hide
 154}
 155
 156/// Queries the LSP and shows type info and documentation
 157/// about the symbol the mouse is currently hovering over.
 158/// Triggered by the `Hover` action when the cursor may be over a symbol.
 159fn show_hover(
 160    editor: &mut Editor,
 161    point: DisplayPoint,
 162    ignore_timeout: bool,
 163    cx: &mut ViewContext<Editor>,
 164) {
 165    if editor.pending_rename.is_some() {
 166        return;
 167    }
 168
 169    let snapshot = editor.snapshot(cx);
 170    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
 171
 172    let (buffer, buffer_position) = if let Some(output) = editor
 173        .buffer
 174        .read(cx)
 175        .text_anchor_for_position(multibuffer_offset, cx)
 176    {
 177        output
 178    } else {
 179        return;
 180    };
 181
 182    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
 183        .buffer()
 184        .read(cx)
 185        .excerpt_containing(multibuffer_offset, cx)
 186    {
 187        excerpt_id
 188    } else {
 189        return;
 190    };
 191
 192    let project = if let Some(project) = editor.project.clone() {
 193        project
 194    } else {
 195        return;
 196    };
 197
 198    if !ignore_timeout {
 199        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
 200            if symbol_range
 201                .as_text_range()
 202                .map(|range| {
 203                    range
 204                        .to_offset(&snapshot.buffer_snapshot)
 205                        .contains(&multibuffer_offset)
 206                })
 207                .unwrap_or(false)
 208            {
 209                // Hover triggered from same location as last time. Don't show again.
 210                return;
 211            } else {
 212                hide_hover(editor, cx);
 213            }
 214        }
 215    }
 216
 217    // Get input anchor
 218    let anchor = snapshot
 219        .buffer_snapshot
 220        .anchor_at(multibuffer_offset, Bias::Left);
 221
 222    // Don't request again if the location is the same as the previous request
 223    if let Some(triggered_from) = &editor.hover_state.triggered_from {
 224        if triggered_from
 225            .cmp(&anchor, &snapshot.buffer_snapshot)
 226            .is_eq()
 227        {
 228            return;
 229        }
 230    }
 231
 232    let task = cx.spawn(|this, mut cx| {
 233        async move {
 234            // If we need to delay, delay a set amount initially before making the lsp request
 235            let delay = if !ignore_timeout {
 236                // Construct delay task to wait for later
 237                let total_delay = Some(
 238                    cx.background_executor()
 239                        .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
 240                );
 241
 242                cx.background_executor()
 243                    .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
 244                    .await;
 245                total_delay
 246            } else {
 247                None
 248            };
 249
 250            // query the LSP for hover info
 251            let hover_request = cx.update(|_, cx| {
 252                project.update(cx, |project, cx| {
 253                    project.hover(&buffer, buffer_position, cx)
 254                })
 255            })?;
 256
 257            if let Some(delay) = delay {
 258                delay.await;
 259            }
 260
 261            // If there's a diagnostic, assign it on the hover state and notify
 262            let local_diagnostic = snapshot
 263                .buffer_snapshot
 264                .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
 265                // Find the entry with the most specific range
 266                .min_by_key(|entry| entry.range.end - entry.range.start)
 267                .map(|entry| DiagnosticEntry {
 268                    diagnostic: entry.diagnostic,
 269                    range: entry.range.to_anchors(&snapshot.buffer_snapshot),
 270                });
 271
 272            // Pull the primary diagnostic out so we can jump to it if the popover is clicked
 273            let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
 274                snapshot
 275                    .buffer_snapshot
 276                    .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
 277                    .find(|diagnostic| diagnostic.diagnostic.is_primary)
 278                    .map(|entry| DiagnosticEntry {
 279                        diagnostic: entry.diagnostic,
 280                        range: entry.range.to_anchors(&snapshot.buffer_snapshot),
 281                    })
 282            });
 283
 284            this.update(&mut cx, |this, _| {
 285                this.hover_state.diagnostic_popover =
 286                    local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
 287                        local_diagnostic,
 288                        primary_diagnostic,
 289                    });
 290            })?;
 291
 292            let hover_result = hover_request.await.ok().flatten();
 293            let hover_popover = match hover_result {
 294                Some(hover_result) if !hover_result.is_empty() => {
 295                    // Create symbol range of anchors for highlighting and filtering of future requests.
 296                    let range = if let Some(range) = hover_result.range {
 297                        let start = snapshot
 298                            .buffer_snapshot
 299                            .anchor_in_excerpt(excerpt_id.clone(), range.start);
 300                        let end = snapshot
 301                            .buffer_snapshot
 302                            .anchor_in_excerpt(excerpt_id.clone(), range.end);
 303
 304                        start..end
 305                    } else {
 306                        anchor..anchor
 307                    };
 308
 309                    let language_registry =
 310                        project.update(&mut cx, |p, _| p.languages().clone())?;
 311                    let blocks = hover_result.contents;
 312                    let language = hover_result.language;
 313                    let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
 314
 315                    Some(InfoPopover {
 316                        project: project.clone(),
 317                        symbol_range: RangeInEditor::Text(range),
 318                        blocks,
 319                        parsed_content,
 320                    })
 321                }
 322
 323                _ => None,
 324            };
 325
 326            this.update(&mut cx, |this, cx| {
 327                if let Some(symbol_range) = hover_popover
 328                    .as_ref()
 329                    .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
 330                {
 331                    // Highlight the selected symbol using a background highlight
 332                    this.highlight_background::<HoverState>(
 333                        vec![symbol_range],
 334                        |theme| theme.element_hover, // todo! update theme
 335                        cx,
 336                    );
 337                } else {
 338                    this.clear_background_highlights::<HoverState>(cx);
 339                }
 340
 341                this.hover_state.info_popover = hover_popover;
 342                cx.notify();
 343            })?;
 344
 345            Ok::<_, anyhow::Error>(())
 346        }
 347        .log_err()
 348    });
 349
 350    editor.hover_state.info_task = Some(task);
 351}
 352
 353async fn parse_blocks(
 354    blocks: &[HoverBlock],
 355    language_registry: &Arc<LanguageRegistry>,
 356    language: Option<Arc<Language>>,
 357) -> markdown::ParsedMarkdown {
 358    let mut text = String::new();
 359    let mut highlights = Vec::new();
 360    let mut region_ranges = Vec::new();
 361    let mut regions = Vec::new();
 362
 363    for block in blocks {
 364        match &block.kind {
 365            HoverBlockKind::PlainText => {
 366                markdown::new_paragraph(&mut text, &mut Vec::new());
 367                text.push_str(&block.text);
 368            }
 369
 370            HoverBlockKind::Markdown => {
 371                markdown::parse_markdown_block(
 372                    &block.text,
 373                    language_registry,
 374                    language.clone(),
 375                    &mut text,
 376                    &mut highlights,
 377                    &mut region_ranges,
 378                    &mut regions,
 379                )
 380                .await
 381            }
 382
 383            HoverBlockKind::Code { language } => {
 384                if let Some(language) = language_registry
 385                    .language_for_name(language)
 386                    .now_or_never()
 387                    .and_then(Result::ok)
 388                {
 389                    markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
 390                } else {
 391                    text.push_str(&block.text);
 392                }
 393            }
 394        }
 395    }
 396
 397    ParsedMarkdown {
 398        text: text.trim().to_string(),
 399        highlights,
 400        region_ranges,
 401        regions,
 402    }
 403}
 404
 405#[derive(Default)]
 406pub struct HoverState {
 407    pub info_popover: Option<InfoPopover>,
 408    pub diagnostic_popover: Option<DiagnosticPopover>,
 409    pub triggered_from: Option<Anchor>,
 410    pub info_task: Option<Task<Option<()>>>,
 411}
 412
 413impl HoverState {
 414    pub fn visible(&self) -> bool {
 415        self.info_popover.is_some() || self.diagnostic_popover.is_some()
 416    }
 417
 418    pub fn render(
 419        &mut self,
 420        snapshot: &EditorSnapshot,
 421        style: &EditorStyle,
 422        visible_rows: Range<u32>,
 423        max_size: Size<Pixels>,
 424        workspace: Option<WeakView<Workspace>>,
 425        cx: &mut ViewContext<Editor>,
 426    ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
 427        // If there is a diagnostic, position the popovers based on that.
 428        // Otherwise use the start of the hover range
 429        let anchor = self
 430            .diagnostic_popover
 431            .as_ref()
 432            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
 433            .or_else(|| {
 434                self.info_popover
 435                    .as_ref()
 436                    .map(|info_popover| match &info_popover.symbol_range {
 437                        RangeInEditor::Text(range) => &range.start,
 438                        RangeInEditor::Inlay(range) => &range.inlay_position,
 439                    })
 440            })?;
 441        let point = anchor.to_display_point(&snapshot.display_snapshot);
 442
 443        // Don't render if the relevant point isn't on screen
 444        if !self.visible() || !visible_rows.contains(&point.row()) {
 445            return None;
 446        }
 447
 448        let mut elements = Vec::new();
 449
 450        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
 451            elements.push(diagnostic_popover.render(style, max_size, cx));
 452        }
 453        if let Some(info_popover) = self.info_popover.as_mut() {
 454            elements.push(info_popover.render(style, max_size, workspace, cx));
 455        }
 456
 457        Some((point, elements))
 458    }
 459}
 460
 461#[derive(Debug, Clone)]
 462pub struct InfoPopover {
 463    pub project: Model<Project>,
 464    symbol_range: RangeInEditor,
 465    pub blocks: Vec<HoverBlock>,
 466    parsed_content: ParsedMarkdown,
 467}
 468
 469impl InfoPopover {
 470    pub fn render(
 471        &mut self,
 472        style: &EditorStyle,
 473        max_size: Size<Pixels>,
 474        workspace: Option<WeakView<Workspace>>,
 475        cx: &mut ViewContext<Editor>,
 476    ) -> AnyElement {
 477        div()
 478            .id("info_popover")
 479            .elevation_2(cx)
 480            .text_ui()
 481            .p_2()
 482            .overflow_y_scroll()
 483            .max_w(max_size.width)
 484            .max_h(max_size.height)
 485            // Prevent a mouse move on the popover from being propagated to the editor,
 486            // because that would dismiss the popover.
 487            .on_mouse_move(|_, cx| cx.stop_propagation())
 488            .child(crate::render_parsed_markdown(
 489                "content",
 490                &self.parsed_content,
 491                style,
 492                workspace,
 493                cx,
 494            ))
 495            .into_any_element()
 496    }
 497}
 498
 499#[derive(Debug, Clone)]
 500pub struct DiagnosticPopover {
 501    local_diagnostic: DiagnosticEntry<Anchor>,
 502    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
 503}
 504
 505impl DiagnosticPopover {
 506    pub fn render(
 507        &self,
 508        style: &EditorStyle,
 509        max_size: Size<Pixels>,
 510        cx: &mut ViewContext<Editor>,
 511    ) -> AnyElement {
 512        let text = match &self.local_diagnostic.diagnostic.source {
 513            Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
 514            None => self.local_diagnostic.diagnostic.message.clone(),
 515        };
 516
 517        let container_bg = crate::diagnostic_style(
 518            self.local_diagnostic.diagnostic.severity,
 519            true,
 520            &style.diagnostic_style,
 521        );
 522
 523        div()
 524            .id("diagnostic")
 525            .overflow_y_scroll()
 526            .bg(container_bg)
 527            .max_w(max_size.width)
 528            .max_h(max_size.height)
 529            .cursor(CursorStyle::PointingHand)
 530            .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
 531            // Prevent a mouse move on the popover from being propagated to the editor,
 532            // because that would dismiss the popover.
 533            .on_mouse_move(|_, cx| cx.stop_propagation())
 534            // Prevent a mouse down on the popover from being propagated to the editor,
 535            // because that would move the cursor.
 536            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
 537            .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
 538            .child(SharedString::from(text))
 539            .into_any_element()
 540    }
 541
 542    pub fn activation_info(&self) -> (usize, Anchor) {
 543        let entry = self
 544            .primary_diagnostic
 545            .as_ref()
 546            .unwrap_or(&self.local_diagnostic);
 547
 548        (entry.diagnostic.group_id, entry.range.start.clone())
 549    }
 550}
 551
 552#[cfg(test)]
 553mod tests {
 554    use super::*;
 555    use crate::{
 556        editor_tests::init_test,
 557        element::PointForPosition,
 558        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
 559        link_go_to_definition::update_inlay_link_and_hover_points,
 560        test::editor_lsp_test_context::EditorLspTestContext,
 561        InlayId,
 562    };
 563    use collections::BTreeSet;
 564    use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
 565    use indoc::indoc;
 566    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
 567    use lsp::LanguageServerId;
 568    use project::{HoverBlock, HoverBlockKind};
 569    use smol::stream::StreamExt;
 570    use unindent::Unindent;
 571    use util::test::marked_text_ranges;
 572
 573    #[gpui::test]
 574    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
 575        init_test(cx, |_| {});
 576
 577        let mut cx = EditorLspTestContext::new_rust(
 578            lsp::ServerCapabilities {
 579                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 580                ..Default::default()
 581            },
 582            cx,
 583        )
 584        .await;
 585
 586        // Basic hover delays and then pops without moving the mouse
 587        cx.set_state(indoc! {"
 588            fn ˇtest() { println!(); }
 589        "});
 590        let hover_point = cx.display_point(indoc! {"
 591            fn test() { printˇln!(); }
 592        "});
 593
 594        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 595        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 596
 597        // After delay, hover should be visible.
 598        let symbol_range = cx.lsp_range(indoc! {"
 599            fn test() { «println!»(); }
 600        "});
 601        let mut requests =
 602            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 603                Ok(Some(lsp::Hover {
 604                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 605                        kind: lsp::MarkupKind::Markdown,
 606                        value: "some basic docs".to_string(),
 607                    }),
 608                    range: Some(symbol_range),
 609                }))
 610            });
 611        cx.background_executor
 612            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 613        requests.next().await;
 614
 615        cx.editor(|editor, _| {
 616            assert!(editor.hover_state.visible());
 617            assert_eq!(
 618                editor.hover_state.info_popover.clone().unwrap().blocks,
 619                vec![HoverBlock {
 620                    text: "some basic docs".to_string(),
 621                    kind: HoverBlockKind::Markdown,
 622                },]
 623            )
 624        });
 625
 626        // Mouse moved with no hover response dismisses
 627        let hover_point = cx.display_point(indoc! {"
 628            fn teˇst() { println!(); }
 629        "});
 630        let mut request = cx
 631            .lsp
 632            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
 633        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 634        cx.background_executor
 635            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 636        request.next().await;
 637        cx.editor(|editor, _| {
 638            assert!(!editor.hover_state.visible());
 639        });
 640    }
 641
 642    #[gpui::test]
 643    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
 644        init_test(cx, |_| {});
 645
 646        let mut cx = EditorLspTestContext::new_rust(
 647            lsp::ServerCapabilities {
 648                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 649                ..Default::default()
 650            },
 651            cx,
 652        )
 653        .await;
 654
 655        // Hover with keyboard has no delay
 656        cx.set_state(indoc! {"
 657            fˇn test() { println!(); }
 658        "});
 659        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 660        let symbol_range = cx.lsp_range(indoc! {"
 661            «fn» test() { println!(); }
 662        "});
 663        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 664            Ok(Some(lsp::Hover {
 665                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 666                    kind: lsp::MarkupKind::Markdown,
 667                    value: "some other basic docs".to_string(),
 668                }),
 669                range: Some(symbol_range),
 670            }))
 671        })
 672        .next()
 673        .await;
 674
 675        cx.condition(|editor, _| editor.hover_state.visible()).await;
 676        cx.editor(|editor, _| {
 677            assert_eq!(
 678                editor.hover_state.info_popover.clone().unwrap().blocks,
 679                vec![HoverBlock {
 680                    text: "some other basic docs".to_string(),
 681                    kind: HoverBlockKind::Markdown,
 682                }]
 683            )
 684        });
 685    }
 686
 687    #[gpui::test]
 688    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
 689        init_test(cx, |_| {});
 690
 691        let mut cx = EditorLspTestContext::new_rust(
 692            lsp::ServerCapabilities {
 693                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 694                ..Default::default()
 695            },
 696            cx,
 697        )
 698        .await;
 699
 700        // Hover with keyboard has no delay
 701        cx.set_state(indoc! {"
 702            fˇn test() { println!(); }
 703        "});
 704        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 705        let symbol_range = cx.lsp_range(indoc! {"
 706            «fn» test() { println!(); }
 707        "});
 708        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 709            Ok(Some(lsp::Hover {
 710                contents: lsp::HoverContents::Array(vec![
 711                    lsp::MarkedString::String("regular text for hover to show".to_string()),
 712                    lsp::MarkedString::String("".to_string()),
 713                    lsp::MarkedString::LanguageString(lsp::LanguageString {
 714                        language: "Rust".to_string(),
 715                        value: "".to_string(),
 716                    }),
 717                ]),
 718                range: Some(symbol_range),
 719            }))
 720        })
 721        .next()
 722        .await;
 723
 724        cx.condition(|editor, _| editor.hover_state.visible()).await;
 725        cx.editor(|editor, _| {
 726            assert_eq!(
 727                editor.hover_state.info_popover.clone().unwrap().blocks,
 728                vec![HoverBlock {
 729                    text: "regular text for hover to show".to_string(),
 730                    kind: HoverBlockKind::Markdown,
 731                }],
 732                "No empty string hovers should be shown"
 733            );
 734        });
 735    }
 736
 737    #[gpui::test]
 738    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
 739        init_test(cx, |_| {});
 740
 741        let mut cx = EditorLspTestContext::new_rust(
 742            lsp::ServerCapabilities {
 743                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 744                ..Default::default()
 745            },
 746            cx,
 747        )
 748        .await;
 749
 750        // Hover with keyboard has no delay
 751        cx.set_state(indoc! {"
 752            fˇn test() { println!(); }
 753        "});
 754        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 755        let symbol_range = cx.lsp_range(indoc! {"
 756            «fn» test() { println!(); }
 757        "});
 758
 759        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
 760        let markdown_string = format!("\n```rust\n{code_str}```");
 761
 762        let closure_markdown_string = markdown_string.clone();
 763        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
 764            let future_markdown_string = closure_markdown_string.clone();
 765            async move {
 766                Ok(Some(lsp::Hover {
 767                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 768                        kind: lsp::MarkupKind::Markdown,
 769                        value: future_markdown_string,
 770                    }),
 771                    range: Some(symbol_range),
 772                }))
 773            }
 774        })
 775        .next()
 776        .await;
 777
 778        cx.condition(|editor, _| editor.hover_state.visible()).await;
 779        cx.editor(|editor, _| {
 780            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
 781            assert_eq!(
 782                blocks,
 783                vec![HoverBlock {
 784                    text: markdown_string,
 785                    kind: HoverBlockKind::Markdown,
 786                }],
 787            );
 788
 789            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 790            assert_eq!(
 791                rendered.text,
 792                code_str.trim(),
 793                "Should not have extra line breaks at end of rendered hover"
 794            );
 795        });
 796    }
 797
 798    #[gpui::test]
 799    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
 800        init_test(cx, |_| {});
 801
 802        let mut cx = EditorLspTestContext::new_rust(
 803            lsp::ServerCapabilities {
 804                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 805                ..Default::default()
 806            },
 807            cx,
 808        )
 809        .await;
 810
 811        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
 812        // info popover once request completes
 813        cx.set_state(indoc! {"
 814            fn teˇst() { println!(); }
 815        "});
 816
 817        // Send diagnostic to client
 818        let range = cx.text_anchor_range(indoc! {"
 819            fn «test»() { println!(); }
 820        "});
 821        cx.update_buffer(|buffer, cx| {
 822            let snapshot = buffer.text_snapshot();
 823            let set = DiagnosticSet::from_sorted_entries(
 824                vec![DiagnosticEntry {
 825                    range,
 826                    diagnostic: Diagnostic {
 827                        message: "A test diagnostic message.".to_string(),
 828                        ..Default::default()
 829                    },
 830                }],
 831                &snapshot,
 832            );
 833            buffer.update_diagnostics(LanguageServerId(0), set, cx);
 834        });
 835
 836        // Hover pops diagnostic immediately
 837        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 838        cx.background_executor.run_until_parked();
 839
 840        cx.editor(|Editor { hover_state, .. }, _| {
 841            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
 842        });
 843
 844        // Info Popover shows after request responded to
 845        let range = cx.lsp_range(indoc! {"
 846            fn «test»() { println!(); }
 847        "});
 848        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 849            Ok(Some(lsp::Hover {
 850                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 851                    kind: lsp::MarkupKind::Markdown,
 852                    value: "some new docs".to_string(),
 853                }),
 854                range: Some(range),
 855            }))
 856        });
 857        cx.background_executor
 858            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 859
 860        cx.background_executor.run_until_parked();
 861        cx.editor(|Editor { hover_state, .. }, _| {
 862            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
 863        });
 864    }
 865
 866    #[gpui::test]
 867    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
 868        init_test(cx, |_| {});
 869
 870        let editor = cx.add_window(|cx| Editor::single_line(cx));
 871        editor
 872            .update(cx, |editor, cx| {
 873                let style = editor.style.clone().unwrap();
 874
 875                struct Row {
 876                    blocks: Vec<HoverBlock>,
 877                    expected_marked_text: String,
 878                    expected_styles: Vec<HighlightStyle>,
 879                }
 880
 881                let rows = &[
 882                    // Strong emphasis
 883                    Row {
 884                        blocks: vec![HoverBlock {
 885                            text: "one **two** three".to_string(),
 886                            kind: HoverBlockKind::Markdown,
 887                        }],
 888                        expected_marked_text: "one «two» three".to_string(),
 889                        expected_styles: vec![HighlightStyle {
 890                            font_weight: Some(FontWeight::BOLD),
 891                            ..Default::default()
 892                        }],
 893                    },
 894                    // Links
 895                    Row {
 896                        blocks: vec![HoverBlock {
 897                            text: "one [two](https://the-url) three".to_string(),
 898                            kind: HoverBlockKind::Markdown,
 899                        }],
 900                        expected_marked_text: "one «two» three".to_string(),
 901                        expected_styles: vec![HighlightStyle {
 902                            underline: Some(UnderlineStyle {
 903                                thickness: 1.0.into(),
 904                                ..Default::default()
 905                            }),
 906                            ..Default::default()
 907                        }],
 908                    },
 909                    // Lists
 910                    Row {
 911                        blocks: vec![HoverBlock {
 912                            text: "
 913                            lists:
 914                            * one
 915                                - a
 916                                - b
 917                            * two
 918                                - [c](https://the-url)
 919                                - d"
 920                            .unindent(),
 921                            kind: HoverBlockKind::Markdown,
 922                        }],
 923                        expected_marked_text: "
 924                        lists:
 925                        - one
 926                          - a
 927                          - b
 928                        - two
 929                          - «c»
 930                          - d"
 931                        .unindent(),
 932                        expected_styles: vec![HighlightStyle {
 933                            underline: Some(UnderlineStyle {
 934                                thickness: 1.0.into(),
 935                                ..Default::default()
 936                            }),
 937                            ..Default::default()
 938                        }],
 939                    },
 940                    // Multi-paragraph list items
 941                    Row {
 942                        blocks: vec![HoverBlock {
 943                            text: "
 944                            * one two
 945                              three
 946
 947                            * four five
 948                                * six seven
 949                                  eight
 950
 951                                  nine
 952                                * ten
 953                            * six"
 954                                .unindent(),
 955                            kind: HoverBlockKind::Markdown,
 956                        }],
 957                        expected_marked_text: "
 958                        - one two three
 959                        - four five
 960                          - six seven eight
 961
 962                            nine
 963                          - ten
 964                        - six"
 965                            .unindent(),
 966                        expected_styles: vec![HighlightStyle {
 967                            underline: Some(UnderlineStyle {
 968                                thickness: 1.0.into(),
 969                                ..Default::default()
 970                            }),
 971                            ..Default::default()
 972                        }],
 973                    },
 974                ];
 975
 976                for Row {
 977                    blocks,
 978                    expected_marked_text,
 979                    expected_styles,
 980                } in &rows[0..]
 981                {
 982                    let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 983
 984                    let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
 985                    let expected_highlights = ranges
 986                        .into_iter()
 987                        .zip(expected_styles.iter().cloned())
 988                        .collect::<Vec<_>>();
 989                    assert_eq!(
 990                        rendered.text, expected_text,
 991                        "wrong text for input {blocks:?}"
 992                    );
 993
 994                    let rendered_highlights: Vec<_> = rendered
 995                        .highlights
 996                        .iter()
 997                        .filter_map(|(range, highlight)| {
 998                            let highlight = highlight.to_highlight_style(&style.syntax)?;
 999                            Some((range.clone(), highlight))
1000                        })
1001                        .collect();
1002
1003                    assert_eq!(
1004                        rendered_highlights, expected_highlights,
1005                        "wrong highlights for input {blocks:?}"
1006                    );
1007                }
1008            })
1009            .unwrap();
1010    }
1011
1012    #[gpui::test]
1013    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1014        init_test(cx, |settings| {
1015            settings.defaults.inlay_hints = Some(InlayHintSettings {
1016                enabled: true,
1017                show_type_hints: true,
1018                show_parameter_hints: true,
1019                show_other_hints: true,
1020            })
1021        });
1022
1023        let mut cx = EditorLspTestContext::new_rust(
1024            lsp::ServerCapabilities {
1025                inlay_hint_provider: Some(lsp::OneOf::Right(
1026                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1027                        resolve_provider: Some(true),
1028                        ..Default::default()
1029                    }),
1030                )),
1031                ..Default::default()
1032            },
1033            cx,
1034        )
1035        .await;
1036
1037        cx.set_state(indoc! {"
1038            struct TestStruct;
1039
1040            // ==================
1041
1042            struct TestNewType<T>(T);
1043
1044            fn main() {
1045                let variableˇ = TestNewType(TestStruct);
1046            }
1047        "});
1048
1049        let hint_start_offset = cx.ranges(indoc! {"
1050            struct TestStruct;
1051
1052            // ==================
1053
1054            struct TestNewType<T>(T);
1055
1056            fn main() {
1057                let variableˇ = TestNewType(TestStruct);
1058            }
1059        "})[0]
1060            .start;
1061        let hint_position = cx.to_lsp(hint_start_offset);
1062        let new_type_target_range = cx.lsp_range(indoc! {"
1063            struct TestStruct;
1064
1065            // ==================
1066
1067            struct «TestNewType»<T>(T);
1068
1069            fn main() {
1070                let variable = TestNewType(TestStruct);
1071            }
1072        "});
1073        let struct_target_range = cx.lsp_range(indoc! {"
1074            struct «TestStruct»;
1075
1076            // ==================
1077
1078            struct TestNewType<T>(T);
1079
1080            fn main() {
1081                let variable = TestNewType(TestStruct);
1082            }
1083        "});
1084
1085        let uri = cx.buffer_lsp_url.clone();
1086        let new_type_label = "TestNewType";
1087        let struct_label = "TestStruct";
1088        let entire_hint_label = ": TestNewType<TestStruct>";
1089        let closure_uri = uri.clone();
1090        cx.lsp
1091            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1092                let task_uri = closure_uri.clone();
1093                async move {
1094                    assert_eq!(params.text_document.uri, task_uri);
1095                    Ok(Some(vec![lsp::InlayHint {
1096                        position: hint_position,
1097                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1098                            value: entire_hint_label.to_string(),
1099                            ..Default::default()
1100                        }]),
1101                        kind: Some(lsp::InlayHintKind::TYPE),
1102                        text_edits: None,
1103                        tooltip: None,
1104                        padding_left: Some(false),
1105                        padding_right: Some(false),
1106                        data: None,
1107                    }]))
1108                }
1109            })
1110            .next()
1111            .await;
1112        cx.background_executor.run_until_parked();
1113        cx.update_editor(|editor, cx| {
1114            let expected_layers = vec![entire_hint_label.to_string()];
1115            assert_eq!(expected_layers, cached_hint_labels(editor));
1116            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1117        });
1118
1119        let inlay_range = cx
1120            .ranges(indoc! {"
1121                struct TestStruct;
1122
1123                // ==================
1124
1125                struct TestNewType<T>(T);
1126
1127                fn main() {
1128                    let variable« »= TestNewType(TestStruct);
1129                }
1130        "})
1131            .get(0)
1132            .cloned()
1133            .unwrap();
1134        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1135            let snapshot = editor.snapshot(cx);
1136            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1137            let next_valid = inlay_range.end.to_display_point(&snapshot);
1138            assert_eq!(previous_valid.row(), next_valid.row());
1139            assert!(previous_valid.column() < next_valid.column());
1140            let exact_unclipped = DisplayPoint::new(
1141                previous_valid.row(),
1142                previous_valid.column()
1143                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1144                        as u32,
1145            );
1146            PointForPosition {
1147                previous_valid,
1148                next_valid,
1149                exact_unclipped,
1150                column_overshoot_after_line_end: 0,
1151            }
1152        });
1153        cx.update_editor(|editor, cx| {
1154            update_inlay_link_and_hover_points(
1155                &editor.snapshot(cx),
1156                new_type_hint_part_hover_position,
1157                editor,
1158                true,
1159                false,
1160                cx,
1161            );
1162        });
1163
1164        let resolve_closure_uri = uri.clone();
1165        cx.lsp
1166            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1167                move |mut hint_to_resolve, _| {
1168                    let mut resolved_hint_positions = BTreeSet::new();
1169                    let task_uri = resolve_closure_uri.clone();
1170                    async move {
1171                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1172                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1173
1174                        // `: TestNewType<TestStruct>`
1175                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1176                            lsp::InlayHintLabelPart {
1177                                value: ": ".to_string(),
1178                                ..Default::default()
1179                            },
1180                            lsp::InlayHintLabelPart {
1181                                value: new_type_label.to_string(),
1182                                location: Some(lsp::Location {
1183                                    uri: task_uri.clone(),
1184                                    range: new_type_target_range,
1185                                }),
1186                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1187                                    "A tooltip for `{new_type_label}`"
1188                                ))),
1189                                ..Default::default()
1190                            },
1191                            lsp::InlayHintLabelPart {
1192                                value: "<".to_string(),
1193                                ..Default::default()
1194                            },
1195                            lsp::InlayHintLabelPart {
1196                                value: struct_label.to_string(),
1197                                location: Some(lsp::Location {
1198                                    uri: task_uri,
1199                                    range: struct_target_range,
1200                                }),
1201                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1202                                    lsp::MarkupContent {
1203                                        kind: lsp::MarkupKind::Markdown,
1204                                        value: format!("A tooltip for `{struct_label}`"),
1205                                    },
1206                                )),
1207                                ..Default::default()
1208                            },
1209                            lsp::InlayHintLabelPart {
1210                                value: ">".to_string(),
1211                                ..Default::default()
1212                            },
1213                        ]);
1214
1215                        Ok(hint_to_resolve)
1216                    }
1217                },
1218            )
1219            .next()
1220            .await;
1221        cx.background_executor.run_until_parked();
1222
1223        cx.update_editor(|editor, cx| {
1224            update_inlay_link_and_hover_points(
1225                &editor.snapshot(cx),
1226                new_type_hint_part_hover_position,
1227                editor,
1228                true,
1229                false,
1230                cx,
1231            );
1232        });
1233        cx.background_executor
1234            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1235        cx.background_executor.run_until_parked();
1236        cx.update_editor(|editor, cx| {
1237            let hover_state = &editor.hover_state;
1238            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1239            let popover = hover_state.info_popover.as_ref().unwrap();
1240            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1241            assert_eq!(
1242                popover.symbol_range,
1243                RangeInEditor::Inlay(InlayHighlight {
1244                    inlay: InlayId::Hint(0),
1245                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1246                    range: ": ".len()..": ".len() + new_type_label.len(),
1247                }),
1248                "Popover range should match the new type label part"
1249            );
1250            assert_eq!(
1251                popover.parsed_content.text,
1252                format!("A tooltip for `{new_type_label}`"),
1253                "Rendered text should not anyhow alter backticks"
1254            );
1255        });
1256
1257        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1258            let snapshot = editor.snapshot(cx);
1259            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1260            let next_valid = inlay_range.end.to_display_point(&snapshot);
1261            assert_eq!(previous_valid.row(), next_valid.row());
1262            assert!(previous_valid.column() < next_valid.column());
1263            let exact_unclipped = DisplayPoint::new(
1264                previous_valid.row(),
1265                previous_valid.column()
1266                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1267                        as u32,
1268            );
1269            PointForPosition {
1270                previous_valid,
1271                next_valid,
1272                exact_unclipped,
1273                column_overshoot_after_line_end: 0,
1274            }
1275        });
1276        cx.update_editor(|editor, cx| {
1277            update_inlay_link_and_hover_points(
1278                &editor.snapshot(cx),
1279                struct_hint_part_hover_position,
1280                editor,
1281                true,
1282                false,
1283                cx,
1284            );
1285        });
1286        cx.background_executor
1287            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1288        cx.background_executor.run_until_parked();
1289        cx.update_editor(|editor, cx| {
1290            let hover_state = &editor.hover_state;
1291            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1292            let popover = hover_state.info_popover.as_ref().unwrap();
1293            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1294            assert_eq!(
1295                popover.symbol_range,
1296                RangeInEditor::Inlay(InlayHighlight {
1297                    inlay: InlayId::Hint(0),
1298                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1299                    range: ": ".len() + new_type_label.len() + "<".len()
1300                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1301                }),
1302                "Popover range should match the struct label part"
1303            );
1304            assert_eq!(
1305                popover.parsed_content.text,
1306                format!("A tooltip for {struct_label}"),
1307                "Rendered markdown element should remove backticks from text"
1308            );
1309        });
1310    }
1311}