hover_popover.rs

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