hover_popover.rs

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