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