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