hover_popover.rs

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