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