hover_popover.rs

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