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