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