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