hover_popover.rs

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