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