hover_popover.rs

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