hover_popover.rs

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