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