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