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