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