hover_popover.rs

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