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