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