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