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