hover_popover.rs

   1use crate::{
   2    ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
   3    EditorSnapshot, GlobalDiagnosticRenderer, HighlightKey, Hover,
   4    display_map::{InlayOffset, ToDisplayPoint, is_invisible},
   5    editor_settings::EditorSettingsScrollbarProxy,
   6    hover_links::{InlayHighlight, RangeInEditor},
   7    movement::TextLayoutDetails,
   8    scroll::ScrollAmount,
   9};
  10use anyhow::Context as _;
  11use gpui::{
  12    AnyElement, App, AsyncWindowContext, Bounds, Context, Entity, Focusable as _, FontWeight, Hsla,
  13    InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
  14    StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
  15    Window, canvas, div, px,
  16};
  17use itertools::Itertools;
  18use language::{DiagnosticEntry, Language, LanguageRegistry};
  19use lsp::DiagnosticSeverity;
  20use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
  21use multi_buffer::{MultiBufferOffset, ToOffset, ToPoint};
  22use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
  23use settings::Settings;
  24use std::{
  25    borrow::Cow,
  26    cell::{Cell, RefCell},
  27};
  28use std::{ops::Range, sync::Arc, time::Duration};
  29use std::{path::PathBuf, rc::Rc};
  30use theme_settings::ThemeSettings;
  31use ui::{CopyButton, Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
  32use url::Url;
  33use util::TryFutureExt;
  34use workspace::{OpenOptions, OpenVisible, Workspace};
  35
  36pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
  37pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
  38pub const POPOVER_RIGHT_OFFSET: Pixels = px(8.0);
  39pub const HOVER_POPOVER_GAP: Pixels = px(10.);
  40
  41/// Bindable action which uses the most recent selection head to trigger a hover
  42pub fn hover(editor: &mut Editor, _: &Hover, window: &mut Window, cx: &mut Context<Editor>) {
  43    let head = editor.selections.newest_anchor().head();
  44    show_hover(editor, head, true, window, cx);
  45}
  46
  47/// The internal hover action dispatches between `show_hover` or `hide_hover`
  48/// depending on whether a point to hover over is provided.
  49pub fn hover_at(
  50    editor: &mut Editor,
  51    anchor: Option<Anchor>,
  52    mouse_position: Option<gpui::Point<Pixels>>,
  53    window: &mut Window,
  54    cx: &mut Context<Editor>,
  55) {
  56    if EditorSettings::get_global(cx).hover_popover_enabled {
  57        if show_keyboard_hover(editor, window, cx) {
  58            return;
  59        }
  60
  61        if let Some(anchor) = anchor {
  62            editor.hover_state.hiding_delay_task = None;
  63            editor.hover_state.closest_mouse_distance = None;
  64            show_hover(editor, anchor, false, window, cx);
  65        } else {
  66            let 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_position, _) = editor
 279        .buffer
 280        .read(cx)
 281        .snapshot(cx)
 282        .anchor_to_buffer_anchor(anchor)?;
 283    let buffer = editor.buffer.read(cx).buffer(buffer_position.buffer_id)?;
 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                            .buffer_anchor_range_to_anchor_range(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_visibility: CopyButtonVisibility::Hidden,
1044                                    border: false,
1045                                })
1046                                .on_url_click(open_markdown_url)
1047                                .p_2(),
1048                        ),
1049                )
1050                .custom_scrollbars(
1051                    Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
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_visibility: CopyButtonVisibility::Hidden,
1158                                    border: false,
1159                                })
1160                                .on_url_click(
1161                                    move |link, window, cx| {
1162                                        if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
1163                                        {
1164                                            this.update(cx, |this, cx| {
1165                                                renderer.as_ref().open_link(this, link, window, cx);
1166                                            })
1167                                            .ok();
1168                                        }
1169                                    },
1170                                ),
1171                            ),
1172                    )
1173                    .child(div().absolute().top_1().right_1().child({
1174                        let message = self.local_diagnostic.diagnostic.message.clone();
1175                        CopyButton::new("copy-diagnostic", message).tooltip_label("Copy Diagnostic")
1176                    }))
1177                    .custom_scrollbars(
1178                        Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1179                            .tracked_scroll_handle(&self.scroll_handle),
1180                        window,
1181                        cx,
1182                    ),
1183            )
1184            .into_any_element()
1185    }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190    use super::*;
1191    use crate::{
1192        PointForPosition,
1193        actions::ConfirmCompletion,
1194        editor_tests::{handle_completion_request, init_test},
1195        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
1196        test::editor_lsp_test_context::EditorLspTestContext,
1197    };
1198    use collections::BTreeSet;
1199    use gpui::App;
1200    use indoc::indoc;
1201    use markdown::parser::MarkdownEvent;
1202    use project::InlayId;
1203    use settings::InlayHintSettingsContent;
1204    use smol::stream::StreamExt;
1205    use std::sync::atomic;
1206    use std::sync::atomic::AtomicUsize;
1207    use text::Bias;
1208
1209    fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
1210        cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 })
1211    }
1212
1213    impl InfoPopover {
1214        fn get_rendered_text(&self, cx: &gpui::App) -> String {
1215            let mut rendered_text = String::new();
1216            if let Some(parsed_content) = self.parsed_content.clone() {
1217                let markdown = parsed_content.read(cx);
1218                let text = markdown.parsed_markdown().source().to_string();
1219                let data = markdown.parsed_markdown().events();
1220                let slice = data;
1221
1222                for (range, event) in slice.iter() {
1223                    match event {
1224                        MarkdownEvent::SubstitutedText(parsed) => {
1225                            rendered_text.push_str(parsed.as_str())
1226                        }
1227                        MarkdownEvent::Text | MarkdownEvent::Code => {
1228                            rendered_text.push_str(&text[range.clone()])
1229                        }
1230                        _ => {}
1231                    }
1232                }
1233            }
1234            rendered_text
1235        }
1236    }
1237
1238    #[gpui::test]
1239    async fn test_mouse_hover_info_popover_with_autocomplete_popover(
1240        cx: &mut gpui::TestAppContext,
1241    ) {
1242        init_test(cx, |_| {});
1243
1244        let mut cx = EditorLspTestContext::new_rust(
1245            lsp::ServerCapabilities {
1246                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1247                completion_provider: Some(lsp::CompletionOptions {
1248                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
1249                    resolve_provider: Some(true),
1250                    ..Default::default()
1251                }),
1252                ..Default::default()
1253            },
1254            cx,
1255        )
1256        .await;
1257        let counter = Arc::new(AtomicUsize::new(0));
1258        // Basic hover delays and then pops without moving the mouse
1259        cx.set_state(indoc! {"
1260                oneˇ
1261                two
1262                three
1263                fn test() { println!(); }
1264            "});
1265
1266        //prompt autocompletion menu
1267        cx.simulate_keystroke(".");
1268        handle_completion_request(
1269            indoc! {"
1270                        one.|<>
1271                        two
1272                        three
1273                    "},
1274            vec!["first_completion", "second_completion"],
1275            true,
1276            counter.clone(),
1277            &mut cx,
1278        )
1279        .await;
1280        cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
1281            .await;
1282        assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
1283
1284        let hover_point = cx.display_point(indoc! {"
1285                one.
1286                two
1287                three
1288                fn test() { printˇln!(); }
1289            "});
1290        cx.update_editor(|editor, window, cx| {
1291            let snapshot = editor.snapshot(window, cx);
1292            let anchor = snapshot
1293                .buffer_snapshot()
1294                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1295            hover_at(editor, Some(anchor), None, window, cx)
1296        });
1297        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1298
1299        // After delay, hover should be visible.
1300        let symbol_range = cx.lsp_range(indoc! {"
1301                one.
1302                two
1303                three
1304                fn test() { «println!»(); }
1305            "});
1306        let mut requests =
1307            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1308                Ok(Some(lsp::Hover {
1309                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1310                        kind: lsp::MarkupKind::Markdown,
1311                        value: "some basic docs".to_string(),
1312                    }),
1313                    range: Some(symbol_range),
1314                }))
1315            });
1316        cx.background_executor
1317            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1318        requests.next().await;
1319
1320        cx.editor(|editor, _window, cx| {
1321            assert!(editor.hover_state.visible());
1322            assert_eq!(
1323                editor.hover_state.info_popovers.len(),
1324                1,
1325                "Expected exactly one hover but got: {:?}",
1326                editor.hover_state.info_popovers.len()
1327            );
1328            let rendered_text = editor
1329                .hover_state
1330                .info_popovers
1331                .first()
1332                .unwrap()
1333                .get_rendered_text(cx);
1334            assert_eq!(rendered_text, "some basic docs".to_string())
1335        });
1336
1337        // check that the completion menu is still visible and that there still has only been 1 completion request
1338        cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
1339        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1340
1341        //apply a completion and check it was successfully applied
1342        let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1343            editor.context_menu_next(&Default::default(), window, cx);
1344            editor
1345                .confirm_completion(&ConfirmCompletion::default(), window, cx)
1346                .unwrap()
1347        });
1348        cx.assert_editor_state(indoc! {"
1349            one.second_completionˇ
1350            two
1351            three
1352            fn test() { println!(); }
1353        "});
1354
1355        // check that the completion menu is no longer visible and that there still has only been 1 completion request
1356        cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1357        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1358
1359        //verify the information popover is still visible and unchanged
1360        cx.editor(|editor, _, cx| {
1361            assert!(editor.hover_state.visible());
1362            assert_eq!(
1363                editor.hover_state.info_popovers.len(),
1364                1,
1365                "Expected exactly one hover but got: {:?}",
1366                editor.hover_state.info_popovers.len()
1367            );
1368            let rendered_text = editor
1369                .hover_state
1370                .info_popovers
1371                .first()
1372                .unwrap()
1373                .get_rendered_text(cx);
1374
1375            assert_eq!(rendered_text, "some basic docs".to_string())
1376        });
1377
1378        // Mouse moved with no hover response dismisses
1379        let hover_point = cx.display_point(indoc! {"
1380                one.second_completionˇ
1381                two
1382                three
1383                fn teˇst() { println!(); }
1384            "});
1385        let mut request = cx
1386            .lsp
1387            .set_request_handler::<lsp::request::HoverRequest, _, _>(
1388                |_, _| async move { Ok(None) },
1389            );
1390        cx.update_editor(|editor, window, cx| {
1391            let snapshot = editor.snapshot(window, cx);
1392            let anchor = snapshot
1393                .buffer_snapshot()
1394                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1395            hover_at(editor, Some(anchor), None, window, cx)
1396        });
1397        cx.background_executor
1398            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1399        request.next().await;
1400
1401        // verify that the information popover is no longer visible
1402        cx.editor(|editor, _, _| {
1403            assert!(!editor.hover_state.visible());
1404        });
1405    }
1406
1407    #[gpui::test]
1408    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1409        init_test(cx, |_| {});
1410
1411        let mut cx = EditorLspTestContext::new_rust(
1412            lsp::ServerCapabilities {
1413                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1414                ..Default::default()
1415            },
1416            cx,
1417        )
1418        .await;
1419
1420        // Basic hover delays and then pops without moving the mouse
1421        cx.set_state(indoc! {"
1422            fn ˇtest() { println!(); }
1423        "});
1424        let hover_point = cx.display_point(indoc! {"
1425            fn test() { printˇln!(); }
1426        "});
1427
1428        cx.update_editor(|editor, window, cx| {
1429            let snapshot = editor.snapshot(window, cx);
1430            let anchor = snapshot
1431                .buffer_snapshot()
1432                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1433            hover_at(editor, Some(anchor), None, window, cx)
1434        });
1435        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1436
1437        // After delay, hover should be visible.
1438        let symbol_range = cx.lsp_range(indoc! {"
1439            fn test() { «println!»(); }
1440        "});
1441        let mut requests =
1442            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1443                Ok(Some(lsp::Hover {
1444                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1445                        kind: lsp::MarkupKind::Markdown,
1446                        value: "some basic docs".to_string(),
1447                    }),
1448                    range: Some(symbol_range),
1449                }))
1450            });
1451        cx.background_executor
1452            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1453        requests.next().await;
1454
1455        cx.editor(|editor, _, cx| {
1456            assert!(editor.hover_state.visible());
1457            assert_eq!(
1458                editor.hover_state.info_popovers.len(),
1459                1,
1460                "Expected exactly one hover but got: {:?}",
1461                editor.hover_state.info_popovers.len()
1462            );
1463            let rendered_text = editor
1464                .hover_state
1465                .info_popovers
1466                .first()
1467                .unwrap()
1468                .get_rendered_text(cx);
1469
1470            assert_eq!(rendered_text, "some basic docs".to_string())
1471        });
1472
1473        // Mouse moved with no hover response dismisses
1474        let hover_point = cx.display_point(indoc! {"
1475            fn teˇst() { println!(); }
1476        "});
1477        let mut request = cx
1478            .lsp
1479            .set_request_handler::<lsp::request::HoverRequest, _, _>(
1480                |_, _| async move { Ok(None) },
1481            );
1482        cx.update_editor(|editor, window, cx| {
1483            let snapshot = editor.snapshot(window, cx);
1484            let anchor = snapshot
1485                .buffer_snapshot()
1486                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1487            hover_at(editor, Some(anchor), None, window, cx)
1488        });
1489        cx.background_executor
1490            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1491        request.next().await;
1492        cx.editor(|editor, _, _| {
1493            assert!(!editor.hover_state.visible());
1494        });
1495    }
1496
1497    #[gpui::test]
1498    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1499        init_test(cx, |_| {});
1500
1501        let mut cx = EditorLspTestContext::new_rust(
1502            lsp::ServerCapabilities {
1503                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1504                ..Default::default()
1505            },
1506            cx,
1507        )
1508        .await;
1509
1510        // Hover with keyboard has no delay
1511        cx.set_state(indoc! {"
1512            fˇn test() { println!(); }
1513        "});
1514        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1515        let symbol_range = cx.lsp_range(indoc! {"
1516            «fn» test() { println!(); }
1517        "});
1518
1519        cx.editor(|editor, _window, _cx| {
1520            assert!(!editor.hover_state.visible());
1521
1522            assert_eq!(
1523                editor.hover_state.info_popovers.len(),
1524                0,
1525                "Expected no hovers but got but got: {:?}",
1526                editor.hover_state.info_popovers.len()
1527            );
1528        });
1529
1530        let mut requests =
1531            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1532                Ok(Some(lsp::Hover {
1533                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1534                        kind: lsp::MarkupKind::Markdown,
1535                        value: "some other basic docs".to_string(),
1536                    }),
1537                    range: Some(symbol_range),
1538                }))
1539            });
1540
1541        requests.next().await;
1542        cx.dispatch_action(Hover);
1543
1544        cx.condition(|editor, _| editor.hover_state.visible()).await;
1545        cx.editor(|editor, _, cx| {
1546            assert_eq!(
1547                editor.hover_state.info_popovers.len(),
1548                1,
1549                "Expected exactly one hover but got: {:?}",
1550                editor.hover_state.info_popovers.len()
1551            );
1552
1553            let rendered_text = editor
1554                .hover_state
1555                .info_popovers
1556                .first()
1557                .unwrap()
1558                .get_rendered_text(cx);
1559
1560            assert_eq!(rendered_text, "some other basic docs".to_string())
1561        });
1562    }
1563
1564    #[gpui::test]
1565    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1566        init_test(cx, |_| {});
1567
1568        let mut cx = EditorLspTestContext::new_rust(
1569            lsp::ServerCapabilities {
1570                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1571                ..Default::default()
1572            },
1573            cx,
1574        )
1575        .await;
1576
1577        // Hover with keyboard has no delay
1578        cx.set_state(indoc! {"
1579            fˇn test() { println!(); }
1580        "});
1581        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1582        let symbol_range = cx.lsp_range(indoc! {"
1583            «fn» test() { println!(); }
1584        "});
1585        cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1586            Ok(Some(lsp::Hover {
1587                contents: lsp::HoverContents::Array(vec![
1588                    lsp::MarkedString::String("regular text for hover to show".to_string()),
1589                    lsp::MarkedString::String("".to_string()),
1590                    lsp::MarkedString::LanguageString(lsp::LanguageString {
1591                        language: "Rust".to_string(),
1592                        value: "".to_string(),
1593                    }),
1594                ]),
1595                range: Some(symbol_range),
1596            }))
1597        })
1598        .next()
1599        .await;
1600        cx.dispatch_action(Hover);
1601
1602        cx.condition(|editor, _| editor.hover_state.visible()).await;
1603        cx.editor(|editor, _, cx| {
1604            assert_eq!(
1605                editor.hover_state.info_popovers.len(),
1606                1,
1607                "Expected exactly one hover but got: {:?}",
1608                editor.hover_state.info_popovers.len()
1609            );
1610            let rendered_text = editor
1611                .hover_state
1612                .info_popovers
1613                .first()
1614                .unwrap()
1615                .get_rendered_text(cx);
1616
1617            assert_eq!(
1618                rendered_text,
1619                "regular text for hover to show".to_string(),
1620                "No empty string hovers should be shown"
1621            );
1622        });
1623    }
1624
1625    #[gpui::test]
1626    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1627        init_test(cx, |_| {});
1628
1629        let mut cx = EditorLspTestContext::new_rust(
1630            lsp::ServerCapabilities {
1631                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1632                ..Default::default()
1633            },
1634            cx,
1635        )
1636        .await;
1637
1638        // Hover with keyboard has no delay
1639        cx.set_state(indoc! {"
1640            fˇn test() { println!(); }
1641        "});
1642        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1643        let symbol_range = cx.lsp_range(indoc! {"
1644            «fn» test() { println!(); }
1645        "});
1646
1647        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1648        let markdown_string = format!("\n```rust\n{code_str}```");
1649
1650        let closure_markdown_string = markdown_string.clone();
1651        cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1652            let future_markdown_string = closure_markdown_string.clone();
1653            async move {
1654                Ok(Some(lsp::Hover {
1655                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1656                        kind: lsp::MarkupKind::Markdown,
1657                        value: future_markdown_string,
1658                    }),
1659                    range: Some(symbol_range),
1660                }))
1661            }
1662        })
1663        .next()
1664        .await;
1665
1666        cx.dispatch_action(Hover);
1667
1668        cx.condition(|editor, _| editor.hover_state.visible()).await;
1669        cx.editor(|editor, _, cx| {
1670            assert_eq!(
1671                editor.hover_state.info_popovers.len(),
1672                1,
1673                "Expected exactly one hover but got: {:?}",
1674                editor.hover_state.info_popovers.len()
1675            );
1676            let rendered_text = editor
1677                .hover_state
1678                .info_popovers
1679                .first()
1680                .unwrap()
1681                .get_rendered_text(cx);
1682
1683            assert_eq!(
1684                rendered_text, code_str,
1685                "Should not have extra line breaks at end of rendered hover"
1686            );
1687        });
1688    }
1689
1690    #[gpui::test]
1691    // https://github.com/zed-industries/zed/issues/15498
1692    async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1693        init_test(cx, |_| {});
1694
1695        let mut cx = EditorLspTestContext::new_rust(
1696            lsp::ServerCapabilities {
1697                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1698                ..Default::default()
1699            },
1700            cx,
1701        )
1702        .await;
1703
1704        cx.set_state(indoc! {"
1705            fn fuˇnc(abc def: i32) -> u32 {
1706            }
1707        "});
1708
1709        cx.lsp
1710            .set_request_handler::<lsp::request::HoverRequest, _, _>({
1711                |_, _| async move {
1712                    Ok(Some(lsp::Hover {
1713                        contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1714                            kind: lsp::MarkupKind::Markdown,
1715                            value: indoc!(
1716                                r#"
1717                    ### function `errands_data_read`
1718
1719                    ---
1720                    → `char *`
1721                    Function to read a file into a string
1722
1723                    ---
1724                    ```cpp
1725                    static char *errands_data_read()
1726                    ```
1727                    "#
1728                            )
1729                            .to_string(),
1730                        }),
1731                        range: None,
1732                    }))
1733                }
1734            });
1735        cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1736        cx.run_until_parked();
1737
1738        cx.update_editor(|editor, _, cx| {
1739            let popover = editor.hover_state.info_popovers.first().unwrap();
1740            let content = popover.get_rendered_text(cx);
1741
1742            assert!(content.contains("Function to read a file"));
1743        });
1744    }
1745
1746    #[gpui::test]
1747    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1748        init_test(cx, |settings| {
1749            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1750                show_value_hints: Some(true),
1751                enabled: Some(true),
1752                edit_debounce_ms: Some(0),
1753                scroll_debounce_ms: Some(0),
1754                show_type_hints: Some(true),
1755                show_parameter_hints: Some(true),
1756                show_other_hints: Some(true),
1757                show_background: Some(false),
1758                toggle_on_modifiers_press: None,
1759            })
1760        });
1761
1762        let mut cx = EditorLspTestContext::new_rust(
1763            lsp::ServerCapabilities {
1764                inlay_hint_provider: Some(lsp::OneOf::Right(
1765                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1766                        resolve_provider: Some(true),
1767                        ..Default::default()
1768                    }),
1769                )),
1770                ..Default::default()
1771            },
1772            cx,
1773        )
1774        .await;
1775
1776        cx.set_state(indoc! {"
1777            struct TestStruct;
1778
1779            // ==================
1780
1781            struct TestNewType<T>(T);
1782
1783            fn main() {
1784                let variableˇ = TestNewType(TestStruct);
1785            }
1786        "});
1787
1788        let hint_start_offset = cx.ranges(indoc! {"
1789            struct TestStruct;
1790
1791            // ==================
1792
1793            struct TestNewType<T>(T);
1794
1795            fn main() {
1796                let variableˇ = TestNewType(TestStruct);
1797            }
1798        "})[0]
1799            .start;
1800        let hint_position = cx.to_lsp(MultiBufferOffset(hint_start_offset));
1801        let new_type_target_range = cx.lsp_range(indoc! {"
1802            struct TestStruct;
1803
1804            // ==================
1805
1806            struct «TestNewType»<T>(T);
1807
1808            fn main() {
1809                let variable = TestNewType(TestStruct);
1810            }
1811        "});
1812        let struct_target_range = cx.lsp_range(indoc! {"
1813            struct «TestStruct»;
1814
1815            // ==================
1816
1817            struct TestNewType<T>(T);
1818
1819            fn main() {
1820                let variable = TestNewType(TestStruct);
1821            }
1822        "});
1823
1824        let uri = cx.buffer_lsp_url.clone();
1825        let new_type_label = "TestNewType";
1826        let struct_label = "TestStruct";
1827        let entire_hint_label = ": TestNewType<TestStruct>";
1828        let closure_uri = uri.clone();
1829        cx.lsp
1830            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1831                let task_uri = closure_uri.clone();
1832                async move {
1833                    assert_eq!(params.text_document.uri, task_uri);
1834                    Ok(Some(vec![lsp::InlayHint {
1835                        position: hint_position,
1836                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1837                            value: entire_hint_label.to_string(),
1838                            ..Default::default()
1839                        }]),
1840                        kind: Some(lsp::InlayHintKind::TYPE),
1841                        text_edits: None,
1842                        tooltip: None,
1843                        padding_left: Some(false),
1844                        padding_right: Some(false),
1845                        data: None,
1846                    }]))
1847                }
1848            })
1849            .next()
1850            .await;
1851        cx.background_executor.run_until_parked();
1852        cx.update_editor(|editor, _, cx| {
1853            let expected_layers = vec![entire_hint_label.to_string()];
1854            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
1855            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1856        });
1857
1858        let inlay_range = cx
1859            .ranges(indoc! {"
1860                struct TestStruct;
1861
1862                // ==================
1863
1864                struct TestNewType<T>(T);
1865
1866                fn main() {
1867                    let variable« »= TestNewType(TestStruct);
1868                }
1869        "})
1870            .first()
1871            .cloned()
1872            .unwrap();
1873        let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1874            let snapshot = editor.snapshot(window, cx);
1875            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
1876            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
1877            assert_eq!(previous_valid.row(), next_valid.row());
1878            assert!(previous_valid.column() < next_valid.column());
1879            let exact_unclipped = DisplayPoint::new(
1880                previous_valid.row(),
1881                previous_valid.column()
1882                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1883                        as u32,
1884            );
1885            PointForPosition {
1886                previous_valid,
1887                next_valid,
1888                exact_unclipped,
1889                column_overshoot_after_line_end: 0,
1890            }
1891        });
1892        cx.update_editor(|editor, window, cx| {
1893            editor.update_inlay_link_and_hover_points(
1894                &editor.snapshot(window, cx),
1895                new_type_hint_part_hover_position,
1896                None,
1897                true,
1898                false,
1899                window,
1900                cx,
1901            );
1902        });
1903
1904        let resolve_closure_uri = uri.clone();
1905        cx.lsp
1906            .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
1907                move |mut hint_to_resolve, _| {
1908                    let mut resolved_hint_positions = BTreeSet::new();
1909                    let task_uri = resolve_closure_uri.clone();
1910                    async move {
1911                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1912                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1913
1914                        // `: TestNewType<TestStruct>`
1915                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1916                            lsp::InlayHintLabelPart {
1917                                value: ": ".to_string(),
1918                                ..Default::default()
1919                            },
1920                            lsp::InlayHintLabelPart {
1921                                value: new_type_label.to_string(),
1922                                location: Some(lsp::Location {
1923                                    uri: task_uri.clone(),
1924                                    range: new_type_target_range,
1925                                }),
1926                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1927                                    "A tooltip for `{new_type_label}`"
1928                                ))),
1929                                ..Default::default()
1930                            },
1931                            lsp::InlayHintLabelPart {
1932                                value: "<".to_string(),
1933                                ..Default::default()
1934                            },
1935                            lsp::InlayHintLabelPart {
1936                                value: struct_label.to_string(),
1937                                location: Some(lsp::Location {
1938                                    uri: task_uri,
1939                                    range: struct_target_range,
1940                                }),
1941                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1942                                    lsp::MarkupContent {
1943                                        kind: lsp::MarkupKind::Markdown,
1944                                        value: format!("A tooltip for `{struct_label}`"),
1945                                    },
1946                                )),
1947                                ..Default::default()
1948                            },
1949                            lsp::InlayHintLabelPart {
1950                                value: ">".to_string(),
1951                                ..Default::default()
1952                            },
1953                        ]);
1954
1955                        Ok(hint_to_resolve)
1956                    }
1957                },
1958            )
1959            .next()
1960            .await;
1961        cx.background_executor.run_until_parked();
1962
1963        cx.update_editor(|editor, window, cx| {
1964            editor.update_inlay_link_and_hover_points(
1965                &editor.snapshot(window, cx),
1966                new_type_hint_part_hover_position,
1967                None,
1968                true,
1969                false,
1970                window,
1971                cx,
1972            );
1973        });
1974        cx.background_executor
1975            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1976        cx.background_executor.run_until_parked();
1977        cx.update_editor(|editor, _, cx| {
1978            let hover_state = &editor.hover_state;
1979            assert!(
1980                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1981            );
1982            let popover = hover_state.info_popovers.first().unwrap();
1983            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1984            assert_eq!(
1985                popover.symbol_range,
1986                RangeInEditor::Inlay(InlayHighlight {
1987                    inlay: InlayId::Hint(0),
1988                    inlay_position: buffer_snapshot
1989                        .anchor_after(MultiBufferOffset(inlay_range.start)),
1990                    range: ": ".len()..": ".len() + new_type_label.len(),
1991                }),
1992                "Popover range should match the new type label part"
1993            );
1994            assert_eq!(
1995                popover.get_rendered_text(cx),
1996                format!("A tooltip for {new_type_label}"),
1997            );
1998        });
1999
2000        let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
2001            let snapshot = editor.snapshot(window, cx);
2002            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
2003            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
2004            assert_eq!(previous_valid.row(), next_valid.row());
2005            assert!(previous_valid.column() < next_valid.column());
2006            let exact_unclipped = DisplayPoint::new(
2007                previous_valid.row(),
2008                previous_valid.column()
2009                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
2010                        as u32,
2011            );
2012            PointForPosition {
2013                previous_valid,
2014                next_valid,
2015                exact_unclipped,
2016                column_overshoot_after_line_end: 0,
2017            }
2018        });
2019        cx.update_editor(|editor, window, cx| {
2020            editor.update_inlay_link_and_hover_points(
2021                &editor.snapshot(window, cx),
2022                struct_hint_part_hover_position,
2023                None,
2024                true,
2025                false,
2026                window,
2027                cx,
2028            );
2029        });
2030        cx.background_executor
2031            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2032        cx.background_executor.run_until_parked();
2033        cx.update_editor(|editor, _, cx| {
2034            let hover_state = &editor.hover_state;
2035            assert!(
2036                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2037            );
2038            let popover = hover_state.info_popovers.first().unwrap();
2039            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2040            assert_eq!(
2041                popover.symbol_range,
2042                RangeInEditor::Inlay(InlayHighlight {
2043                    inlay: InlayId::Hint(0),
2044                    inlay_position: buffer_snapshot
2045                        .anchor_after(MultiBufferOffset(inlay_range.start)),
2046                    range: ": ".len() + new_type_label.len() + "<".len()
2047                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
2048                }),
2049                "Popover range should match the struct label part"
2050            );
2051            assert_eq!(
2052                popover.get_rendered_text(cx),
2053                format!("A tooltip for {struct_label}"),
2054                "Rendered markdown element should remove backticks from text"
2055            );
2056        });
2057    }
2058
2059    #[test]
2060    fn test_find_hovered_hint_part_with_multibyte_characters() {
2061        use crate::display_map::InlayOffset;
2062        use multi_buffer::MultiBufferOffset;
2063        use project::InlayHintLabelPart;
2064
2065        // Test with multi-byte UTF-8 character "→" (3 bytes, 1 character)
2066        let label = "→ app/Livewire/UserProfile.php";
2067        let label_parts = vec![InlayHintLabelPart {
2068            value: label.to_string(),
2069            tooltip: None,
2070            location: None,
2071        }];
2072
2073        let hint_start = InlayOffset(MultiBufferOffset(100));
2074
2075        // Verify the label has more bytes than characters (due to "→")
2076        assert_eq!(label.len(), 32); // bytes
2077        assert_eq!(label.chars().count(), 30); // characters
2078
2079        // Test hovering at the last byte (should find the part)
2080        let last_byte_offset = InlayOffset(MultiBufferOffset(100 + label.len() - 1));
2081        let result = find_hovered_hint_part(label_parts.clone(), hint_start, last_byte_offset);
2082        assert!(
2083            result.is_some(),
2084            "Should find part when hovering at last byte"
2085        );
2086        let (part, range) = result.unwrap();
2087        assert_eq!(part.value, label);
2088        assert_eq!(range.start, hint_start);
2089        assert_eq!(range.end, InlayOffset(MultiBufferOffset(100 + label.len())));
2090
2091        // Test hovering at the first byte of "→" (byte 0)
2092        let first_byte_offset = InlayOffset(MultiBufferOffset(100));
2093        let result = find_hovered_hint_part(label_parts.clone(), hint_start, first_byte_offset);
2094        assert!(
2095            result.is_some(),
2096            "Should find part when hovering at first byte"
2097        );
2098
2099        // Test hovering in the middle of "→" (byte 1, still part of the arrow character)
2100        let mid_arrow_offset = InlayOffset(MultiBufferOffset(101));
2101        let result = find_hovered_hint_part(label_parts, hint_start, mid_arrow_offset);
2102        assert!(
2103            result.is_some(),
2104            "Should find part when hovering in middle of multi-byte char"
2105        );
2106
2107        // Test with multiple parts containing multi-byte characters
2108        // Part ranges are [start, end) - start inclusive, end exclusive
2109        // "→ " occupies bytes [0, 4), "path" occupies bytes [4, 8)
2110        let parts = vec![
2111            InlayHintLabelPart {
2112                value: "".to_string(), // 4 bytes (3 + 1)
2113                tooltip: None,
2114                location: None,
2115            },
2116            InlayHintLabelPart {
2117                value: "path".to_string(), // 4 bytes
2118                tooltip: None,
2119                location: None,
2120            },
2121        ];
2122
2123        // Hover at byte 3 (last byte of "→ ", the space character)
2124        let arrow_last_byte = InlayOffset(MultiBufferOffset(100 + 3));
2125        let result = find_hovered_hint_part(parts.clone(), hint_start, arrow_last_byte);
2126        assert!(result.is_some(), "Should find first part at its last byte");
2127        let (part, range) = result.unwrap();
2128        assert_eq!(part.value, "");
2129        assert_eq!(
2130            range,
2131            InlayOffset(MultiBufferOffset(100))..InlayOffset(MultiBufferOffset(104))
2132        );
2133
2134        // Hover at byte 4 (first byte of "path", at the boundary)
2135        let path_start_offset = InlayOffset(MultiBufferOffset(100 + 4));
2136        let result = find_hovered_hint_part(parts.clone(), hint_start, path_start_offset);
2137        assert!(result.is_some(), "Should find second part at boundary");
2138        let (part, _) = result.unwrap();
2139        assert_eq!(part.value, "path");
2140
2141        // Hover at byte 7 (last byte of "path")
2142        let path_end_offset = InlayOffset(MultiBufferOffset(100 + 7));
2143        let result = find_hovered_hint_part(parts, hint_start, path_end_offset);
2144        assert!(result.is_some(), "Should find second part at last byte");
2145        let (part, range) = result.unwrap();
2146        assert_eq!(part.value, "path");
2147        assert_eq!(
2148            range,
2149            InlayOffset(MultiBufferOffset(104))..InlayOffset(MultiBufferOffset(108))
2150        );
2151    }
2152}