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