hover_popover.rs

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