hover_popover.rs

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