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