hover_popover.rs

   1use crate::{
   2    ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
   3    EditorSnapshot, GlobalDiagnosticRenderer, HighlightKey, Hover,
   4    display_map::{InlayOffset, ToDisplayPoint, is_invisible},
   5    editor_settings::EditorSettingsScrollbarProxy,
   6    hover_links::{InlayHighlight, RangeInEditor},
   7    movement::TextLayoutDetails,
   8    scroll::ScrollAmount,
   9};
  10use anyhow::Context as _;
  11use gpui::{
  12    AnyElement, App, AsyncWindowContext, Bounds, Context, Entity, Focusable as _, FontWeight, Hsla,
  13    InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
  14    StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
  15    Window, canvas, div, px,
  16};
  17use itertools::Itertools;
  18use language::{DiagnosticEntry, Language, LanguageRegistry};
  19use lsp::DiagnosticSeverity;
  20use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
  21use multi_buffer::{MultiBufferOffset, ToOffset, ToPoint};
  22use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
  23use settings::Settings;
  24use std::{
  25    borrow::Cow,
  26    cell::{Cell, RefCell},
  27};
  28use std::{ops::Range, sync::Arc, time::Duration};
  29use std::{path::PathBuf, rc::Rc};
  30use theme_settings::ThemeSettings;
  31use ui::{CopyButton, Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
  32use url::Url;
  33use util::TryFutureExt;
  34use workspace::{OpenOptions, OpenVisible, Workspace};
  35
  36pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
  37pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
  38pub const POPOVER_RIGHT_OFFSET: Pixels = px(8.0);
  39pub const HOVER_POPOVER_GAP: Pixels = px(10.);
  40
  41/// Bindable action which uses the most recent selection head to trigger a hover
  42pub fn hover(editor: &mut Editor, _: &Hover, window: &mut Window, cx: &mut Context<Editor>) {
  43    let head = editor.selections.newest_anchor().head();
  44    show_hover(editor, head, true, window, cx);
  45}
  46
  47/// The internal hover action dispatches between `show_hover` or `hide_hover`
  48/// depending on whether a point to hover over is provided.
  49pub fn hover_at(
  50    editor: &mut Editor,
  51    anchor: Option<Anchor>,
  52    mouse_position: Option<gpui::Point<Pixels>>,
  53    window: &mut Window,
  54    cx: &mut Context<Editor>,
  55) {
  56    if EditorSettings::get_global(cx).hover_popover_enabled {
  57        if show_keyboard_hover(editor, window, cx) {
  58            return;
  59        }
  60
  61        if let Some(anchor) = anchor {
  62            editor.hover_state.hiding_delay_task = None;
  63            editor.hover_state.closest_mouse_distance = None;
  64            show_hover(editor, anchor, false, window, cx);
  65        } else {
  66            let settings = EditorSettings::get_global(cx);
  67            if !settings.hover_popover_sticky {
  68                hide_hover(editor, cx);
  69                return;
  70            }
  71
  72            let mut getting_closer = false;
  73            if let Some(mouse_position) = mouse_position {
  74                getting_closer = editor.hover_state.is_mouse_getting_closer(mouse_position);
  75            }
  76
  77            // If we are moving away and a timer is already running, just let it count down.
  78            if !getting_closer && editor.hover_state.hiding_delay_task.is_some() {
  79                return;
  80            }
  81
  82            // If we are moving closer, or if no timer is running at all, start/restart the timer.
  83            let delay = Duration::from_millis(settings.hover_popover_hiding_delay.0);
  84            let task = cx.spawn(async move |this, cx| {
  85                cx.background_executor().timer(delay).await;
  86                this.update(cx, |editor, cx| {
  87                    hide_hover(editor, cx);
  88                })
  89                .ok();
  90            });
  91            editor.hover_state.hiding_delay_task = Some(task);
  92        }
  93    }
  94}
  95
  96pub fn show_keyboard_hover(
  97    editor: &mut Editor,
  98    window: &mut Window,
  99    cx: &mut Context<Editor>,
 100) -> bool {
 101    if let Some(anchor) = editor.hover_state.info_popovers.iter().find_map(|p| {
 102        if *p.keyboard_grace.borrow() {
 103            p.anchor
 104        } else {
 105            None
 106        }
 107    }) {
 108        show_hover(editor, anchor, false, window, cx);
 109        return true;
 110    }
 111
 112    if let Some(anchor) = editor
 113        .hover_state
 114        .diagnostic_popover
 115        .as_ref()
 116        .and_then(|d| {
 117            if *d.keyboard_grace.borrow() {
 118                Some(d.anchor)
 119            } else {
 120                None
 121            }
 122        })
 123    {
 124        show_hover(editor, anchor, false, window, cx);
 125        return true;
 126    }
 127
 128    false
 129}
 130
 131pub struct InlayHover {
 132    pub(crate) range: InlayHighlight,
 133    pub tooltip: HoverBlock,
 134}
 135
 136pub fn find_hovered_hint_part(
 137    label_parts: Vec<InlayHintLabelPart>,
 138    hint_start: InlayOffset,
 139    hovered_offset: InlayOffset,
 140) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
 141    if hovered_offset >= hint_start {
 142        let mut offset_in_hint = hovered_offset - hint_start;
 143        let mut part_start = hint_start;
 144        for part in label_parts {
 145            let part_len = part.value.len();
 146            if offset_in_hint >= part_len {
 147                offset_in_hint -= part_len;
 148                part_start.0 += part_len;
 149            } else {
 150                let part_end = InlayOffset(part_start.0 + part_len);
 151                return Some((part, part_start..part_end));
 152            }
 153        }
 154    }
 155    None
 156}
 157
 158pub fn hover_at_inlay(
 159    editor: &mut Editor,
 160    inlay_hover: InlayHover,
 161    window: &mut Window,
 162    cx: &mut Context<Editor>,
 163) {
 164    if EditorSettings::get_global(cx).hover_popover_enabled {
 165        if editor.pending_rename.is_some() {
 166            return;
 167        }
 168
 169        let Some(project) = editor.project.clone() else {
 170            return;
 171        };
 172
 173        if editor
 174            .hover_state
 175            .info_popovers
 176            .iter()
 177            .any(|InfoPopover { symbol_range, .. }| {
 178                if let RangeInEditor::Inlay(range) = symbol_range
 179                    && range == &inlay_hover.range
 180                {
 181                    // Hover triggered from same location as last time. Don't show again.
 182                    return true;
 183                }
 184                false
 185            })
 186        {
 187            return;
 188        }
 189
 190        let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
 191
 192        editor.hover_state.hiding_delay_task = None;
 193        editor.hover_state.closest_mouse_distance = None;
 194
 195        let task = cx.spawn_in(window, async move |this, cx| {
 196            async move {
 197                cx.background_executor()
 198                    .timer(Duration::from_millis(hover_popover_delay))
 199                    .await;
 200                this.update(cx, |this, _| {
 201                    this.hover_state.diagnostic_popover = None;
 202                })?;
 203
 204                let language_registry = project.read_with(cx, |p, _| p.languages().clone());
 205                let blocks = vec![inlay_hover.tooltip];
 206                let parsed_content =
 207                    parse_blocks(&blocks, Some(&language_registry), None, cx).await;
 208
 209                let scroll_handle = ScrollHandle::new();
 210
 211                let subscription = this
 212                    .update(cx, |_, cx| {
 213                        parsed_content.as_ref().map(|parsed_content| {
 214                            cx.observe(parsed_content, |_, _, cx| cx.notify())
 215                        })
 216                    })
 217                    .ok()
 218                    .flatten();
 219
 220                let hover_popover = InfoPopover {
 221                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
 222                    parsed_content,
 223                    scroll_handle,
 224                    keyboard_grace: Rc::new(RefCell::new(false)),
 225                    anchor: None,
 226                    last_bounds: Rc::new(Cell::new(None)),
 227                    _subscription: subscription,
 228                };
 229
 230                this.update(cx, |this, cx| {
 231                    // TODO: no background highlights happen for inlays currently
 232                    this.hover_state.info_popovers = vec![hover_popover];
 233                    cx.notify();
 234                })?;
 235
 236                anyhow::Ok(())
 237            }
 238            .log_err()
 239            .await
 240        });
 241
 242        editor.hover_state.info_task = Some(task);
 243    }
 244}
 245
 246/// Hides the type information popup.
 247/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 248/// selections changed.
 249pub fn hide_hover(editor: &mut Editor, cx: &mut Context<Editor>) -> bool {
 250    let info_popovers = editor.hover_state.info_popovers.drain(..);
 251    let diagnostics_popover = editor.hover_state.diagnostic_popover.take();
 252    let did_hide = info_popovers.count() > 0 || diagnostics_popover.is_some();
 253
 254    editor.hover_state.info_task = None;
 255    editor.hover_state.triggered_from = None;
 256    editor.hover_state.hiding_delay_task = None;
 257    editor.hover_state.closest_mouse_distance = None;
 258
 259    editor.clear_background_highlights(HighlightKey::HoverState, cx);
 260
 261    if did_hide {
 262        cx.notify();
 263    }
 264
 265    did_hide
 266}
 267
 268/// Queries the LSP and shows type info and documentation
 269/// about the symbol the mouse is currently hovering over.
 270/// Triggered by the `Hover` action when the cursor may be over a symbol.
 271fn show_hover(
 272    editor: &mut Editor,
 273    anchor: Anchor,
 274    ignore_timeout: bool,
 275    window: &mut Window,
 276    cx: &mut Context<Editor>,
 277) -> Option<()> {
 278    if editor.pending_rename.is_some() {
 279        return None;
 280    }
 281
 282    let snapshot = editor.snapshot(window, cx);
 283
 284    let (buffer_position, _) = editor
 285        .buffer
 286        .read(cx)
 287        .snapshot(cx)
 288        .anchor_to_buffer_anchor(anchor)?;
 289    let buffer = editor.buffer.read(cx).buffer(buffer_position.buffer_id)?;
 290
 291    let language_registry = editor
 292        .project()
 293        .map(|project| project.read(cx).languages().clone());
 294    let provider = editor.semantics_provider.clone()?;
 295
 296    editor.hover_state.hiding_delay_task = None;
 297    editor.hover_state.closest_mouse_distance = None;
 298
 299    if !ignore_timeout {
 300        if same_info_hover(editor, &snapshot, anchor)
 301            || same_diagnostic_hover(editor, &snapshot, anchor)
 302            || editor.hover_state.diagnostic_popover.is_some()
 303        {
 304            // Hover triggered from same location as last time. Don't show again.
 305            return None;
 306        } else {
 307            hide_hover(editor, cx);
 308        }
 309    }
 310
 311    // Don't request again if the location is the same as the previous request
 312    if let Some(triggered_from) = &editor.hover_state.triggered_from
 313        && triggered_from
 314            .cmp(&anchor, &snapshot.buffer_snapshot())
 315            .is_eq()
 316    {
 317        return None;
 318    }
 319
 320    let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
 321    let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
 322    let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics {
 323        Some(group.group_id)
 324    } else {
 325        None
 326    };
 327
 328    let renderer = GlobalDiagnosticRenderer::global(cx);
 329    let task = cx.spawn_in(window, async move |this, cx| {
 330        async move {
 331            // If we need to delay, delay a set amount initially before making the lsp request
 332            let delay = if ignore_timeout {
 333                None
 334            } else {
 335                let lsp_request_early = hover_popover_delay / 2;
 336                cx.background_executor()
 337                    .timer(Duration::from_millis(
 338                        hover_popover_delay - lsp_request_early,
 339                    ))
 340                    .await;
 341
 342                // Construct delay task to wait for later
 343                let total_delay = Some(
 344                    cx.background_executor()
 345                        .timer(Duration::from_millis(lsp_request_early)),
 346                );
 347                total_delay
 348            };
 349
 350            let hover_request = cx.update(|_, cx| provider.hover(&buffer, buffer_position, cx))?;
 351
 352            if let Some(delay) = delay {
 353                delay.await;
 354            }
 355            let offset = anchor.to_offset(&snapshot.buffer_snapshot());
 356            let local_diagnostic = if all_diagnostics_active {
 357                None
 358            } else {
 359                snapshot
 360                    .buffer_snapshot()
 361                    .diagnostics_with_buffer_ids_in_range::<MultiBufferOffset>(offset..offset)
 362                    .filter(|(_, diagnostic)| {
 363                        Some(diagnostic.diagnostic.group_id) != active_group_id
 364                    })
 365                    // Find the entry with the most specific range
 366                    .min_by_key(|(_, entry)| entry.range.end - entry.range.start)
 367            };
 368
 369            let diagnostic_popover = if let Some((buffer_id, local_diagnostic)) = local_diagnostic {
 370                let group = snapshot
 371                    .buffer_snapshot()
 372                    .diagnostic_group(buffer_id, local_diagnostic.diagnostic.group_id)
 373                    .collect::<Vec<_>>();
 374                let point_range = local_diagnostic
 375                    .range
 376                    .start
 377                    .to_point(&snapshot.buffer_snapshot())
 378                    ..local_diagnostic
 379                        .range
 380                        .end
 381                        .to_point(&snapshot.buffer_snapshot());
 382                let markdown = cx.update(|_, cx| {
 383                    renderer
 384                        .as_ref()
 385                        .and_then(|renderer| {
 386                            renderer.render_hover(
 387                                group,
 388                                point_range,
 389                                buffer_id,
 390                                language_registry.clone(),
 391                                cx,
 392                            )
 393                        })
 394                        .context("no rendered diagnostic")
 395                })??;
 396
 397                let (background_color, border_color) = cx.update(|_, cx| {
 398                    let status_colors = cx.theme().status();
 399                    match local_diagnostic.diagnostic.severity {
 400                        DiagnosticSeverity::ERROR => {
 401                            (status_colors.error_background, status_colors.error_border)
 402                        }
 403                        DiagnosticSeverity::WARNING => (
 404                            status_colors.warning_background,
 405                            status_colors.warning_border,
 406                        ),
 407                        DiagnosticSeverity::INFORMATION => {
 408                            (status_colors.info_background, status_colors.info_border)
 409                        }
 410                        DiagnosticSeverity::HINT => {
 411                            (status_colors.hint_background, status_colors.hint_border)
 412                        }
 413                        _ => (
 414                            status_colors.ignored_background,
 415                            status_colors.ignored_border,
 416                        ),
 417                    }
 418                })?;
 419
 420                let subscription =
 421                    this.update(cx, |_, cx| cx.observe(&markdown, |_, _, cx| cx.notify()))?;
 422
 423                let local_diagnostic = DiagnosticEntry {
 424                    diagnostic: local_diagnostic.diagnostic.to_owned(),
 425                    range: snapshot
 426                        .buffer_snapshot()
 427                        .anchor_before(local_diagnostic.range.start)
 428                        ..snapshot
 429                            .buffer_snapshot()
 430                            .anchor_after(local_diagnostic.range.end),
 431                };
 432
 433                let scroll_handle = ScrollHandle::new();
 434
 435                Some(DiagnosticPopover {
 436                    local_diagnostic,
 437                    markdown,
 438                    border_color,
 439                    scroll_handle,
 440                    background_color,
 441                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
 442                    anchor,
 443                    last_bounds: Rc::new(Cell::new(None)),
 444                    _subscription: subscription,
 445                })
 446            } else {
 447                None
 448            };
 449
 450            this.update(cx, |this, _| {
 451                this.hover_state.diagnostic_popover = diagnostic_popover;
 452            })?;
 453
 454            let invisible_char = if let Some(invisible) = snapshot
 455                .buffer_snapshot()
 456                .chars_at(anchor)
 457                .next()
 458                .filter(|&c| is_invisible(c))
 459            {
 460                let after = snapshot.buffer_snapshot().anchor_after(
 461                    anchor.to_offset(&snapshot.buffer_snapshot()) + invisible.len_utf8(),
 462                );
 463                Some((invisible, anchor..after))
 464            } else if let Some(invisible) = snapshot
 465                .buffer_snapshot()
 466                .reversed_chars_at(anchor)
 467                .next()
 468                .filter(|&c| is_invisible(c))
 469            {
 470                let before = snapshot.buffer_snapshot().anchor_before(
 471                    anchor.to_offset(&snapshot.buffer_snapshot()) - invisible.len_utf8(),
 472                );
 473
 474                Some((invisible, before..anchor))
 475            } else {
 476                None
 477            };
 478
 479            let hovers_response = if let Some(hover_request) = hover_request {
 480                hover_request.await.unwrap_or_default()
 481            } else {
 482                Vec::new()
 483            };
 484            let snapshot = this.update_in(cx, |this, window, cx| this.snapshot(window, cx))?;
 485            let mut hover_highlights = Vec::with_capacity(hovers_response.len());
 486            let mut info_popovers = Vec::with_capacity(
 487                hovers_response.len() + if invisible_char.is_some() { 1 } else { 0 },
 488            );
 489
 490            if let Some((invisible, range)) = invisible_char {
 491                let blocks = vec![HoverBlock {
 492                    text: format!("Unicode character U+{:02X}", invisible as u32),
 493                    kind: HoverBlockKind::PlainText,
 494                }];
 495                let parsed_content =
 496                    parse_blocks(&blocks, language_registry.as_ref(), None, cx).await;
 497                let scroll_handle = ScrollHandle::new();
 498                let subscription = this
 499                    .update(cx, |_, cx| {
 500                        parsed_content.as_ref().map(|parsed_content| {
 501                            cx.observe(parsed_content, |_, _, cx| cx.notify())
 502                        })
 503                    })
 504                    .ok()
 505                    .flatten();
 506                info_popovers.push(InfoPopover {
 507                    symbol_range: RangeInEditor::Text(range),
 508                    parsed_content,
 509                    scroll_handle,
 510                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
 511                    anchor: Some(anchor),
 512                    last_bounds: Rc::new(Cell::new(None)),
 513                    _subscription: subscription,
 514                })
 515            }
 516
 517            for hover_result in hovers_response {
 518                // Create symbol range of anchors for highlighting and filtering of future requests.
 519                let range = hover_result
 520                    .range
 521                    .and_then(|range| {
 522                        let range = snapshot
 523                            .buffer_snapshot()
 524                            .buffer_anchor_range_to_anchor_range(range)?;
 525                        Some(range)
 526                    })
 527                    .or_else(|| {
 528                        let snapshot = &snapshot.buffer_snapshot();
 529                        let range = snapshot.syntax_ancestor(anchor..anchor)?.1;
 530                        Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
 531                    })
 532                    .unwrap_or_else(|| anchor..anchor);
 533
 534                let blocks = hover_result.contents;
 535                let language = hover_result.language;
 536                let parsed_content =
 537                    parse_blocks(&blocks, language_registry.as_ref(), language, cx).await;
 538                let scroll_handle = ScrollHandle::new();
 539                hover_highlights.push(range.clone());
 540                let subscription = this
 541                    .update(cx, |_, cx| {
 542                        parsed_content.as_ref().map(|parsed_content| {
 543                            cx.observe(parsed_content, |_, _, cx| cx.notify())
 544                        })
 545                    })
 546                    .ok()
 547                    .flatten();
 548                info_popovers.push(InfoPopover {
 549                    symbol_range: RangeInEditor::Text(range),
 550                    parsed_content,
 551                    scroll_handle,
 552                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
 553                    anchor: Some(anchor),
 554                    last_bounds: Rc::new(Cell::new(None)),
 555                    _subscription: subscription,
 556                });
 557            }
 558
 559            this.update_in(cx, |editor, window, cx| {
 560                if hover_highlights.is_empty() {
 561                    editor.clear_background_highlights(HighlightKey::HoverState, cx);
 562                } else {
 563                    // Highlight the selected symbol using a background highlight
 564                    editor.highlight_background(
 565                        HighlightKey::HoverState,
 566                        &hover_highlights,
 567                        |_, theme| theme.colors().element_hover, // todo update theme
 568                        cx,
 569                    );
 570                }
 571
 572                editor.hover_state.info_popovers = info_popovers;
 573                cx.notify();
 574                window.refresh();
 575            })?;
 576
 577            anyhow::Ok(())
 578        }
 579        .log_err()
 580        .await
 581    });
 582
 583    editor.hover_state.info_task = Some(task);
 584    None
 585}
 586
 587fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
 588    editor
 589        .hover_state
 590        .info_popovers
 591        .iter()
 592        .any(|InfoPopover { symbol_range, .. }| {
 593            symbol_range
 594                .as_text_range()
 595                .map(|range| {
 596                    let hover_range = range.to_offset(&snapshot.buffer_snapshot());
 597                    let offset = anchor.to_offset(&snapshot.buffer_snapshot());
 598                    // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
 599                    // use an inclusive range here to check if we should dismiss the popover
 600                    (hover_range.start..=hover_range.end).contains(&offset)
 601                })
 602                .unwrap_or(false)
 603        })
 604}
 605
 606fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
 607    editor
 608        .hover_state
 609        .diagnostic_popover
 610        .as_ref()
 611        .map(|diagnostic| {
 612            let hover_range = diagnostic
 613                .local_diagnostic
 614                .range
 615                .to_offset(&snapshot.buffer_snapshot());
 616            let offset = anchor.to_offset(&snapshot.buffer_snapshot());
 617
 618            // Here we do basically the same as in `same_info_hover`, see comment there for an explanation
 619            (hover_range.start..=hover_range.end).contains(&offset)
 620        })
 621        .unwrap_or(false)
 622}
 623
 624async fn parse_blocks(
 625    blocks: &[HoverBlock],
 626    language_registry: Option<&Arc<LanguageRegistry>>,
 627    language: Option<Arc<Language>>,
 628    cx: &mut AsyncWindowContext,
 629) -> Option<Entity<Markdown>> {
 630    let combined_text = blocks
 631        .iter()
 632        .map(|block| match &block.kind {
 633            project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
 634                Cow::Borrowed(block.text.trim())
 635            }
 636            project::HoverBlockKind::Code { language } => {
 637                Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
 638            }
 639        })
 640        .join("\n\n");
 641
 642    cx.new_window_entity(|_window, cx| {
 643        Markdown::new(
 644            combined_text.into(),
 645            language_registry.cloned(),
 646            language.map(|language| language.name()),
 647            cx,
 648        )
 649    })
 650    .ok()
 651}
 652
 653pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 654    let settings = ThemeSettings::get_global(cx);
 655    let ui_font_family = settings.ui_font.family.clone();
 656    let ui_font_features = settings.ui_font.features.clone();
 657    let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
 658    let buffer_font_family = settings.buffer_font.family.clone();
 659    let buffer_font_features = settings.buffer_font.features.clone();
 660    let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
 661    let buffer_font_weight = settings.buffer_font.weight;
 662
 663    let mut base_text_style = window.text_style();
 664    base_text_style.refine(&TextStyleRefinement {
 665        font_family: Some(ui_font_family),
 666        font_features: Some(ui_font_features),
 667        font_fallbacks: ui_font_fallbacks,
 668        color: Some(cx.theme().colors().editor_foreground),
 669        ..Default::default()
 670    });
 671    MarkdownStyle {
 672        base_text_style,
 673        code_block: StyleRefinement::default()
 674            .my(rems(1.))
 675            .font_buffer(cx)
 676            .font_features(buffer_font_features.clone())
 677            .font_weight(buffer_font_weight),
 678        inline_code: TextStyleRefinement {
 679            background_color: Some(cx.theme().colors().background),
 680            font_family: Some(buffer_font_family),
 681            font_features: Some(buffer_font_features),
 682            font_fallbacks: buffer_font_fallbacks,
 683            font_weight: Some(buffer_font_weight),
 684            ..Default::default()
 685        },
 686        rule_color: cx.theme().colors().border,
 687        block_quote_border_color: Color::Muted.color(cx),
 688        block_quote: TextStyleRefinement {
 689            color: Some(Color::Muted.color(cx)),
 690            ..Default::default()
 691        },
 692        link: TextStyleRefinement {
 693            color: Some(cx.theme().colors().editor_foreground),
 694            underline: Some(gpui::UnderlineStyle {
 695                thickness: px(1.),
 696                color: Some(cx.theme().colors().editor_foreground),
 697                wavy: false,
 698            }),
 699            ..Default::default()
 700        },
 701        syntax: cx.theme().syntax().clone(),
 702        selection_background_color: cx.theme().colors().element_selection_background,
 703        heading: StyleRefinement::default()
 704            .font_weight(FontWeight::BOLD)
 705            .text_base()
 706            .mt(rems(1.))
 707            .mb_0(),
 708        table_columns_min_size: true,
 709        ..Default::default()
 710    }
 711}
 712
 713pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 714    let settings = ThemeSettings::get_global(cx);
 715    let ui_font_family = settings.ui_font.family.clone();
 716    let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
 717    let ui_font_features = settings.ui_font.features.clone();
 718    let buffer_font_family = settings.buffer_font.family.clone();
 719    let buffer_font_features = settings.buffer_font.features.clone();
 720    let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
 721
 722    let mut base_text_style = window.text_style();
 723    base_text_style.refine(&TextStyleRefinement {
 724        font_family: Some(ui_font_family),
 725        font_features: Some(ui_font_features),
 726        font_fallbacks: ui_font_fallbacks,
 727        color: Some(cx.theme().colors().editor_foreground),
 728        ..Default::default()
 729    });
 730    MarkdownStyle {
 731        base_text_style,
 732        code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
 733        inline_code: TextStyleRefinement {
 734            background_color: Some(cx.theme().colors().editor_background.opacity(0.5)),
 735            font_family: Some(buffer_font_family),
 736            font_features: Some(buffer_font_features),
 737            font_fallbacks: buffer_font_fallbacks,
 738            ..Default::default()
 739        },
 740        rule_color: cx.theme().colors().border,
 741        block_quote_border_color: Color::Muted.color(cx),
 742        block_quote: TextStyleRefinement {
 743            color: Some(Color::Muted.color(cx)),
 744            ..Default::default()
 745        },
 746        link: TextStyleRefinement {
 747            color: Some(cx.theme().colors().editor_foreground),
 748            underline: Some(gpui::UnderlineStyle {
 749                thickness: px(1.),
 750                color: Some(cx.theme().colors().editor_foreground),
 751                wavy: false,
 752            }),
 753            ..Default::default()
 754        },
 755        syntax: cx.theme().syntax().clone(),
 756        selection_background_color: cx.theme().colors().element_selection_background,
 757        height_is_multiple_of_line_height: true,
 758        heading: StyleRefinement::default()
 759            .font_weight(FontWeight::BOLD)
 760            .text_base()
 761            .mb_0(),
 762        table_columns_min_size: true,
 763        ..Default::default()
 764    }
 765}
 766
 767pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
 768    if let Ok(uri) = Url::parse(&link)
 769        && uri.scheme() == "file"
 770        && let Some(workspace) = Workspace::for_window(window, cx)
 771    {
 772        workspace.update(cx, |workspace, cx| {
 773            let task = workspace.open_abs_path(
 774                PathBuf::from(uri.path()),
 775                OpenOptions {
 776                    visible: Some(OpenVisible::None),
 777                    ..Default::default()
 778                },
 779                window,
 780                cx,
 781            );
 782
 783            cx.spawn_in(window, async move |_, cx| {
 784                let item = task.await?;
 785                // Ruby LSP uses URLs with #L1,1-4,4
 786                // we'll just take the first number and assume it's a line number
 787                let Some(fragment) = uri.fragment() else {
 788                    return anyhow::Ok(());
 789                };
 790                let mut accum = 0u32;
 791                for c in fragment.chars() {
 792                    if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
 793                        accum *= 10;
 794                        accum += c as u32 - '0' as u32;
 795                    } else if accum > 0 {
 796                        break;
 797                    }
 798                }
 799                if accum == 0 {
 800                    return Ok(());
 801                }
 802                let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
 803                    return Ok(());
 804                };
 805                editor.update_in(cx, |editor, window, cx| {
 806                    editor.change_selections(Default::default(), window, cx, |selections| {
 807                        selections.select_ranges([
 808                            text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0)
 809                        ]);
 810                    });
 811                })
 812            })
 813            .detach_and_log_err(cx);
 814        });
 815        return;
 816    }
 817    cx.open_url(&link);
 818}
 819
 820#[derive(Default)]
 821pub struct HoverState {
 822    pub info_popovers: Vec<InfoPopover>,
 823    pub diagnostic_popover: Option<DiagnosticPopover>,
 824    pub triggered_from: Option<Anchor>,
 825    pub info_task: Option<Task<Option<()>>>,
 826    pub closest_mouse_distance: Option<Pixels>,
 827    pub hiding_delay_task: Option<Task<()>>,
 828}
 829
 830impl HoverState {
 831    pub fn visible(&self) -> bool {
 832        !self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
 833    }
 834
 835    pub fn is_mouse_getting_closer(&mut self, mouse_position: gpui::Point<Pixels>) -> bool {
 836        if !self.visible() {
 837            return false;
 838        }
 839
 840        let mut popover_bounds = Vec::new();
 841        for info_popover in &self.info_popovers {
 842            if let Some(bounds) = info_popover.last_bounds.get() {
 843                popover_bounds.push(bounds);
 844            }
 845        }
 846        if let Some(diagnostic_popover) = &self.diagnostic_popover {
 847            if let Some(bounds) = diagnostic_popover.last_bounds.get() {
 848                popover_bounds.push(bounds);
 849            }
 850        }
 851
 852        if popover_bounds.is_empty() {
 853            return false;
 854        }
 855
 856        let distance = popover_bounds
 857            .iter()
 858            .map(|bounds| self.distance_from_point_to_bounds(mouse_position, *bounds))
 859            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
 860            .unwrap_or(px(f32::MAX));
 861
 862        if let Some(closest_distance) = self.closest_mouse_distance {
 863            if distance > closest_distance + px(4.0) {
 864                return false;
 865            }
 866        }
 867
 868        self.closest_mouse_distance =
 869            Some(distance.min(self.closest_mouse_distance.unwrap_or(distance)));
 870        true
 871    }
 872
 873    fn distance_from_point_to_bounds(
 874        &self,
 875        point: gpui::Point<Pixels>,
 876        bounds: Bounds<Pixels>,
 877    ) -> Pixels {
 878        let center_x = bounds.origin.x + bounds.size.width / 2.;
 879        let center_y = bounds.origin.y + bounds.size.height / 2.;
 880        let dx: f32 = ((point.x - center_x).abs() - bounds.size.width / 2.)
 881            .max(px(0.0))
 882            .into();
 883        let dy: f32 = ((point.y - center_y).abs() - bounds.size.height / 2.)
 884            .max(px(0.0))
 885            .into();
 886        px((dx.powi(2) + dy.powi(2)).sqrt())
 887    }
 888
 889    pub(crate) fn render(
 890        &mut self,
 891        snapshot: &EditorSnapshot,
 892        visible_rows: Range<DisplayRow>,
 893        max_size: Size<Pixels>,
 894        text_layout_details: &TextLayoutDetails,
 895        window: &mut Window,
 896        cx: &mut Context<Editor>,
 897    ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
 898        if !self.visible() {
 899            return None;
 900        }
 901        // If there is a diagnostic, position the popovers based on that.
 902        // Otherwise use the start of the hover range
 903        let anchor = self
 904            .diagnostic_popover
 905            .as_ref()
 906            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
 907            .or_else(|| {
 908                self.info_popovers.iter().find_map(|info_popover| {
 909                    match &info_popover.symbol_range {
 910                        RangeInEditor::Text(range) => Some(&range.start),
 911                        RangeInEditor::Inlay(_) => None,
 912                    }
 913                })
 914            })
 915            .or_else(|| {
 916                self.info_popovers.iter().find_map(|info_popover| {
 917                    match &info_popover.symbol_range {
 918                        RangeInEditor::Text(_) => None,
 919                        RangeInEditor::Inlay(range) => Some(&range.inlay_position),
 920                    }
 921                })
 922            })?;
 923        let mut point = anchor.to_display_point(&snapshot.display_snapshot);
 924        // Clamp the point within the visible rows in case the popup source spans multiple lines
 925        if visible_rows.end <= point.row() {
 926            point = crate::movement::up_by_rows(
 927                &snapshot.display_snapshot,
 928                point,
 929                1 + (point.row() - visible_rows.end).0,
 930                text::SelectionGoal::None,
 931                true,
 932                text_layout_details,
 933            )
 934            .0;
 935        } else if point.row() < visible_rows.start {
 936            point = crate::movement::down_by_rows(
 937                &snapshot.display_snapshot,
 938                point,
 939                (visible_rows.start - point.row()).0,
 940                text::SelectionGoal::None,
 941                true,
 942                text_layout_details,
 943            )
 944            .0;
 945        }
 946
 947        if !visible_rows.contains(&point.row()) {
 948            log::error!("Hover popover point out of bounds after moving");
 949            return None;
 950        }
 951
 952        let mut elements = Vec::new();
 953
 954        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
 955            elements.push(diagnostic_popover.render(max_size, window, cx));
 956        }
 957        for info_popover in &mut self.info_popovers {
 958            elements.push(info_popover.render(max_size, window, cx));
 959        }
 960
 961        Some((point, elements))
 962    }
 963
 964    pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
 965        let mut hover_popover_is_focused = false;
 966        for info_popover in &self.info_popovers {
 967            if let Some(markdown_view) = &info_popover.parsed_content
 968                && markdown_view.focus_handle(cx).is_focused(window)
 969            {
 970                hover_popover_is_focused = true;
 971            }
 972        }
 973        if let Some(diagnostic_popover) = &self.diagnostic_popover
 974            && diagnostic_popover
 975                .markdown
 976                .focus_handle(cx)
 977                .is_focused(window)
 978        {
 979            hover_popover_is_focused = true;
 980        }
 981        hover_popover_is_focused
 982    }
 983}
 984
 985pub struct InfoPopover {
 986    pub symbol_range: RangeInEditor,
 987    pub parsed_content: Option<Entity<Markdown>>,
 988    pub scroll_handle: ScrollHandle,
 989    pub keyboard_grace: Rc<RefCell<bool>>,
 990    pub anchor: Option<Anchor>,
 991    pub last_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
 992    _subscription: Option<Subscription>,
 993}
 994
 995impl InfoPopover {
 996    pub(crate) fn render(
 997        &mut self,
 998        max_size: Size<Pixels>,
 999        window: &mut Window,
1000        cx: &mut Context<Editor>,
1001    ) -> AnyElement {
1002        let keyboard_grace = Rc::clone(&self.keyboard_grace);
1003        let this = cx.entity().downgrade();
1004        let bounds_cell = self.last_bounds.clone();
1005        div()
1006            .id("info_popover")
1007            .occlude()
1008            .elevation_2(cx)
1009            .child(
1010                canvas(
1011                    {
1012                        move |bounds, _window, _cx| {
1013                            bounds_cell.set(Some(bounds));
1014                        }
1015                    },
1016                    |_, _, _, _| {},
1017                )
1018                .absolute()
1019                .size_full(),
1020            )
1021            // Prevent a mouse down/move on the popover from being propagated to the editor,
1022            // because that would dismiss the popover.
1023            .on_mouse_move({
1024                move |_, _, cx: &mut App| {
1025                    this.update(cx, |editor, _| {
1026                        editor.hover_state.closest_mouse_distance = Some(px(0.0));
1027                        editor.hover_state.hiding_delay_task = None;
1028                    })
1029                    .ok();
1030                    cx.stop_propagation()
1031                }
1032            })
1033            .on_mouse_down(MouseButton::Left, move |_, _, cx| {
1034                let mut keyboard_grace = keyboard_grace.borrow_mut();
1035                *keyboard_grace = false;
1036                cx.stop_propagation();
1037            })
1038            .when_some(self.parsed_content.clone(), |this, markdown| {
1039                this.child(
1040                    div()
1041                        .id("info-md-container")
1042                        .overflow_y_scroll()
1043                        .max_w(max_size.width)
1044                        .max_h(max_size.height)
1045                        .track_scroll(&self.scroll_handle)
1046                        .child(
1047                            MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1048                                .code_block_renderer(markdown::CodeBlockRenderer::Default {
1049                                    copy_button_visibility: CopyButtonVisibility::Hidden,
1050                                    border: false,
1051                                })
1052                                .on_url_click(open_markdown_url)
1053                                .p_2(),
1054                        ),
1055                )
1056                .custom_scrollbars(
1057                    Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1058                        .tracked_scroll_handle(&self.scroll_handle),
1059                    window,
1060                    cx,
1061                )
1062            })
1063            .into_any_element()
1064    }
1065
1066    pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
1067        let mut current = self.scroll_handle.offset();
1068        current.y -= amount.pixels(
1069            window.line_height(),
1070            self.scroll_handle.bounds().size.height - px(16.),
1071        ) / 2.0;
1072        cx.notify();
1073        self.scroll_handle.set_offset(current);
1074    }
1075}
1076
1077pub struct DiagnosticPopover {
1078    pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
1079    markdown: Entity<Markdown>,
1080    border_color: Hsla,
1081    background_color: Hsla,
1082    pub keyboard_grace: Rc<RefCell<bool>>,
1083    pub anchor: Anchor,
1084    pub last_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
1085    _subscription: Subscription,
1086    pub scroll_handle: ScrollHandle,
1087}
1088
1089impl DiagnosticPopover {
1090    pub fn render(
1091        &self,
1092        max_size: Size<Pixels>,
1093        window: &mut Window,
1094        cx: &mut Context<Editor>,
1095    ) -> AnyElement {
1096        let keyboard_grace = Rc::clone(&self.keyboard_grace);
1097        let this = cx.entity().downgrade();
1098        let bounds_cell = self.last_bounds.clone();
1099        div()
1100            .id("diagnostic")
1101            .occlude()
1102            .elevation_2_borderless(cx)
1103            .child(
1104                canvas(
1105                    {
1106                        move |bounds, _window, _cx| {
1107                            bounds_cell.set(Some(bounds));
1108                        }
1109                    },
1110                    |_, _, _, _| {},
1111                )
1112                .absolute()
1113                .size_full(),
1114            )
1115            // Don't draw the background color if the theme
1116            // allows transparent surfaces.
1117            .when(theme_is_transparent(cx), |this| {
1118                this.bg(gpui::transparent_black())
1119            })
1120            // Prevent a mouse move on the popover from being propagated to the editor,
1121            // because that would dismiss the popover.
1122            .on_mouse_move({
1123                let this = this.clone();
1124                move |_, _, cx: &mut App| {
1125                    this.update(cx, |editor, _| {
1126                        editor.hover_state.closest_mouse_distance = Some(px(0.0));
1127                        editor.hover_state.hiding_delay_task = None;
1128                    })
1129                    .ok();
1130                    cx.stop_propagation()
1131                }
1132            })
1133            // Prevent a mouse down on the popover from being propagated to the editor,
1134            // because that would move the cursor.
1135            .on_mouse_down(MouseButton::Left, move |_, _, cx| {
1136                let mut keyboard_grace = keyboard_grace.borrow_mut();
1137                *keyboard_grace = false;
1138                cx.stop_propagation();
1139            })
1140            .child(
1141                div()
1142                    .relative()
1143                    .py_1()
1144                    .pl_2()
1145                    .pr_8()
1146                    .bg(self.background_color)
1147                    .border_1()
1148                    .border_color(self.border_color)
1149                    .rounded_lg()
1150                    .child(
1151                        div()
1152                            .id("diagnostic-content-container")
1153                            .max_w(max_size.width)
1154                            .max_h(max_size.height)
1155                            .overflow_y_scroll()
1156                            .track_scroll(&self.scroll_handle)
1157                            .child(
1158                                MarkdownElement::new(
1159                                    self.markdown.clone(),
1160                                    diagnostics_markdown_style(window, cx),
1161                                )
1162                                .code_block_renderer(markdown::CodeBlockRenderer::Default {
1163                                    copy_button_visibility: CopyButtonVisibility::Hidden,
1164                                    border: false,
1165                                })
1166                                .on_url_click(
1167                                    move |link, window, cx| {
1168                                        if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
1169                                        {
1170                                            this.update(cx, |this, cx| {
1171                                                renderer.as_ref().open_link(this, link, window, cx);
1172                                            })
1173                                            .ok();
1174                                        }
1175                                    },
1176                                ),
1177                            ),
1178                    )
1179                    .child(div().absolute().top_1().right_1().child({
1180                        let message = self.local_diagnostic.diagnostic.message.clone();
1181                        CopyButton::new("copy-diagnostic", message).tooltip_label("Copy Diagnostic")
1182                    }))
1183                    .custom_scrollbars(
1184                        Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1185                            .tracked_scroll_handle(&self.scroll_handle),
1186                        window,
1187                        cx,
1188                    ),
1189            )
1190            .into_any_element()
1191    }
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196    use super::*;
1197    use crate::{
1198        PointForPosition,
1199        actions::ConfirmCompletion,
1200        editor_tests::{handle_completion_request, init_test},
1201        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
1202        test::editor_lsp_test_context::EditorLspTestContext,
1203    };
1204    use collections::BTreeSet;
1205    use gpui::App;
1206    use indoc::indoc;
1207    use markdown::parser::MarkdownEvent;
1208    use project::InlayId;
1209    use settings::InlayHintSettingsContent;
1210    use settings::{DelayMs, SettingsStore};
1211    use smol::stream::StreamExt;
1212    use std::sync::atomic;
1213    use std::sync::atomic::AtomicUsize;
1214    use text::Bias;
1215
1216    fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
1217        cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 })
1218    }
1219
1220    impl InfoPopover {
1221        fn get_rendered_text(&self, cx: &gpui::App) -> String {
1222            let mut rendered_text = String::new();
1223            if let Some(parsed_content) = self.parsed_content.clone() {
1224                let markdown = parsed_content.read(cx);
1225                let text = markdown.parsed_markdown().source().to_string();
1226                let data = markdown.parsed_markdown().events();
1227                let slice = data;
1228
1229                for (range, event) in slice.iter() {
1230                    match event {
1231                        MarkdownEvent::SubstitutedText(parsed) => {
1232                            rendered_text.push_str(parsed.as_str())
1233                        }
1234                        MarkdownEvent::Text | MarkdownEvent::Code => {
1235                            rendered_text.push_str(&text[range.clone()])
1236                        }
1237                        _ => {}
1238                    }
1239                }
1240            }
1241            rendered_text
1242        }
1243    }
1244
1245    #[gpui::test]
1246    async fn test_mouse_hover_info_popover_with_autocomplete_popover(
1247        cx: &mut gpui::TestAppContext,
1248    ) {
1249        init_test(cx, |_| {});
1250
1251        let mut cx = EditorLspTestContext::new_rust(
1252            lsp::ServerCapabilities {
1253                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1254                completion_provider: Some(lsp::CompletionOptions {
1255                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
1256                    resolve_provider: Some(true),
1257                    ..Default::default()
1258                }),
1259                ..Default::default()
1260            },
1261            cx,
1262        )
1263        .await;
1264        let counter = Arc::new(AtomicUsize::new(0));
1265        // Basic hover delays and then pops without moving the mouse
1266        cx.set_state(indoc! {"
1267                oneˇ
1268                two
1269                three
1270                fn test() { println!(); }
1271            "});
1272
1273        //prompt autocompletion menu
1274        cx.simulate_keystroke(".");
1275        handle_completion_request(
1276            indoc! {"
1277                        one.|<>
1278                        two
1279                        three
1280                    "},
1281            vec!["first_completion", "second_completion"],
1282            true,
1283            counter.clone(),
1284            &mut cx,
1285        )
1286        .await;
1287        cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
1288            .await;
1289        assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
1290
1291        let hover_point = cx.display_point(indoc! {"
1292                one.
1293                two
1294                three
1295                fn test() { printˇln!(); }
1296            "});
1297        cx.update_editor(|editor, window, cx| {
1298            let snapshot = editor.snapshot(window, cx);
1299            let anchor = snapshot
1300                .buffer_snapshot()
1301                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1302            hover_at(editor, Some(anchor), None, window, cx)
1303        });
1304        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1305
1306        // After delay, hover should be visible.
1307        let symbol_range = cx.lsp_range(indoc! {"
1308                one.
1309                two
1310                three
1311                fn test() { «println!»(); }
1312            "});
1313        let mut requests =
1314            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1315                Ok(Some(lsp::Hover {
1316                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1317                        kind: lsp::MarkupKind::Markdown,
1318                        value: "some basic docs".to_string(),
1319                    }),
1320                    range: Some(symbol_range),
1321                }))
1322            });
1323        cx.background_executor
1324            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1325        requests.next().await;
1326
1327        cx.editor(|editor, _window, cx| {
1328            assert!(editor.hover_state.visible());
1329            assert_eq!(
1330                editor.hover_state.info_popovers.len(),
1331                1,
1332                "Expected exactly one hover but got: {:?}",
1333                editor.hover_state.info_popovers.len()
1334            );
1335            let rendered_text = editor
1336                .hover_state
1337                .info_popovers
1338                .first()
1339                .unwrap()
1340                .get_rendered_text(cx);
1341            assert_eq!(rendered_text, "some basic docs".to_string())
1342        });
1343
1344        // check that the completion menu is still visible and that there still has only been 1 completion request
1345        cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
1346        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1347
1348        //apply a completion and check it was successfully applied
1349        let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1350            editor.context_menu_next(&Default::default(), window, cx);
1351            editor
1352                .confirm_completion(&ConfirmCompletion::default(), window, cx)
1353                .unwrap()
1354        });
1355        cx.assert_editor_state(indoc! {"
1356            one.second_completionˇ
1357            two
1358            three
1359            fn test() { println!(); }
1360        "});
1361
1362        // check that the completion menu is no longer visible and that there still has only been 1 completion request
1363        cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1364        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1365
1366        //verify the information popover is still visible and unchanged
1367        cx.editor(|editor, _, cx| {
1368            assert!(editor.hover_state.visible());
1369            assert_eq!(
1370                editor.hover_state.info_popovers.len(),
1371                1,
1372                "Expected exactly one hover but got: {:?}",
1373                editor.hover_state.info_popovers.len()
1374            );
1375            let rendered_text = editor
1376                .hover_state
1377                .info_popovers
1378                .first()
1379                .unwrap()
1380                .get_rendered_text(cx);
1381
1382            assert_eq!(rendered_text, "some basic docs".to_string())
1383        });
1384
1385        // Mouse moved with no hover response dismisses
1386        let hover_point = cx.display_point(indoc! {"
1387                one.second_completionˇ
1388                two
1389                three
1390                fn teˇst() { println!(); }
1391            "});
1392        let mut request = cx
1393            .lsp
1394            .set_request_handler::<lsp::request::HoverRequest, _, _>(
1395                |_, _| async move { Ok(None) },
1396            );
1397        cx.update_editor(|editor, window, cx| {
1398            let snapshot = editor.snapshot(window, cx);
1399            let anchor = snapshot
1400                .buffer_snapshot()
1401                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1402            hover_at(editor, Some(anchor), None, window, cx)
1403        });
1404        cx.background_executor
1405            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1406        request.next().await;
1407
1408        // verify that the information popover is no longer visible
1409        cx.editor(|editor, _, _| {
1410            assert!(!editor.hover_state.visible());
1411        });
1412    }
1413
1414    #[gpui::test]
1415    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1416        init_test(cx, |_| {});
1417
1418        let mut cx = EditorLspTestContext::new_rust(
1419            lsp::ServerCapabilities {
1420                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1421                ..Default::default()
1422            },
1423            cx,
1424        )
1425        .await;
1426
1427        // Basic hover delays and then pops without moving the mouse
1428        cx.set_state(indoc! {"
1429            fn ˇtest() { println!(); }
1430        "});
1431        let hover_point = cx.display_point(indoc! {"
1432            fn test() { printˇln!(); }
1433        "});
1434
1435        cx.update_editor(|editor, window, cx| {
1436            let snapshot = editor.snapshot(window, cx);
1437            let anchor = snapshot
1438                .buffer_snapshot()
1439                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1440            hover_at(editor, Some(anchor), None, window, cx)
1441        });
1442        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1443
1444        // After delay, hover should be visible.
1445        let symbol_range = cx.lsp_range(indoc! {"
1446            fn test() { «println!»(); }
1447        "});
1448        let mut requests =
1449            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1450                Ok(Some(lsp::Hover {
1451                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1452                        kind: lsp::MarkupKind::Markdown,
1453                        value: "some basic docs".to_string(),
1454                    }),
1455                    range: Some(symbol_range),
1456                }))
1457            });
1458        cx.background_executor
1459            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1460        requests.next().await;
1461
1462        cx.editor(|editor, _, cx| {
1463            assert!(editor.hover_state.visible());
1464            assert_eq!(
1465                editor.hover_state.info_popovers.len(),
1466                1,
1467                "Expected exactly one hover but got: {:?}",
1468                editor.hover_state.info_popovers.len()
1469            );
1470            let rendered_text = editor
1471                .hover_state
1472                .info_popovers
1473                .first()
1474                .unwrap()
1475                .get_rendered_text(cx);
1476
1477            assert_eq!(rendered_text, "some basic docs".to_string())
1478        });
1479
1480        // Mouse moved with no hover response dismisses
1481        let hover_point = cx.display_point(indoc! {"
1482            fn teˇst() { println!(); }
1483        "});
1484        let mut request = cx
1485            .lsp
1486            .set_request_handler::<lsp::request::HoverRequest, _, _>(
1487                |_, _| async move { Ok(None) },
1488            );
1489        cx.update_editor(|editor, window, cx| {
1490            let snapshot = editor.snapshot(window, cx);
1491            let anchor = snapshot
1492                .buffer_snapshot()
1493                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1494            hover_at(editor, Some(anchor), None, window, cx)
1495        });
1496        cx.background_executor
1497            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1498        request.next().await;
1499        cx.editor(|editor, _, _| {
1500            assert!(!editor.hover_state.visible());
1501        });
1502    }
1503
1504    #[gpui::test]
1505    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1506        init_test(cx, |_| {});
1507
1508        let mut cx = EditorLspTestContext::new_rust(
1509            lsp::ServerCapabilities {
1510                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1511                ..Default::default()
1512            },
1513            cx,
1514        )
1515        .await;
1516
1517        // Hover with keyboard has no delay
1518        cx.set_state(indoc! {"
1519            fˇn test() { println!(); }
1520        "});
1521        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1522        let symbol_range = cx.lsp_range(indoc! {"
1523            «fn» test() { println!(); }
1524        "});
1525
1526        cx.editor(|editor, _window, _cx| {
1527            assert!(!editor.hover_state.visible());
1528
1529            assert_eq!(
1530                editor.hover_state.info_popovers.len(),
1531                0,
1532                "Expected no hovers but got but got: {:?}",
1533                editor.hover_state.info_popovers.len()
1534            );
1535        });
1536
1537        let mut requests =
1538            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1539                Ok(Some(lsp::Hover {
1540                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1541                        kind: lsp::MarkupKind::Markdown,
1542                        value: "some other basic docs".to_string(),
1543                    }),
1544                    range: Some(symbol_range),
1545                }))
1546            });
1547
1548        requests.next().await;
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
1560            let rendered_text = editor
1561                .hover_state
1562                .info_popovers
1563                .first()
1564                .unwrap()
1565                .get_rendered_text(cx);
1566
1567            assert_eq!(rendered_text, "some other basic docs".to_string())
1568        });
1569    }
1570
1571    #[gpui::test]
1572    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1573        init_test(cx, |_| {});
1574
1575        let mut cx = EditorLspTestContext::new_rust(
1576            lsp::ServerCapabilities {
1577                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1578                ..Default::default()
1579            },
1580            cx,
1581        )
1582        .await;
1583
1584        // Hover with keyboard has no delay
1585        cx.set_state(indoc! {"
1586            fˇn test() { println!(); }
1587        "});
1588        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1589        let symbol_range = cx.lsp_range(indoc! {"
1590            «fn» test() { println!(); }
1591        "});
1592        cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1593            Ok(Some(lsp::Hover {
1594                contents: lsp::HoverContents::Array(vec![
1595                    lsp::MarkedString::String("regular text for hover to show".to_string()),
1596                    lsp::MarkedString::String("".to_string()),
1597                    lsp::MarkedString::LanguageString(lsp::LanguageString {
1598                        language: "Rust".to_string(),
1599                        value: "".to_string(),
1600                    }),
1601                ]),
1602                range: Some(symbol_range),
1603            }))
1604        })
1605        .next()
1606        .await;
1607        cx.dispatch_action(Hover);
1608
1609        cx.condition(|editor, _| editor.hover_state.visible()).await;
1610        cx.editor(|editor, _, cx| {
1611            assert_eq!(
1612                editor.hover_state.info_popovers.len(),
1613                1,
1614                "Expected exactly one hover but got: {:?}",
1615                editor.hover_state.info_popovers.len()
1616            );
1617            let rendered_text = editor
1618                .hover_state
1619                .info_popovers
1620                .first()
1621                .unwrap()
1622                .get_rendered_text(cx);
1623
1624            assert_eq!(
1625                rendered_text,
1626                "regular text for hover to show".to_string(),
1627                "No empty string hovers should be shown"
1628            );
1629        });
1630    }
1631
1632    #[gpui::test]
1633    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1634        init_test(cx, |_| {});
1635
1636        let mut cx = EditorLspTestContext::new_rust(
1637            lsp::ServerCapabilities {
1638                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1639                ..Default::default()
1640            },
1641            cx,
1642        )
1643        .await;
1644
1645        // Hover with keyboard has no delay
1646        cx.set_state(indoc! {"
1647            fˇn test() { println!(); }
1648        "});
1649        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1650        let symbol_range = cx.lsp_range(indoc! {"
1651            «fn» test() { println!(); }
1652        "});
1653
1654        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1655        let markdown_string = format!("\n```rust\n{code_str}```");
1656
1657        let closure_markdown_string = markdown_string.clone();
1658        cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1659            let future_markdown_string = closure_markdown_string.clone();
1660            async move {
1661                Ok(Some(lsp::Hover {
1662                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1663                        kind: lsp::MarkupKind::Markdown,
1664                        value: future_markdown_string,
1665                    }),
1666                    range: Some(symbol_range),
1667                }))
1668            }
1669        })
1670        .next()
1671        .await;
1672
1673        cx.dispatch_action(Hover);
1674
1675        cx.condition(|editor, _| editor.hover_state.visible()).await;
1676        cx.editor(|editor, _, cx| {
1677            assert_eq!(
1678                editor.hover_state.info_popovers.len(),
1679                1,
1680                "Expected exactly one hover but got: {:?}",
1681                editor.hover_state.info_popovers.len()
1682            );
1683            let rendered_text = editor
1684                .hover_state
1685                .info_popovers
1686                .first()
1687                .unwrap()
1688                .get_rendered_text(cx);
1689
1690            assert_eq!(
1691                rendered_text, code_str,
1692                "Should not have extra line breaks at end of rendered hover"
1693            );
1694        });
1695    }
1696
1697    #[gpui::test]
1698    // https://github.com/zed-industries/zed/issues/15498
1699    async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1700        init_test(cx, |_| {});
1701
1702        let mut cx = EditorLspTestContext::new_rust(
1703            lsp::ServerCapabilities {
1704                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1705                ..Default::default()
1706            },
1707            cx,
1708        )
1709        .await;
1710
1711        cx.set_state(indoc! {"
1712            fn fuˇnc(abc def: i32) -> u32 {
1713            }
1714        "});
1715
1716        cx.lsp
1717            .set_request_handler::<lsp::request::HoverRequest, _, _>({
1718                |_, _| async move {
1719                    Ok(Some(lsp::Hover {
1720                        contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1721                            kind: lsp::MarkupKind::Markdown,
1722                            value: indoc!(
1723                                r#"
1724                    ### function `errands_data_read`
1725
1726                    ---
1727                    → `char *`
1728                    Function to read a file into a string
1729
1730                    ---
1731                    ```cpp
1732                    static char *errands_data_read()
1733                    ```
1734                    "#
1735                            )
1736                            .to_string(),
1737                        }),
1738                        range: None,
1739                    }))
1740                }
1741            });
1742        cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1743        cx.run_until_parked();
1744
1745        cx.update_editor(|editor, _, cx| {
1746            let popover = editor.hover_state.info_popovers.first().unwrap();
1747            let content = popover.get_rendered_text(cx);
1748
1749            assert!(content.contains("Function to read a file"));
1750        });
1751    }
1752
1753    #[gpui::test]
1754    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1755        init_test(cx, |settings| {
1756            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1757                show_value_hints: Some(true),
1758                enabled: Some(true),
1759                edit_debounce_ms: Some(0),
1760                scroll_debounce_ms: Some(0),
1761                show_type_hints: Some(true),
1762                show_parameter_hints: Some(true),
1763                show_other_hints: Some(true),
1764                show_background: Some(false),
1765                toggle_on_modifiers_press: None,
1766            })
1767        });
1768
1769        let mut cx = EditorLspTestContext::new_rust(
1770            lsp::ServerCapabilities {
1771                inlay_hint_provider: Some(lsp::OneOf::Right(
1772                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1773                        resolve_provider: Some(true),
1774                        ..Default::default()
1775                    }),
1776                )),
1777                ..Default::default()
1778            },
1779            cx,
1780        )
1781        .await;
1782
1783        cx.set_state(indoc! {"
1784            struct TestStruct;
1785
1786            // ==================
1787
1788            struct TestNewType<T>(T);
1789
1790            fn main() {
1791                let variableˇ = TestNewType(TestStruct);
1792            }
1793        "});
1794
1795        let hint_start_offset = cx.ranges(indoc! {"
1796            struct TestStruct;
1797
1798            // ==================
1799
1800            struct TestNewType<T>(T);
1801
1802            fn main() {
1803                let variableˇ = TestNewType(TestStruct);
1804            }
1805        "})[0]
1806            .start;
1807        let hint_position = cx.to_lsp(MultiBufferOffset(hint_start_offset));
1808        let new_type_target_range = cx.lsp_range(indoc! {"
1809            struct TestStruct;
1810
1811            // ==================
1812
1813            struct «TestNewType»<T>(T);
1814
1815            fn main() {
1816                let variable = TestNewType(TestStruct);
1817            }
1818        "});
1819        let struct_target_range = cx.lsp_range(indoc! {"
1820            struct «TestStruct»;
1821
1822            // ==================
1823
1824            struct TestNewType<T>(T);
1825
1826            fn main() {
1827                let variable = TestNewType(TestStruct);
1828            }
1829        "});
1830
1831        let uri = cx.buffer_lsp_url.clone();
1832        let new_type_label = "TestNewType";
1833        let struct_label = "TestStruct";
1834        let entire_hint_label = ": TestNewType<TestStruct>";
1835        let closure_uri = uri.clone();
1836        cx.lsp
1837            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1838                let task_uri = closure_uri.clone();
1839                async move {
1840                    assert_eq!(params.text_document.uri, task_uri);
1841                    Ok(Some(vec![lsp::InlayHint {
1842                        position: hint_position,
1843                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1844                            value: entire_hint_label.to_string(),
1845                            ..Default::default()
1846                        }]),
1847                        kind: Some(lsp::InlayHintKind::TYPE),
1848                        text_edits: None,
1849                        tooltip: None,
1850                        padding_left: Some(false),
1851                        padding_right: Some(false),
1852                        data: None,
1853                    }]))
1854                }
1855            })
1856            .next()
1857            .await;
1858        cx.background_executor.run_until_parked();
1859        cx.update_editor(|editor, _, cx| {
1860            let expected_layers = vec![entire_hint_label.to_string()];
1861            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
1862            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1863        });
1864
1865        let inlay_range = cx
1866            .ranges(indoc! {"
1867                struct TestStruct;
1868
1869                // ==================
1870
1871                struct TestNewType<T>(T);
1872
1873                fn main() {
1874                    let variable« »= TestNewType(TestStruct);
1875                }
1876        "})
1877            .first()
1878            .cloned()
1879            .unwrap();
1880        let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1881            let snapshot = editor.snapshot(window, cx);
1882            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
1883            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
1884            assert_eq!(previous_valid.row(), next_valid.row());
1885            assert!(previous_valid.column() < next_valid.column());
1886            let exact_unclipped = DisplayPoint::new(
1887                previous_valid.row(),
1888                previous_valid.column()
1889                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1890                        as u32,
1891            );
1892            PointForPosition {
1893                previous_valid,
1894                next_valid,
1895                exact_unclipped,
1896                column_overshoot_after_line_end: 0,
1897            }
1898        });
1899        cx.update_editor(|editor, window, cx| {
1900            editor.update_inlay_link_and_hover_points(
1901                &editor.snapshot(window, cx),
1902                new_type_hint_part_hover_position,
1903                None,
1904                true,
1905                false,
1906                window,
1907                cx,
1908            );
1909        });
1910
1911        let resolve_closure_uri = uri.clone();
1912        cx.lsp
1913            .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
1914                move |mut hint_to_resolve, _| {
1915                    let mut resolved_hint_positions = BTreeSet::new();
1916                    let task_uri = resolve_closure_uri.clone();
1917                    async move {
1918                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1919                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1920
1921                        // `: TestNewType<TestStruct>`
1922                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1923                            lsp::InlayHintLabelPart {
1924                                value: ": ".to_string(),
1925                                ..Default::default()
1926                            },
1927                            lsp::InlayHintLabelPart {
1928                                value: new_type_label.to_string(),
1929                                location: Some(lsp::Location {
1930                                    uri: task_uri.clone(),
1931                                    range: new_type_target_range,
1932                                }),
1933                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1934                                    "A tooltip for `{new_type_label}`"
1935                                ))),
1936                                ..Default::default()
1937                            },
1938                            lsp::InlayHintLabelPart {
1939                                value: "<".to_string(),
1940                                ..Default::default()
1941                            },
1942                            lsp::InlayHintLabelPart {
1943                                value: struct_label.to_string(),
1944                                location: Some(lsp::Location {
1945                                    uri: task_uri,
1946                                    range: struct_target_range,
1947                                }),
1948                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1949                                    lsp::MarkupContent {
1950                                        kind: lsp::MarkupKind::Markdown,
1951                                        value: format!("A tooltip for `{struct_label}`"),
1952                                    },
1953                                )),
1954                                ..Default::default()
1955                            },
1956                            lsp::InlayHintLabelPart {
1957                                value: ">".to_string(),
1958                                ..Default::default()
1959                            },
1960                        ]);
1961
1962                        Ok(hint_to_resolve)
1963                    }
1964                },
1965            )
1966            .next()
1967            .await;
1968        cx.background_executor.run_until_parked();
1969
1970        cx.update_editor(|editor, window, cx| {
1971            editor.update_inlay_link_and_hover_points(
1972                &editor.snapshot(window, cx),
1973                new_type_hint_part_hover_position,
1974                None,
1975                true,
1976                false,
1977                window,
1978                cx,
1979            );
1980        });
1981        cx.background_executor
1982            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1983        cx.background_executor.run_until_parked();
1984        cx.update_editor(|editor, _, cx| {
1985            let hover_state = &editor.hover_state;
1986            assert!(
1987                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1988            );
1989            let popover = hover_state.info_popovers.first().unwrap();
1990            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1991            assert_eq!(
1992                popover.symbol_range,
1993                RangeInEditor::Inlay(InlayHighlight {
1994                    inlay: InlayId::Hint(0),
1995                    inlay_position: buffer_snapshot
1996                        .anchor_after(MultiBufferOffset(inlay_range.start)),
1997                    range: ": ".len()..": ".len() + new_type_label.len(),
1998                }),
1999                "Popover range should match the new type label part"
2000            );
2001            assert_eq!(
2002                popover.get_rendered_text(cx),
2003                format!("A tooltip for {new_type_label}"),
2004            );
2005        });
2006
2007        let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
2008            let snapshot = editor.snapshot(window, cx);
2009            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
2010            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
2011            assert_eq!(previous_valid.row(), next_valid.row());
2012            assert!(previous_valid.column() < next_valid.column());
2013            let exact_unclipped = DisplayPoint::new(
2014                previous_valid.row(),
2015                previous_valid.column()
2016                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
2017                        as u32,
2018            );
2019            PointForPosition {
2020                previous_valid,
2021                next_valid,
2022                exact_unclipped,
2023                column_overshoot_after_line_end: 0,
2024            }
2025        });
2026        cx.update_editor(|editor, window, cx| {
2027            editor.update_inlay_link_and_hover_points(
2028                &editor.snapshot(window, cx),
2029                struct_hint_part_hover_position,
2030                None,
2031                true,
2032                false,
2033                window,
2034                cx,
2035            );
2036        });
2037        cx.background_executor
2038            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2039        cx.background_executor.run_until_parked();
2040        cx.update_editor(|editor, _, cx| {
2041            let hover_state = &editor.hover_state;
2042            assert!(
2043                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2044            );
2045            let popover = hover_state.info_popovers.first().unwrap();
2046            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2047            assert_eq!(
2048                popover.symbol_range,
2049                RangeInEditor::Inlay(InlayHighlight {
2050                    inlay: InlayId::Hint(0),
2051                    inlay_position: buffer_snapshot
2052                        .anchor_after(MultiBufferOffset(inlay_range.start)),
2053                    range: ": ".len() + new_type_label.len() + "<".len()
2054                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
2055                }),
2056                "Popover range should match the struct label part"
2057            );
2058            assert_eq!(
2059                popover.get_rendered_text(cx),
2060                format!("A tooltip for {struct_label}"),
2061                "Rendered markdown element should remove backticks from text"
2062            );
2063        });
2064    }
2065
2066    #[test]
2067    fn test_find_hovered_hint_part_with_multibyte_characters() {
2068        use crate::display_map::InlayOffset;
2069        use multi_buffer::MultiBufferOffset;
2070        use project::InlayHintLabelPart;
2071
2072        // Test with multi-byte UTF-8 character "→" (3 bytes, 1 character)
2073        let label = "→ app/Livewire/UserProfile.php";
2074        let label_parts = vec![InlayHintLabelPart {
2075            value: label.to_string(),
2076            tooltip: None,
2077            location: None,
2078        }];
2079
2080        let hint_start = InlayOffset(MultiBufferOffset(100));
2081
2082        // Verify the label has more bytes than characters (due to "→")
2083        assert_eq!(label.len(), 32); // bytes
2084        assert_eq!(label.chars().count(), 30); // characters
2085
2086        // Test hovering at the last byte (should find the part)
2087        let last_byte_offset = InlayOffset(MultiBufferOffset(100 + label.len() - 1));
2088        let result = find_hovered_hint_part(label_parts.clone(), hint_start, last_byte_offset);
2089        assert!(
2090            result.is_some(),
2091            "Should find part when hovering at last byte"
2092        );
2093        let (part, range) = result.unwrap();
2094        assert_eq!(part.value, label);
2095        assert_eq!(range.start, hint_start);
2096        assert_eq!(range.end, InlayOffset(MultiBufferOffset(100 + label.len())));
2097
2098        // Test hovering at the first byte of "→" (byte 0)
2099        let first_byte_offset = InlayOffset(MultiBufferOffset(100));
2100        let result = find_hovered_hint_part(label_parts.clone(), hint_start, first_byte_offset);
2101        assert!(
2102            result.is_some(),
2103            "Should find part when hovering at first byte"
2104        );
2105
2106        // Test hovering in the middle of "→" (byte 1, still part of the arrow character)
2107        let mid_arrow_offset = InlayOffset(MultiBufferOffset(101));
2108        let result = find_hovered_hint_part(label_parts, hint_start, mid_arrow_offset);
2109        assert!(
2110            result.is_some(),
2111            "Should find part when hovering in middle of multi-byte char"
2112        );
2113
2114        // Test with multiple parts containing multi-byte characters
2115        // Part ranges are [start, end) - start inclusive, end exclusive
2116        // "→ " occupies bytes [0, 4), "path" occupies bytes [4, 8)
2117        let parts = vec![
2118            InlayHintLabelPart {
2119                value: "".to_string(), // 4 bytes (3 + 1)
2120                tooltip: None,
2121                location: None,
2122            },
2123            InlayHintLabelPart {
2124                value: "path".to_string(), // 4 bytes
2125                tooltip: None,
2126                location: None,
2127            },
2128        ];
2129
2130        // Hover at byte 3 (last byte of "→ ", the space character)
2131        let arrow_last_byte = InlayOffset(MultiBufferOffset(100 + 3));
2132        let result = find_hovered_hint_part(parts.clone(), hint_start, arrow_last_byte);
2133        assert!(result.is_some(), "Should find first part at its last byte");
2134        let (part, range) = result.unwrap();
2135        assert_eq!(part.value, "");
2136        assert_eq!(
2137            range,
2138            InlayOffset(MultiBufferOffset(100))..InlayOffset(MultiBufferOffset(104))
2139        );
2140
2141        // Hover at byte 4 (first byte of "path", at the boundary)
2142        let path_start_offset = InlayOffset(MultiBufferOffset(100 + 4));
2143        let result = find_hovered_hint_part(parts.clone(), hint_start, path_start_offset);
2144        assert!(result.is_some(), "Should find second part at boundary");
2145        let (part, _) = result.unwrap();
2146        assert_eq!(part.value, "path");
2147
2148        // Hover at byte 7 (last byte of "path")
2149        let path_end_offset = InlayOffset(MultiBufferOffset(100 + 7));
2150        let result = find_hovered_hint_part(parts, hint_start, path_end_offset);
2151        assert!(result.is_some(), "Should find second part at last byte");
2152        let (part, range) = result.unwrap();
2153        assert_eq!(part.value, "path");
2154        assert_eq!(
2155            range,
2156            InlayOffset(MultiBufferOffset(104))..InlayOffset(MultiBufferOffset(108))
2157        );
2158    }
2159
2160    #[gpui::test]
2161    async fn test_hover_popover_hiding_delay(cx: &mut gpui::TestAppContext) {
2162        init_test(cx, |_| {});
2163
2164        let custom_delay_ms = 500u64;
2165        cx.update(|cx| {
2166            cx.update_global::<SettingsStore, _>(|settings, cx| {
2167                settings.update_user_settings(cx, |settings| {
2168                    settings.editor.hover_popover_sticky = Some(true);
2169                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2170                });
2171            });
2172        });
2173
2174        let mut cx = EditorLspTestContext::new_rust(
2175            lsp::ServerCapabilities {
2176                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2177                ..Default::default()
2178            },
2179            cx,
2180        )
2181        .await;
2182
2183        cx.set_state(indoc! {"
2184            fn ˇtest() { println!(); }
2185        "});
2186
2187        // Trigger hover on a symbol
2188        let hover_point = cx.display_point(indoc! {"
2189            fn test() { printˇln!(); }
2190        "});
2191        let symbol_range = cx.lsp_range(indoc! {"
2192            fn test() { «println!»(); }
2193        "});
2194        let mut requests =
2195            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2196                Ok(Some(lsp::Hover {
2197                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2198                        kind: lsp::MarkupKind::Markdown,
2199                        value: "some basic docs".to_string(),
2200                    }),
2201                    range: Some(symbol_range),
2202                }))
2203            });
2204        cx.update_editor(|editor, window, cx| {
2205            let snapshot = editor.snapshot(window, cx);
2206            let anchor = snapshot
2207                .buffer_snapshot()
2208                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2209            hover_at(editor, Some(anchor), None, window, cx)
2210        });
2211        cx.background_executor
2212            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2213        requests.next().await;
2214
2215        // Hover should be visible
2216        cx.editor(|editor, _, _| {
2217            assert!(editor.hover_state.visible());
2218        });
2219
2220        // Move mouse away (hover_at with None anchor triggers the hiding delay)
2221        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2222
2223        // Popover should still be visible before the custom hiding delay expires
2224        cx.background_executor
2225            .advance_clock(Duration::from_millis(custom_delay_ms - 100));
2226        cx.editor(|editor, _, _| {
2227            assert!(
2228                editor.hover_state.visible(),
2229                "Popover should remain visible before the hiding delay expires"
2230            );
2231        });
2232
2233        // After the full custom delay, the popover should be hidden
2234        cx.background_executor
2235            .advance_clock(Duration::from_millis(200));
2236        cx.editor(|editor, _, _| {
2237            assert!(
2238                !editor.hover_state.visible(),
2239                "Popover should be hidden after the hiding delay expires"
2240            );
2241        });
2242    }
2243
2244    #[gpui::test]
2245    async fn test_hover_popover_sticky_disabled(cx: &mut gpui::TestAppContext) {
2246        init_test(cx, |_| {});
2247
2248        cx.update(|cx| {
2249            cx.update_global::<SettingsStore, _>(|settings, cx| {
2250                settings.update_user_settings(cx, |settings| {
2251                    settings.editor.hover_popover_sticky = Some(false);
2252                });
2253            });
2254        });
2255
2256        let mut cx = EditorLspTestContext::new_rust(
2257            lsp::ServerCapabilities {
2258                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2259                ..Default::default()
2260            },
2261            cx,
2262        )
2263        .await;
2264
2265        cx.set_state(indoc! {"
2266            fn ˇtest() { println!(); }
2267        "});
2268
2269        // Trigger hover on a symbol
2270        let hover_point = cx.display_point(indoc! {"
2271            fn test() { printˇln!(); }
2272        "});
2273        let symbol_range = cx.lsp_range(indoc! {"
2274            fn test() { «println!»(); }
2275        "});
2276        let mut requests =
2277            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2278                Ok(Some(lsp::Hover {
2279                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2280                        kind: lsp::MarkupKind::Markdown,
2281                        value: "some basic docs".to_string(),
2282                    }),
2283                    range: Some(symbol_range),
2284                }))
2285            });
2286        cx.update_editor(|editor, window, cx| {
2287            let snapshot = editor.snapshot(window, cx);
2288            let anchor = snapshot
2289                .buffer_snapshot()
2290                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2291            hover_at(editor, Some(anchor), None, window, cx)
2292        });
2293        cx.background_executor
2294            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2295        requests.next().await;
2296
2297        // Hover should be visible
2298        cx.editor(|editor, _, _| {
2299            assert!(editor.hover_state.visible());
2300        });
2301
2302        // Move mouse away — with sticky disabled, hide immediately
2303        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2304
2305        // Popover should be hidden immediately without any delay
2306        cx.editor(|editor, _, _| {
2307            assert!(
2308                !editor.hover_state.visible(),
2309                "Popover should be hidden immediately when sticky is disabled"
2310            );
2311        });
2312    }
2313
2314    #[gpui::test]
2315    async fn test_hover_popover_hiding_delay_restarts_when_mouse_gets_closer(
2316        cx: &mut gpui::TestAppContext,
2317    ) {
2318        init_test(cx, |_| {});
2319
2320        let custom_delay_ms = 600u64;
2321        cx.update(|cx| {
2322            cx.update_global::<SettingsStore, _>(|settings, cx| {
2323                settings.update_user_settings(cx, |settings| {
2324                    settings.editor.hover_popover_sticky = Some(true);
2325                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2326                });
2327            });
2328        });
2329
2330        let mut cx = EditorLspTestContext::new_rust(
2331            lsp::ServerCapabilities {
2332                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2333                ..Default::default()
2334            },
2335            cx,
2336        )
2337        .await;
2338
2339        cx.set_state(indoc! {"
2340            fn ˇtest() { println!(); }
2341        "});
2342
2343        let hover_point = cx.display_point(indoc! {"
2344            fn test() { printˇln!(); }
2345        "});
2346        let symbol_range = cx.lsp_range(indoc! {"
2347            fn test() { «println!»(); }
2348        "});
2349        let mut requests =
2350            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2351                Ok(Some(lsp::Hover {
2352                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2353                        kind: lsp::MarkupKind::Markdown,
2354                        value: "some basic docs".to_string(),
2355                    }),
2356                    range: Some(symbol_range),
2357                }))
2358            });
2359        cx.update_editor(|editor, window, cx| {
2360            let snapshot = editor.snapshot(window, cx);
2361            let anchor = snapshot
2362                .buffer_snapshot()
2363                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2364            hover_at(editor, Some(anchor), None, window, cx)
2365        });
2366        cx.background_executor
2367            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2368        requests.next().await;
2369
2370        cx.editor(|editor, _, _| {
2371            assert!(editor.hover_state.visible());
2372        });
2373
2374        cx.update_editor(|editor, _, _| {
2375            let popover = editor.hover_state.info_popovers.first().unwrap();
2376            popover.last_bounds.set(Some(Bounds {
2377                origin: gpui::Point {
2378                    x: px(100.0),
2379                    y: px(100.0),
2380                },
2381                size: Size {
2382                    width: px(100.0),
2383                    height: px(60.0),
2384                },
2385            }));
2386        });
2387
2388        let far_point = gpui::Point {
2389            x: px(260.0),
2390            y: px(130.0),
2391        };
2392        cx.update_editor(|editor, window, cx| hover_at(editor, None, Some(far_point), window, cx));
2393
2394        cx.background_executor
2395            .advance_clock(Duration::from_millis(400));
2396        cx.background_executor.run_until_parked();
2397
2398        let closer_point = gpui::Point {
2399            x: px(220.0),
2400            y: px(130.0),
2401        };
2402        cx.update_editor(|editor, window, cx| {
2403            hover_at(editor, None, Some(closer_point), window, cx)
2404        });
2405
2406        cx.background_executor
2407            .advance_clock(Duration::from_millis(250));
2408        cx.background_executor.run_until_parked();
2409
2410        cx.editor(|editor, _, _| {
2411            assert!(
2412                editor.hover_state.visible(),
2413                "Popover should remain visible because moving closer restarts the hiding timer"
2414            );
2415        });
2416
2417        cx.background_executor
2418            .advance_clock(Duration::from_millis(350));
2419        cx.background_executor.run_until_parked();
2420
2421        cx.editor(|editor, _, _| {
2422            assert!(
2423                !editor.hover_state.visible(),
2424                "Popover should hide after the restarted hiding timer expires"
2425            );
2426        });
2427    }
2428
2429    #[gpui::test]
2430    async fn test_hover_popover_cancel_hide_on_rehover(cx: &mut gpui::TestAppContext) {
2431        init_test(cx, |_| {});
2432
2433        let custom_delay_ms = 500u64;
2434        cx.update(|cx| {
2435            cx.update_global::<SettingsStore, _>(|settings, cx| {
2436                settings.update_user_settings(cx, |settings| {
2437                    settings.editor.hover_popover_sticky = Some(true);
2438                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2439                });
2440            });
2441        });
2442
2443        let mut cx = EditorLspTestContext::new_rust(
2444            lsp::ServerCapabilities {
2445                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2446                ..Default::default()
2447            },
2448            cx,
2449        )
2450        .await;
2451
2452        cx.set_state(indoc! {"
2453            fn ˇtest() { println!(); }
2454        "});
2455
2456        let hover_point = cx.display_point(indoc! {"
2457            fn test() { printˇln!(); }
2458        "});
2459        let symbol_range = cx.lsp_range(indoc! {"
2460            fn test() { «println!»(); }
2461        "});
2462        let mut requests =
2463            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2464                Ok(Some(lsp::Hover {
2465                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2466                        kind: lsp::MarkupKind::Markdown,
2467                        value: "some basic docs".to_string(),
2468                    }),
2469                    range: Some(symbol_range),
2470                }))
2471            });
2472        cx.update_editor(|editor, window, cx| {
2473            let snapshot = editor.snapshot(window, cx);
2474            let anchor = snapshot
2475                .buffer_snapshot()
2476                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2477            hover_at(editor, Some(anchor), None, window, cx)
2478        });
2479        cx.background_executor
2480            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2481        requests.next().await;
2482
2483        cx.editor(|editor, _, _| {
2484            assert!(editor.hover_state.visible());
2485        });
2486
2487        // Move mouse away — starts the 500ms hide timer
2488        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2489
2490        cx.background_executor
2491            .advance_clock(Duration::from_millis(300));
2492        cx.background_executor.run_until_parked();
2493        cx.editor(|editor, _, _| {
2494            assert!(
2495                editor.hover_state.visible(),
2496                "Popover should still be visible before hiding delay expires"
2497            );
2498        });
2499
2500        // Move back to the symbol — should cancel the hiding timer
2501        cx.update_editor(|editor, window, cx| {
2502            let snapshot = editor.snapshot(window, cx);
2503            let anchor = snapshot
2504                .buffer_snapshot()
2505                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2506            hover_at(editor, Some(anchor), None, window, cx)
2507        });
2508
2509        // Advance past the original deadline — popover should still be visible
2510        // because re-hovering cleared the hiding_delay_task
2511        cx.background_executor
2512            .advance_clock(Duration::from_millis(300));
2513        cx.background_executor.run_until_parked();
2514        cx.editor(|editor, _, _| {
2515            assert!(
2516                editor.hover_state.visible(),
2517                "Popover should remain visible after re-hovering the symbol"
2518            );
2519            assert!(
2520                editor.hover_state.hiding_delay_task.is_none(),
2521                "Hiding delay task should have been cleared by re-hover"
2522            );
2523        });
2524
2525        // Move away again — starts a fresh 500ms timer
2526        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2527
2528        cx.background_executor
2529            .advance_clock(Duration::from_millis(custom_delay_ms + 100));
2530        cx.background_executor.run_until_parked();
2531        cx.editor(|editor, _, _| {
2532            assert!(
2533                !editor.hover_state.visible(),
2534                "Popover should hide after the new hiding timer expires"
2535            );
2536        });
2537    }
2538
2539    #[gpui::test]
2540    async fn test_hover_popover_enabled_false_ignores_sticky(cx: &mut gpui::TestAppContext) {
2541        init_test(cx, |_| {});
2542
2543        cx.update(|cx| {
2544            cx.update_global::<SettingsStore, _>(|settings, cx| {
2545                settings.update_user_settings(cx, |settings| {
2546                    settings.editor.hover_popover_enabled = Some(false);
2547                    settings.editor.hover_popover_sticky = Some(true);
2548                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(500));
2549                });
2550            });
2551        });
2552
2553        let mut cx = EditorLspTestContext::new_rust(
2554            lsp::ServerCapabilities {
2555                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2556                ..Default::default()
2557            },
2558            cx,
2559        )
2560        .await;
2561
2562        cx.set_state(indoc! {"
2563            fn ˇtest() { println!(); }
2564        "});
2565
2566        let hover_point = cx.display_point(indoc! {"
2567            fn test() { printˇln!(); }
2568        "});
2569
2570        // Trigger hover_at — should be gated by hover_popover_enabled=false
2571        cx.update_editor(|editor, window, cx| {
2572            let snapshot = editor.snapshot(window, cx);
2573            let anchor = snapshot
2574                .buffer_snapshot()
2575                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2576            hover_at(editor, Some(anchor), None, window, cx)
2577        });
2578
2579        // No need to advance clock or wait for LSP — the gate should prevent any work
2580        cx.editor(|editor, _, _| {
2581            assert!(
2582                !editor.hover_state.visible(),
2583                "Popover should not appear when hover_popover_enabled is false"
2584            );
2585            assert!(
2586                editor.hover_state.info_task.is_none(),
2587                "No hover info task should be scheduled when hover is disabled"
2588            );
2589            assert!(
2590                editor.hover_state.triggered_from.is_none(),
2591                "No hover trigger should be recorded when hover is disabled"
2592            );
2593        });
2594    }
2595}