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                break_style: Default::default(),
 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                    if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
 889                        rendered_text.push_str(&text[range.clone()])
 890                    }
 891                }
 892            }
 893            rendered_text
 894        }
 895    }
 896
 897    #[gpui::test]
 898    async fn test_mouse_hover_info_popover_with_autocomplete_popover(
 899        cx: &mut gpui::TestAppContext,
 900    ) {
 901        init_test(cx, |_| {});
 902
 903        let mut cx = EditorLspTestContext::new_rust(
 904            lsp::ServerCapabilities {
 905                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 906                completion_provider: Some(lsp::CompletionOptions {
 907                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
 908                    resolve_provider: Some(true),
 909                    ..Default::default()
 910                }),
 911                ..Default::default()
 912            },
 913            cx,
 914        )
 915        .await;
 916        let counter = Arc::new(AtomicUsize::new(0));
 917        // Basic hover delays and then pops without moving the mouse
 918        cx.set_state(indoc! {"
 919                oneˇ
 920                two
 921                three
 922                fn test() { println!(); }
 923            "});
 924
 925        //prompt autocompletion menu
 926        cx.simulate_keystroke(".");
 927        handle_completion_request(
 928            &mut cx,
 929            indoc! {"
 930                        one.|<>
 931                        two
 932                        three
 933                    "},
 934            vec!["first_completion", "second_completion"],
 935            counter.clone(),
 936        )
 937        .await;
 938        cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
 939            .await;
 940        assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
 941
 942        let hover_point = cx.display_point(indoc! {"
 943                one.
 944                two
 945                three
 946                fn test() { printˇln!(); }
 947            "});
 948        cx.update_editor(|editor, window, cx| {
 949            let snapshot = editor.snapshot(window, cx);
 950            let anchor = snapshot
 951                .buffer_snapshot
 952                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
 953            hover_at(editor, Some(anchor), window, cx)
 954        });
 955        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
 956
 957        // After delay, hover should be visible.
 958        let symbol_range = cx.lsp_range(indoc! {"
 959                one.
 960                two
 961                three
 962                fn test() { «println!»(); }
 963            "});
 964        let mut requests =
 965            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 966                Ok(Some(lsp::Hover {
 967                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 968                        kind: lsp::MarkupKind::Markdown,
 969                        value: "some basic docs".to_string(),
 970                    }),
 971                    range: Some(symbol_range),
 972                }))
 973            });
 974        cx.background_executor
 975            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
 976        requests.next().await;
 977
 978        cx.editor(|editor, _window, cx| {
 979            assert!(editor.hover_state.visible());
 980            assert_eq!(
 981                editor.hover_state.info_popovers.len(),
 982                1,
 983                "Expected exactly one hover but got: {:?}",
 984                editor.hover_state.info_popovers
 985            );
 986            let rendered_text = editor
 987                .hover_state
 988                .info_popovers
 989                .first()
 990                .unwrap()
 991                .get_rendered_text(cx);
 992            assert_eq!(rendered_text, "some basic docs".to_string())
 993        });
 994
 995        // check that the completion menu is still visible and that there still has only been 1 completion request
 996        cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
 997        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
 998
 999        //apply a completion and check it was successfully applied
1000        let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1001            editor.context_menu_next(&Default::default(), window, cx);
1002            editor
1003                .confirm_completion(&ConfirmCompletion::default(), window, cx)
1004                .unwrap()
1005        });
1006        cx.assert_editor_state(indoc! {"
1007            one.second_completionˇ
1008            two
1009            three
1010            fn test() { println!(); }
1011        "});
1012
1013        // check that the completion menu is no longer visible and that there still has only been 1 completion request
1014        cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1015        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1016
1017        //verify the information popover is still visible and unchanged
1018        cx.editor(|editor, _, cx| {
1019            assert!(editor.hover_state.visible());
1020            assert_eq!(
1021                editor.hover_state.info_popovers.len(),
1022                1,
1023                "Expected exactly one hover but got: {:?}",
1024                editor.hover_state.info_popovers
1025            );
1026            let rendered_text = editor
1027                .hover_state
1028                .info_popovers
1029                .first()
1030                .unwrap()
1031                .get_rendered_text(cx);
1032
1033            assert_eq!(rendered_text, "some basic docs".to_string())
1034        });
1035
1036        // Mouse moved with no hover response dismisses
1037        let hover_point = cx.display_point(indoc! {"
1038                one.second_completionˇ
1039                two
1040                three
1041                fn teˇst() { println!(); }
1042            "});
1043        let mut request = cx
1044            .lsp
1045            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1046        cx.update_editor(|editor, window, cx| {
1047            let snapshot = editor.snapshot(window, cx);
1048            let anchor = snapshot
1049                .buffer_snapshot
1050                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1051            hover_at(editor, Some(anchor), window, cx)
1052        });
1053        cx.background_executor
1054            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1055        request.next().await;
1056
1057        // verify that the information popover is no longer visible
1058        cx.editor(|editor, _, _| {
1059            assert!(!editor.hover_state.visible());
1060        });
1061    }
1062
1063    #[gpui::test]
1064    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1065        init_test(cx, |_| {});
1066
1067        let mut cx = EditorLspTestContext::new_rust(
1068            lsp::ServerCapabilities {
1069                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1070                ..Default::default()
1071            },
1072            cx,
1073        )
1074        .await;
1075
1076        // Basic hover delays and then pops without moving the mouse
1077        cx.set_state(indoc! {"
1078            fn ˇtest() { println!(); }
1079        "});
1080        let hover_point = cx.display_point(indoc! {"
1081            fn test() { printˇln!(); }
1082        "});
1083
1084        cx.update_editor(|editor, window, cx| {
1085            let snapshot = editor.snapshot(window, cx);
1086            let anchor = snapshot
1087                .buffer_snapshot
1088                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1089            hover_at(editor, Some(anchor), window, cx)
1090        });
1091        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1092
1093        // After delay, hover should be visible.
1094        let symbol_range = cx.lsp_range(indoc! {"
1095            fn test() { «println!»(); }
1096        "});
1097        let mut requests =
1098            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1099                Ok(Some(lsp::Hover {
1100                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1101                        kind: lsp::MarkupKind::Markdown,
1102                        value: "some basic docs".to_string(),
1103                    }),
1104                    range: Some(symbol_range),
1105                }))
1106            });
1107        cx.background_executor
1108            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1109        requests.next().await;
1110
1111        cx.editor(|editor, _, cx| {
1112            assert!(editor.hover_state.visible());
1113            assert_eq!(
1114                editor.hover_state.info_popovers.len(),
1115                1,
1116                "Expected exactly one hover but got: {:?}",
1117                editor.hover_state.info_popovers
1118            );
1119            let rendered_text = editor
1120                .hover_state
1121                .info_popovers
1122                .first()
1123                .unwrap()
1124                .get_rendered_text(cx);
1125
1126            assert_eq!(rendered_text, "some basic docs".to_string())
1127        });
1128
1129        // Mouse moved with no hover response dismisses
1130        let hover_point = cx.display_point(indoc! {"
1131            fn teˇst() { println!(); }
1132        "});
1133        let mut request = cx
1134            .lsp
1135            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1136        cx.update_editor(|editor, window, cx| {
1137            let snapshot = editor.snapshot(window, cx);
1138            let anchor = snapshot
1139                .buffer_snapshot
1140                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1141            hover_at(editor, Some(anchor), window, cx)
1142        });
1143        cx.background_executor
1144            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1145        request.next().await;
1146        cx.editor(|editor, _, _| {
1147            assert!(!editor.hover_state.visible());
1148        });
1149    }
1150
1151    #[gpui::test]
1152    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1153        init_test(cx, |_| {});
1154
1155        let mut cx = EditorLspTestContext::new_rust(
1156            lsp::ServerCapabilities {
1157                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1158                ..Default::default()
1159            },
1160            cx,
1161        )
1162        .await;
1163
1164        // Hover with keyboard has no delay
1165        cx.set_state(indoc! {"
1166            fˇn test() { println!(); }
1167        "});
1168        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1169        let symbol_range = cx.lsp_range(indoc! {"
1170            «fn» test() { println!(); }
1171        "});
1172
1173        cx.editor(|editor, _window, _cx| {
1174            assert!(!editor.hover_state.visible());
1175
1176            assert_eq!(
1177                editor.hover_state.info_popovers.len(),
1178                0,
1179                "Expected no hovers but got but got: {:?}",
1180                editor.hover_state.info_popovers
1181            );
1182        });
1183
1184        let mut requests =
1185            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1186                Ok(Some(lsp::Hover {
1187                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1188                        kind: lsp::MarkupKind::Markdown,
1189                        value: "some other basic docs".to_string(),
1190                    }),
1191                    range: Some(symbol_range),
1192                }))
1193            });
1194
1195        requests.next().await;
1196        cx.dispatch_action(Hover);
1197
1198        cx.condition(|editor, _| editor.hover_state.visible()).await;
1199        cx.editor(|editor, _, cx| {
1200            assert_eq!(
1201                editor.hover_state.info_popovers.len(),
1202                1,
1203                "Expected exactly one hover but got: {:?}",
1204                editor.hover_state.info_popovers
1205            );
1206
1207            let rendered_text = editor
1208                .hover_state
1209                .info_popovers
1210                .first()
1211                .unwrap()
1212                .get_rendered_text(cx);
1213
1214            assert_eq!(rendered_text, "some other basic docs".to_string())
1215        });
1216    }
1217
1218    #[gpui::test]
1219    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1220        init_test(cx, |_| {});
1221
1222        let mut cx = EditorLspTestContext::new_rust(
1223            lsp::ServerCapabilities {
1224                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1225                ..Default::default()
1226            },
1227            cx,
1228        )
1229        .await;
1230
1231        // Hover with keyboard has no delay
1232        cx.set_state(indoc! {"
1233            fˇn test() { println!(); }
1234        "});
1235        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1236        let symbol_range = cx.lsp_range(indoc! {"
1237            «fn» test() { println!(); }
1238        "});
1239        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1240            Ok(Some(lsp::Hover {
1241                contents: lsp::HoverContents::Array(vec![
1242                    lsp::MarkedString::String("regular text for hover to show".to_string()),
1243                    lsp::MarkedString::String("".to_string()),
1244                    lsp::MarkedString::LanguageString(lsp::LanguageString {
1245                        language: "Rust".to_string(),
1246                        value: "".to_string(),
1247                    }),
1248                ]),
1249                range: Some(symbol_range),
1250            }))
1251        })
1252        .next()
1253        .await;
1254        cx.dispatch_action(Hover);
1255
1256        cx.condition(|editor, _| editor.hover_state.visible()).await;
1257        cx.editor(|editor, _, cx| {
1258            assert_eq!(
1259                editor.hover_state.info_popovers.len(),
1260                1,
1261                "Expected exactly one hover but got: {:?}",
1262                editor.hover_state.info_popovers
1263            );
1264            let rendered_text = editor
1265                .hover_state
1266                .info_popovers
1267                .first()
1268                .unwrap()
1269                .get_rendered_text(cx);
1270
1271            assert_eq!(
1272                rendered_text,
1273                "regular text for hover to show".to_string(),
1274                "No empty string hovers should be shown"
1275            );
1276        });
1277    }
1278
1279    #[gpui::test]
1280    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1281        init_test(cx, |_| {});
1282
1283        let mut cx = EditorLspTestContext::new_rust(
1284            lsp::ServerCapabilities {
1285                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1286                ..Default::default()
1287            },
1288            cx,
1289        )
1290        .await;
1291
1292        // Hover with keyboard has no delay
1293        cx.set_state(indoc! {"
1294            fˇn test() { println!(); }
1295        "});
1296        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1297        let symbol_range = cx.lsp_range(indoc! {"
1298            «fn» test() { println!(); }
1299        "});
1300
1301        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1302        let markdown_string = format!("\n```rust\n{code_str}```");
1303
1304        let closure_markdown_string = markdown_string.clone();
1305        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1306            let future_markdown_string = closure_markdown_string.clone();
1307            async move {
1308                Ok(Some(lsp::Hover {
1309                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1310                        kind: lsp::MarkupKind::Markdown,
1311                        value: future_markdown_string,
1312                    }),
1313                    range: Some(symbol_range),
1314                }))
1315            }
1316        })
1317        .next()
1318        .await;
1319
1320        cx.dispatch_action(Hover);
1321
1322        cx.condition(|editor, _| editor.hover_state.visible()).await;
1323        cx.editor(|editor, _, cx| {
1324            assert_eq!(
1325                editor.hover_state.info_popovers.len(),
1326                1,
1327                "Expected exactly one hover but got: {:?}",
1328                editor.hover_state.info_popovers
1329            );
1330            let rendered_text = editor
1331                .hover_state
1332                .info_popovers
1333                .first()
1334                .unwrap()
1335                .get_rendered_text(cx);
1336
1337            assert_eq!(
1338                rendered_text, code_str,
1339                "Should not have extra line breaks at end of rendered hover"
1340            );
1341        });
1342    }
1343
1344    #[gpui::test]
1345    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1346        init_test(cx, |_| {});
1347
1348        let mut cx = EditorLspTestContext::new_rust(
1349            lsp::ServerCapabilities {
1350                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1351                ..Default::default()
1352            },
1353            cx,
1354        )
1355        .await;
1356
1357        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1358        // info popover once request completes
1359        cx.set_state(indoc! {"
1360            fn teˇst() { println!(); }
1361        "});
1362
1363        // Send diagnostic to client
1364        let range = cx.text_anchor_range(indoc! {"
1365            fn «test»() { println!(); }
1366        "});
1367        cx.update_buffer(|buffer, cx| {
1368            let snapshot = buffer.text_snapshot();
1369            let set = DiagnosticSet::from_sorted_entries(
1370                vec![DiagnosticEntry {
1371                    range,
1372                    diagnostic: Diagnostic {
1373                        message: "A test diagnostic message.".to_string(),
1374                        ..Default::default()
1375                    },
1376                }],
1377                &snapshot,
1378            );
1379            buffer.update_diagnostics(LanguageServerId(0), set, cx);
1380        });
1381
1382        // Hover pops diagnostic immediately
1383        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1384        cx.background_executor.run_until_parked();
1385
1386        cx.editor(|Editor { hover_state, .. }, _, _| {
1387            assert!(
1388                hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1389            )
1390        });
1391
1392        // Info Popover shows after request responded to
1393        let range = cx.lsp_range(indoc! {"
1394            fn «test»() { println!(); }
1395        "});
1396        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1397            Ok(Some(lsp::Hover {
1398                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1399                    kind: lsp::MarkupKind::Markdown,
1400                    value: "some new docs".to_string(),
1401                }),
1402                range: Some(range),
1403            }))
1404        });
1405        cx.background_executor
1406            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1407
1408        cx.background_executor.run_until_parked();
1409        cx.editor(|Editor { hover_state, .. }, _, _| {
1410            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1411        });
1412    }
1413
1414    #[gpui::test]
1415    // https://github.com/zed-industries/zed/issues/15498
1416    async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1417        init_test(cx, |_| {});
1418
1419        let mut cx = EditorLspTestContext::new_rust(
1420            lsp::ServerCapabilities {
1421                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1422                ..Default::default()
1423            },
1424            cx,
1425        )
1426        .await;
1427
1428        cx.set_state(indoc! {"
1429            fn fuˇnc(abc def: i32) -> u32 {
1430            }
1431        "});
1432
1433        cx.lsp.handle_request::<lsp::request::HoverRequest, _, _>({
1434            |_, _| async move {
1435                Ok(Some(lsp::Hover {
1436                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1437                        kind: lsp::MarkupKind::Markdown,
1438                        value: indoc!(
1439                            r#"
1440                    ### function `errands_data_read`
1441
1442                    ---
1443                    → `char *`
1444                    Function to read a file into a string
1445
1446                    ---
1447                    ```cpp
1448                    static char *errands_data_read()
1449                    ```
1450                    "#
1451                        )
1452                        .to_string(),
1453                    }),
1454                    range: None,
1455                }))
1456            }
1457        });
1458        cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1459        cx.run_until_parked();
1460
1461        cx.update_editor(|editor, _, cx| {
1462            let popover = editor.hover_state.info_popovers.first().unwrap();
1463            let content = popover.get_rendered_text(cx);
1464
1465            assert!(content.contains("Function to read a file"));
1466        });
1467    }
1468
1469    #[gpui::test]
1470    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1471        init_test(cx, |settings| {
1472            settings.defaults.inlay_hints = Some(InlayHintSettings {
1473                enabled: true,
1474                edit_debounce_ms: 0,
1475                scroll_debounce_ms: 0,
1476                show_type_hints: true,
1477                show_parameter_hints: true,
1478                show_other_hints: true,
1479                show_background: false,
1480            })
1481        });
1482
1483        let mut cx = EditorLspTestContext::new_rust(
1484            lsp::ServerCapabilities {
1485                inlay_hint_provider: Some(lsp::OneOf::Right(
1486                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1487                        resolve_provider: Some(true),
1488                        ..Default::default()
1489                    }),
1490                )),
1491                ..Default::default()
1492            },
1493            cx,
1494        )
1495        .await;
1496
1497        cx.set_state(indoc! {"
1498            struct TestStruct;
1499
1500            // ==================
1501
1502            struct TestNewType<T>(T);
1503
1504            fn main() {
1505                let variableˇ = TestNewType(TestStruct);
1506            }
1507        "});
1508
1509        let hint_start_offset = cx.ranges(indoc! {"
1510            struct TestStruct;
1511
1512            // ==================
1513
1514            struct TestNewType<T>(T);
1515
1516            fn main() {
1517                let variableˇ = TestNewType(TestStruct);
1518            }
1519        "})[0]
1520            .start;
1521        let hint_position = cx.to_lsp(hint_start_offset);
1522        let new_type_target_range = cx.lsp_range(indoc! {"
1523            struct TestStruct;
1524
1525            // ==================
1526
1527            struct «TestNewType»<T>(T);
1528
1529            fn main() {
1530                let variable = TestNewType(TestStruct);
1531            }
1532        "});
1533        let struct_target_range = cx.lsp_range(indoc! {"
1534            struct «TestStruct»;
1535
1536            // ==================
1537
1538            struct TestNewType<T>(T);
1539
1540            fn main() {
1541                let variable = TestNewType(TestStruct);
1542            }
1543        "});
1544
1545        let uri = cx.buffer_lsp_url.clone();
1546        let new_type_label = "TestNewType";
1547        let struct_label = "TestStruct";
1548        let entire_hint_label = ": TestNewType<TestStruct>";
1549        let closure_uri = uri.clone();
1550        cx.lsp
1551            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1552                let task_uri = closure_uri.clone();
1553                async move {
1554                    assert_eq!(params.text_document.uri, task_uri);
1555                    Ok(Some(vec![lsp::InlayHint {
1556                        position: hint_position,
1557                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1558                            value: entire_hint_label.to_string(),
1559                            ..Default::default()
1560                        }]),
1561                        kind: Some(lsp::InlayHintKind::TYPE),
1562                        text_edits: None,
1563                        tooltip: None,
1564                        padding_left: Some(false),
1565                        padding_right: Some(false),
1566                        data: None,
1567                    }]))
1568                }
1569            })
1570            .next()
1571            .await;
1572        cx.background_executor.run_until_parked();
1573        cx.update_editor(|editor, _, cx| {
1574            let expected_layers = vec![entire_hint_label.to_string()];
1575            assert_eq!(expected_layers, cached_hint_labels(editor));
1576            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1577        });
1578
1579        let inlay_range = cx
1580            .ranges(indoc! {"
1581                struct TestStruct;
1582
1583                // ==================
1584
1585                struct TestNewType<T>(T);
1586
1587                fn main() {
1588                    let variable« »= TestNewType(TestStruct);
1589                }
1590        "})
1591            .first()
1592            .cloned()
1593            .unwrap();
1594        let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1595            let snapshot = editor.snapshot(window, cx);
1596            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1597            let next_valid = inlay_range.end.to_display_point(&snapshot);
1598            assert_eq!(previous_valid.row(), next_valid.row());
1599            assert!(previous_valid.column() < next_valid.column());
1600            let exact_unclipped = DisplayPoint::new(
1601                previous_valid.row(),
1602                previous_valid.column()
1603                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1604                        as u32,
1605            );
1606            PointForPosition {
1607                previous_valid,
1608                next_valid,
1609                exact_unclipped,
1610                column_overshoot_after_line_end: 0,
1611            }
1612        });
1613        cx.update_editor(|editor, window, cx| {
1614            update_inlay_link_and_hover_points(
1615                &editor.snapshot(window, cx),
1616                new_type_hint_part_hover_position,
1617                editor,
1618                true,
1619                false,
1620                window,
1621                cx,
1622            );
1623        });
1624
1625        let resolve_closure_uri = uri.clone();
1626        cx.lsp
1627            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1628                move |mut hint_to_resolve, _| {
1629                    let mut resolved_hint_positions = BTreeSet::new();
1630                    let task_uri = resolve_closure_uri.clone();
1631                    async move {
1632                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1633                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1634
1635                        // `: TestNewType<TestStruct>`
1636                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1637                            lsp::InlayHintLabelPart {
1638                                value: ": ".to_string(),
1639                                ..Default::default()
1640                            },
1641                            lsp::InlayHintLabelPart {
1642                                value: new_type_label.to_string(),
1643                                location: Some(lsp::Location {
1644                                    uri: task_uri.clone(),
1645                                    range: new_type_target_range,
1646                                }),
1647                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1648                                    "A tooltip for `{new_type_label}`"
1649                                ))),
1650                                ..Default::default()
1651                            },
1652                            lsp::InlayHintLabelPart {
1653                                value: "<".to_string(),
1654                                ..Default::default()
1655                            },
1656                            lsp::InlayHintLabelPart {
1657                                value: struct_label.to_string(),
1658                                location: Some(lsp::Location {
1659                                    uri: task_uri,
1660                                    range: struct_target_range,
1661                                }),
1662                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1663                                    lsp::MarkupContent {
1664                                        kind: lsp::MarkupKind::Markdown,
1665                                        value: format!("A tooltip for `{struct_label}`"),
1666                                    },
1667                                )),
1668                                ..Default::default()
1669                            },
1670                            lsp::InlayHintLabelPart {
1671                                value: ">".to_string(),
1672                                ..Default::default()
1673                            },
1674                        ]);
1675
1676                        Ok(hint_to_resolve)
1677                    }
1678                },
1679            )
1680            .next()
1681            .await;
1682        cx.background_executor.run_until_parked();
1683
1684        cx.update_editor(|editor, window, cx| {
1685            update_inlay_link_and_hover_points(
1686                &editor.snapshot(window, cx),
1687                new_type_hint_part_hover_position,
1688                editor,
1689                true,
1690                false,
1691                window,
1692                cx,
1693            );
1694        });
1695        cx.background_executor
1696            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1697        cx.background_executor.run_until_parked();
1698        cx.update_editor(|editor, _, cx| {
1699            let hover_state = &editor.hover_state;
1700            assert!(
1701                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1702            );
1703            let popover = hover_state.info_popovers.first().cloned().unwrap();
1704            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1705            assert_eq!(
1706                popover.symbol_range,
1707                RangeInEditor::Inlay(InlayHighlight {
1708                    inlay: InlayId::Hint(0),
1709                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1710                    range: ": ".len()..": ".len() + new_type_label.len(),
1711                }),
1712                "Popover range should match the new type label part"
1713            );
1714            assert_eq!(
1715                popover.get_rendered_text(cx),
1716                format!("A tooltip for {new_type_label}"),
1717            );
1718        });
1719
1720        let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1721            let snapshot = editor.snapshot(window, cx);
1722            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1723            let next_valid = inlay_range.end.to_display_point(&snapshot);
1724            assert_eq!(previous_valid.row(), next_valid.row());
1725            assert!(previous_valid.column() < next_valid.column());
1726            let exact_unclipped = DisplayPoint::new(
1727                previous_valid.row(),
1728                previous_valid.column()
1729                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1730                        as u32,
1731            );
1732            PointForPosition {
1733                previous_valid,
1734                next_valid,
1735                exact_unclipped,
1736                column_overshoot_after_line_end: 0,
1737            }
1738        });
1739        cx.update_editor(|editor, window, cx| {
1740            update_inlay_link_and_hover_points(
1741                &editor.snapshot(window, cx),
1742                struct_hint_part_hover_position,
1743                editor,
1744                true,
1745                false,
1746                window,
1747                cx,
1748            );
1749        });
1750        cx.background_executor
1751            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1752        cx.background_executor.run_until_parked();
1753        cx.update_editor(|editor, _, cx| {
1754            let hover_state = &editor.hover_state;
1755            assert!(
1756                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1757            );
1758            let popover = hover_state.info_popovers.first().cloned().unwrap();
1759            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1760            assert_eq!(
1761                popover.symbol_range,
1762                RangeInEditor::Inlay(InlayHighlight {
1763                    inlay: InlayId::Hint(0),
1764                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1765                    range: ": ".len() + new_type_label.len() + "<".len()
1766                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1767                }),
1768                "Popover range should match the struct label part"
1769            );
1770            assert_eq!(
1771                popover.get_rendered_text(cx),
1772                format!("A tooltip for {struct_label}"),
1773                "Rendered markdown element should remove backticks from text"
1774            );
1775        });
1776    }
1777}