hover_popover.rs

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