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::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!(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            .overflow_y_scroll()
 480            .bg(gpui::red())
 481            .max_w(max_size.width)
 482            .max_h(max_size.height)
 483            // Prevent a mouse move on the popover from being propagated to the editor,
 484            // because that would dismiss the popover.
 485            .on_mouse_move(|_, cx| cx.stop_propagation())
 486            // Prevent a mouse down on the popover from being propagated to the editor,
 487            // because that would move the cursor.
 488            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
 489            .child(crate::render_parsed_markdown(
 490                "content",
 491                &self.parsed_content,
 492                style,
 493                workspace,
 494                cx,
 495            ))
 496            .into_any_element()
 497    }
 498}
 499
 500#[derive(Debug, Clone)]
 501pub struct DiagnosticPopover {
 502    local_diagnostic: DiagnosticEntry<Anchor>,
 503    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
 504}
 505
 506impl DiagnosticPopover {
 507    pub fn render(
 508        &self,
 509        style: &EditorStyle,
 510        max_size: Size<Pixels>,
 511        cx: &mut ViewContext<Editor>,
 512    ) -> AnyElement {
 513        let text = match &self.local_diagnostic.diagnostic.source {
 514            Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
 515            None => self.local_diagnostic.diagnostic.message.clone(),
 516        };
 517
 518        let container_bg = crate::diagnostic_style(
 519            self.local_diagnostic.diagnostic.severity,
 520            true,
 521            &style.diagnostic_style,
 522        );
 523
 524        div()
 525            .id("diagnostic")
 526            .overflow_y_scroll()
 527            .bg(container_bg)
 528            .max_w(max_size.width)
 529            .max_h(max_size.height)
 530            .cursor(CursorStyle::PointingHand)
 531            .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
 532            // Prevent a mouse move on the popover from being propagated to the editor,
 533            // because that would dismiss the popover.
 534            .on_mouse_move(|_, cx| cx.stop_propagation())
 535            // Prevent a mouse down on the popover from being propagated to the editor,
 536            // because that would move the cursor.
 537            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
 538            .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
 539            .child(SharedString::from(text))
 540            .into_any_element()
 541    }
 542
 543    pub fn activation_info(&self) -> (usize, Anchor) {
 544        let entry = self
 545            .primary_diagnostic
 546            .as_ref()
 547            .unwrap_or(&self.local_diagnostic);
 548
 549        (entry.diagnostic.group_id, entry.range.start.clone())
 550    }
 551}
 552
 553#[cfg(test)]
 554mod tests {
 555    use super::*;
 556    use crate::{
 557        editor_tests::init_test,
 558        element::PointForPosition,
 559        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
 560        link_go_to_definition::update_inlay_link_and_hover_points,
 561        test::editor_lsp_test_context::EditorLspTestContext,
 562        InlayId,
 563    };
 564    use collections::BTreeSet;
 565    use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
 566    use indoc::indoc;
 567    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
 568    use lsp::LanguageServerId;
 569    use project::{HoverBlock, HoverBlockKind};
 570    use smol::stream::StreamExt;
 571    use unindent::Unindent;
 572    use util::test::marked_text_ranges;
 573
 574    #[gpui::test]
 575    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
 576        init_test(cx, |_| {});
 577
 578        let mut cx = EditorLspTestContext::new_rust(
 579            lsp::ServerCapabilities {
 580                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 581                ..Default::default()
 582            },
 583            cx,
 584        )
 585        .await;
 586
 587        // Basic hover delays and then pops without moving the mouse
 588        cx.set_state(indoc! {"
 589            fn ˇtest() { println!(); }
 590        "});
 591        let hover_point = cx.display_point(indoc! {"
 592            fn test() { printˇln!(); }
 593        "});
 594
 595        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 596        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 597
 598        // After delay, hover should be visible.
 599        let symbol_range = cx.lsp_range(indoc! {"
 600            fn test() { «println!»(); }
 601        "});
 602        let mut requests =
 603            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 604                Ok(Some(lsp::Hover {
 605                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 606                        kind: lsp::MarkupKind::Markdown,
 607                        value: "some basic docs".to_string(),
 608                    }),
 609                    range: Some(symbol_range),
 610                }))
 611            });
 612        cx.background_executor
 613            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 614        requests.next().await;
 615
 616        cx.editor(|editor, _| {
 617            assert!(editor.hover_state.visible());
 618            assert_eq!(
 619                editor.hover_state.info_popover.clone().unwrap().blocks,
 620                vec![HoverBlock {
 621                    text: "some basic docs".to_string(),
 622                    kind: HoverBlockKind::Markdown,
 623                },]
 624            )
 625        });
 626
 627        // Mouse moved with no hover response dismisses
 628        let hover_point = cx.display_point(indoc! {"
 629            fn teˇst() { println!(); }
 630        "});
 631        let mut request = cx
 632            .lsp
 633            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
 634        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 635        cx.background_executor
 636            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 637        request.next().await;
 638        cx.editor(|editor, _| {
 639            assert!(!editor.hover_state.visible());
 640        });
 641    }
 642
 643    #[gpui::test]
 644    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
 645        init_test(cx, |_| {});
 646
 647        let mut cx = EditorLspTestContext::new_rust(
 648            lsp::ServerCapabilities {
 649                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 650                ..Default::default()
 651            },
 652            cx,
 653        )
 654        .await;
 655
 656        // Hover with keyboard has no delay
 657        cx.set_state(indoc! {"
 658            fˇn test() { println!(); }
 659        "});
 660        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 661        let symbol_range = cx.lsp_range(indoc! {"
 662            «fn» test() { println!(); }
 663        "});
 664        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 665            Ok(Some(lsp::Hover {
 666                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 667                    kind: lsp::MarkupKind::Markdown,
 668                    value: "some other basic docs".to_string(),
 669                }),
 670                range: Some(symbol_range),
 671            }))
 672        })
 673        .next()
 674        .await;
 675
 676        cx.condition(|editor, _| editor.hover_state.visible()).await;
 677        cx.editor(|editor, _| {
 678            assert_eq!(
 679                editor.hover_state.info_popover.clone().unwrap().blocks,
 680                vec![HoverBlock {
 681                    text: "some other basic docs".to_string(),
 682                    kind: HoverBlockKind::Markdown,
 683                }]
 684            )
 685        });
 686    }
 687
 688    #[gpui::test]
 689    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
 690        init_test(cx, |_| {});
 691
 692        let mut cx = EditorLspTestContext::new_rust(
 693            lsp::ServerCapabilities {
 694                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 695                ..Default::default()
 696            },
 697            cx,
 698        )
 699        .await;
 700
 701        // Hover with keyboard has no delay
 702        cx.set_state(indoc! {"
 703            fˇn test() { println!(); }
 704        "});
 705        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 706        let symbol_range = cx.lsp_range(indoc! {"
 707            «fn» test() { println!(); }
 708        "});
 709        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 710            Ok(Some(lsp::Hover {
 711                contents: lsp::HoverContents::Array(vec![
 712                    lsp::MarkedString::String("regular text for hover to show".to_string()),
 713                    lsp::MarkedString::String("".to_string()),
 714                    lsp::MarkedString::LanguageString(lsp::LanguageString {
 715                        language: "Rust".to_string(),
 716                        value: "".to_string(),
 717                    }),
 718                ]),
 719                range: Some(symbol_range),
 720            }))
 721        })
 722        .next()
 723        .await;
 724
 725        cx.condition(|editor, _| editor.hover_state.visible()).await;
 726        cx.editor(|editor, _| {
 727            assert_eq!(
 728                editor.hover_state.info_popover.clone().unwrap().blocks,
 729                vec![HoverBlock {
 730                    text: "regular text for hover to show".to_string(),
 731                    kind: HoverBlockKind::Markdown,
 732                }],
 733                "No empty string hovers should be shown"
 734            );
 735        });
 736    }
 737
 738    #[gpui::test]
 739    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
 740        init_test(cx, |_| {});
 741
 742        let mut cx = EditorLspTestContext::new_rust(
 743            lsp::ServerCapabilities {
 744                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 745                ..Default::default()
 746            },
 747            cx,
 748        )
 749        .await;
 750
 751        // Hover with keyboard has no delay
 752        cx.set_state(indoc! {"
 753            fˇn test() { println!(); }
 754        "});
 755        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 756        let symbol_range = cx.lsp_range(indoc! {"
 757            «fn» test() { println!(); }
 758        "});
 759
 760        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
 761        let markdown_string = format!("\n```rust\n{code_str}```");
 762
 763        let closure_markdown_string = markdown_string.clone();
 764        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
 765            let future_markdown_string = closure_markdown_string.clone();
 766            async move {
 767                Ok(Some(lsp::Hover {
 768                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 769                        kind: lsp::MarkupKind::Markdown,
 770                        value: future_markdown_string,
 771                    }),
 772                    range: Some(symbol_range),
 773                }))
 774            }
 775        })
 776        .next()
 777        .await;
 778
 779        cx.condition(|editor, _| editor.hover_state.visible()).await;
 780        cx.editor(|editor, _| {
 781            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
 782            assert_eq!(
 783                blocks,
 784                vec![HoverBlock {
 785                    text: markdown_string,
 786                    kind: HoverBlockKind::Markdown,
 787                }],
 788            );
 789
 790            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 791            assert_eq!(
 792                rendered.text,
 793                code_str.trim(),
 794                "Should not have extra line breaks at end of rendered hover"
 795            );
 796        });
 797    }
 798
 799    #[gpui::test]
 800    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
 801        init_test(cx, |_| {});
 802
 803        let mut cx = EditorLspTestContext::new_rust(
 804            lsp::ServerCapabilities {
 805                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 806                ..Default::default()
 807            },
 808            cx,
 809        )
 810        .await;
 811
 812        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
 813        // info popover once request completes
 814        cx.set_state(indoc! {"
 815            fn teˇst() { println!(); }
 816        "});
 817
 818        // Send diagnostic to client
 819        let range = cx.text_anchor_range(indoc! {"
 820            fn «test»() { println!(); }
 821        "});
 822        cx.update_buffer(|buffer, cx| {
 823            let snapshot = buffer.text_snapshot();
 824            let set = DiagnosticSet::from_sorted_entries(
 825                vec![DiagnosticEntry {
 826                    range,
 827                    diagnostic: Diagnostic {
 828                        message: "A test diagnostic message.".to_string(),
 829                        ..Default::default()
 830                    },
 831                }],
 832                &snapshot,
 833            );
 834            buffer.update_diagnostics(LanguageServerId(0), set, cx);
 835        });
 836
 837        // Hover pops diagnostic immediately
 838        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 839        cx.background_executor.run_until_parked();
 840
 841        cx.editor(|Editor { hover_state, .. }, _| {
 842            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
 843        });
 844
 845        // Info Popover shows after request responded to
 846        let range = cx.lsp_range(indoc! {"
 847            fn «test»() { println!(); }
 848        "});
 849        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 850            Ok(Some(lsp::Hover {
 851                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 852                    kind: lsp::MarkupKind::Markdown,
 853                    value: "some new docs".to_string(),
 854                }),
 855                range: Some(range),
 856            }))
 857        });
 858        cx.background_executor
 859            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 860
 861        cx.background_executor.run_until_parked();
 862        cx.editor(|Editor { hover_state, .. }, _| {
 863            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
 864        });
 865    }
 866
 867    #[gpui::test]
 868    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
 869        init_test(cx, |_| {});
 870
 871        let editor = cx.add_window(|cx| Editor::single_line(cx));
 872        editor
 873            .update(cx, |editor, cx| {
 874                let style = editor.style.clone().unwrap();
 875
 876                struct Row {
 877                    blocks: Vec<HoverBlock>,
 878                    expected_marked_text: String,
 879                    expected_styles: Vec<HighlightStyle>,
 880                }
 881
 882                let rows = &[
 883                    // Strong emphasis
 884                    Row {
 885                        blocks: vec![HoverBlock {
 886                            text: "one **two** three".to_string(),
 887                            kind: HoverBlockKind::Markdown,
 888                        }],
 889                        expected_marked_text: "one «two» three".to_string(),
 890                        expected_styles: vec![HighlightStyle {
 891                            font_weight: Some(FontWeight::BOLD),
 892                            ..Default::default()
 893                        }],
 894                    },
 895                    // Links
 896                    Row {
 897                        blocks: vec![HoverBlock {
 898                            text: "one [two](https://the-url) three".to_string(),
 899                            kind: HoverBlockKind::Markdown,
 900                        }],
 901                        expected_marked_text: "one «two» three".to_string(),
 902                        expected_styles: vec![HighlightStyle {
 903                            underline: Some(UnderlineStyle {
 904                                thickness: 1.0.into(),
 905                                ..Default::default()
 906                            }),
 907                            ..Default::default()
 908                        }],
 909                    },
 910                    // Lists
 911                    Row {
 912                        blocks: vec![HoverBlock {
 913                            text: "
 914                            lists:
 915                            * one
 916                                - a
 917                                - b
 918                            * two
 919                                - [c](https://the-url)
 920                                - d"
 921                            .unindent(),
 922                            kind: HoverBlockKind::Markdown,
 923                        }],
 924                        expected_marked_text: "
 925                        lists:
 926                        - one
 927                          - a
 928                          - b
 929                        - two
 930                          - «c»
 931                          - d"
 932                        .unindent(),
 933                        expected_styles: vec![HighlightStyle {
 934                            underline: Some(UnderlineStyle {
 935                                thickness: 1.0.into(),
 936                                ..Default::default()
 937                            }),
 938                            ..Default::default()
 939                        }],
 940                    },
 941                    // Multi-paragraph list items
 942                    Row {
 943                        blocks: vec![HoverBlock {
 944                            text: "
 945                            * one two
 946                              three
 947
 948                            * four five
 949                                * six seven
 950                                  eight
 951
 952                                  nine
 953                                * ten
 954                            * six"
 955                                .unindent(),
 956                            kind: HoverBlockKind::Markdown,
 957                        }],
 958                        expected_marked_text: "
 959                        - one two three
 960                        - four five
 961                          - six seven eight
 962
 963                            nine
 964                          - ten
 965                        - six"
 966                            .unindent(),
 967                        expected_styles: vec![HighlightStyle {
 968                            underline: Some(UnderlineStyle {
 969                                thickness: 1.0.into(),
 970                                ..Default::default()
 971                            }),
 972                            ..Default::default()
 973                        }],
 974                    },
 975                ];
 976
 977                for Row {
 978                    blocks,
 979                    expected_marked_text,
 980                    expected_styles,
 981                } in &rows[0..]
 982                {
 983                    let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 984
 985                    let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
 986                    let expected_highlights = ranges
 987                        .into_iter()
 988                        .zip(expected_styles.iter().cloned())
 989                        .collect::<Vec<_>>();
 990                    assert_eq!(
 991                        rendered.text, expected_text,
 992                        "wrong text for input {blocks:?}"
 993                    );
 994
 995                    let rendered_highlights: Vec<_> = rendered
 996                        .highlights
 997                        .iter()
 998                        .filter_map(|(range, highlight)| {
 999                            let highlight = highlight.to_highlight_style(&style.syntax)?;
1000                            Some((range.clone(), highlight))
1001                        })
1002                        .collect();
1003
1004                    assert_eq!(
1005                        rendered_highlights, expected_highlights,
1006                        "wrong highlights for input {blocks:?}"
1007                    );
1008                }
1009            })
1010            .unwrap();
1011    }
1012
1013    #[gpui::test]
1014    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1015        init_test(cx, |settings| {
1016            settings.defaults.inlay_hints = Some(InlayHintSettings {
1017                enabled: true,
1018                show_type_hints: true,
1019                show_parameter_hints: true,
1020                show_other_hints: true,
1021            })
1022        });
1023
1024        let mut cx = EditorLspTestContext::new_rust(
1025            lsp::ServerCapabilities {
1026                inlay_hint_provider: Some(lsp::OneOf::Right(
1027                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1028                        resolve_provider: Some(true),
1029                        ..Default::default()
1030                    }),
1031                )),
1032                ..Default::default()
1033            },
1034            cx,
1035        )
1036        .await;
1037
1038        cx.set_state(indoc! {"
1039            struct TestStruct;
1040
1041            // ==================
1042
1043            struct TestNewType<T>(T);
1044
1045            fn main() {
1046                let variableˇ = TestNewType(TestStruct);
1047            }
1048        "});
1049
1050        let hint_start_offset = cx.ranges(indoc! {"
1051            struct TestStruct;
1052
1053            // ==================
1054
1055            struct TestNewType<T>(T);
1056
1057            fn main() {
1058                let variableˇ = TestNewType(TestStruct);
1059            }
1060        "})[0]
1061            .start;
1062        let hint_position = cx.to_lsp(hint_start_offset);
1063        let new_type_target_range = cx.lsp_range(indoc! {"
1064            struct TestStruct;
1065
1066            // ==================
1067
1068            struct «TestNewType»<T>(T);
1069
1070            fn main() {
1071                let variable = TestNewType(TestStruct);
1072            }
1073        "});
1074        let struct_target_range = cx.lsp_range(indoc! {"
1075            struct «TestStruct»;
1076
1077            // ==================
1078
1079            struct TestNewType<T>(T);
1080
1081            fn main() {
1082                let variable = TestNewType(TestStruct);
1083            }
1084        "});
1085
1086        let uri = cx.buffer_lsp_url.clone();
1087        let new_type_label = "TestNewType";
1088        let struct_label = "TestStruct";
1089        let entire_hint_label = ": TestNewType<TestStruct>";
1090        let closure_uri = uri.clone();
1091        cx.lsp
1092            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1093                let task_uri = closure_uri.clone();
1094                async move {
1095                    assert_eq!(params.text_document.uri, task_uri);
1096                    Ok(Some(vec![lsp::InlayHint {
1097                        position: hint_position,
1098                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1099                            value: entire_hint_label.to_string(),
1100                            ..Default::default()
1101                        }]),
1102                        kind: Some(lsp::InlayHintKind::TYPE),
1103                        text_edits: None,
1104                        tooltip: None,
1105                        padding_left: Some(false),
1106                        padding_right: Some(false),
1107                        data: None,
1108                    }]))
1109                }
1110            })
1111            .next()
1112            .await;
1113        cx.background_executor.run_until_parked();
1114        cx.update_editor(|editor, cx| {
1115            let expected_layers = vec![entire_hint_label.to_string()];
1116            assert_eq!(expected_layers, cached_hint_labels(editor));
1117            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1118        });
1119
1120        let inlay_range = cx
1121            .ranges(indoc! {"
1122                struct TestStruct;
1123
1124                // ==================
1125
1126                struct TestNewType<T>(T);
1127
1128                fn main() {
1129                    let variable« »= TestNewType(TestStruct);
1130                }
1131        "})
1132            .get(0)
1133            .cloned()
1134            .unwrap();
1135        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1136            let snapshot = editor.snapshot(cx);
1137            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1138            let next_valid = inlay_range.end.to_display_point(&snapshot);
1139            assert_eq!(previous_valid.row(), next_valid.row());
1140            assert!(previous_valid.column() < next_valid.column());
1141            let exact_unclipped = DisplayPoint::new(
1142                previous_valid.row(),
1143                previous_valid.column()
1144                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1145                        as u32,
1146            );
1147            PointForPosition {
1148                previous_valid,
1149                next_valid,
1150                exact_unclipped,
1151                column_overshoot_after_line_end: 0,
1152            }
1153        });
1154        cx.update_editor(|editor, cx| {
1155            update_inlay_link_and_hover_points(
1156                &editor.snapshot(cx),
1157                new_type_hint_part_hover_position,
1158                editor,
1159                true,
1160                false,
1161                cx,
1162            );
1163        });
1164
1165        let resolve_closure_uri = uri.clone();
1166        cx.lsp
1167            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1168                move |mut hint_to_resolve, _| {
1169                    let mut resolved_hint_positions = BTreeSet::new();
1170                    let task_uri = resolve_closure_uri.clone();
1171                    async move {
1172                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1173                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1174
1175                        // `: TestNewType<TestStruct>`
1176                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1177                            lsp::InlayHintLabelPart {
1178                                value: ": ".to_string(),
1179                                ..Default::default()
1180                            },
1181                            lsp::InlayHintLabelPart {
1182                                value: new_type_label.to_string(),
1183                                location: Some(lsp::Location {
1184                                    uri: task_uri.clone(),
1185                                    range: new_type_target_range,
1186                                }),
1187                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1188                                    "A tooltip for `{new_type_label}`"
1189                                ))),
1190                                ..Default::default()
1191                            },
1192                            lsp::InlayHintLabelPart {
1193                                value: "<".to_string(),
1194                                ..Default::default()
1195                            },
1196                            lsp::InlayHintLabelPart {
1197                                value: struct_label.to_string(),
1198                                location: Some(lsp::Location {
1199                                    uri: task_uri,
1200                                    range: struct_target_range,
1201                                }),
1202                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1203                                    lsp::MarkupContent {
1204                                        kind: lsp::MarkupKind::Markdown,
1205                                        value: format!("A tooltip for `{struct_label}`"),
1206                                    },
1207                                )),
1208                                ..Default::default()
1209                            },
1210                            lsp::InlayHintLabelPart {
1211                                value: ">".to_string(),
1212                                ..Default::default()
1213                            },
1214                        ]);
1215
1216                        Ok(hint_to_resolve)
1217                    }
1218                },
1219            )
1220            .next()
1221            .await;
1222        cx.background_executor.run_until_parked();
1223
1224        cx.update_editor(|editor, cx| {
1225            update_inlay_link_and_hover_points(
1226                &editor.snapshot(cx),
1227                new_type_hint_part_hover_position,
1228                editor,
1229                true,
1230                false,
1231                cx,
1232            );
1233        });
1234        cx.background_executor
1235            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1236        cx.background_executor.run_until_parked();
1237        cx.update_editor(|editor, cx| {
1238            let hover_state = &editor.hover_state;
1239            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1240            let popover = hover_state.info_popover.as_ref().unwrap();
1241            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1242            assert_eq!(
1243                popover.symbol_range,
1244                RangeInEditor::Inlay(InlayHighlight {
1245                    inlay: InlayId::Hint(0),
1246                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1247                    range: ": ".len()..": ".len() + new_type_label.len(),
1248                }),
1249                "Popover range should match the new type label part"
1250            );
1251            assert_eq!(
1252                popover.parsed_content.text,
1253                format!("A tooltip for `{new_type_label}`"),
1254                "Rendered text should not anyhow alter backticks"
1255            );
1256        });
1257
1258        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1259            let snapshot = editor.snapshot(cx);
1260            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1261            let next_valid = inlay_range.end.to_display_point(&snapshot);
1262            assert_eq!(previous_valid.row(), next_valid.row());
1263            assert!(previous_valid.column() < next_valid.column());
1264            let exact_unclipped = DisplayPoint::new(
1265                previous_valid.row(),
1266                previous_valid.column()
1267                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1268                        as u32,
1269            );
1270            PointForPosition {
1271                previous_valid,
1272                next_valid,
1273                exact_unclipped,
1274                column_overshoot_after_line_end: 0,
1275            }
1276        });
1277        cx.update_editor(|editor, cx| {
1278            update_inlay_link_and_hover_points(
1279                &editor.snapshot(cx),
1280                struct_hint_part_hover_position,
1281                editor,
1282                true,
1283                false,
1284                cx,
1285            );
1286        });
1287        cx.background_executor
1288            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1289        cx.background_executor.run_until_parked();
1290        cx.update_editor(|editor, cx| {
1291            let hover_state = &editor.hover_state;
1292            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1293            let popover = hover_state.info_popover.as_ref().unwrap();
1294            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1295            assert_eq!(
1296                popover.symbol_range,
1297                RangeInEditor::Inlay(InlayHighlight {
1298                    inlay: InlayId::Hint(0),
1299                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1300                    range: ": ".len() + new_type_label.len() + "<".len()
1301                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1302                }),
1303                "Popover range should match the struct label part"
1304            );
1305            assert_eq!(
1306                popover.parsed_content.text,
1307                format!("A tooltip for {struct_label}"),
1308                "Rendered markdown element should remove backticks from text"
1309            );
1310        });
1311    }
1312}