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