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