hover_popover.rs

   1use crate::{
   2    display_map::{InlayOffset, ToDisplayPoint},
   3    link_go_to_definition::{InlayHighlight, RangeInEditor},
   4    Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
   5    ExcerptId, RangeToAnchorExt,
   6};
   7use futures::FutureExt;
   8use gpui::{
   9    actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model,
  10    MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,
  11    Task, ViewContext, WeakView,
  12};
  13use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
  14use lsp::DiagnosticSeverity;
  15use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
  16use settings::Settings;
  17use std::{ops::Range, sync::Arc, time::Duration};
  18use ui::Tooltip;
  19use util::TryFutureExt;
  20use workspace::Workspace;
  21
  22pub const HOVER_DELAY_MILLIS: u64 = 350;
  23pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
  24
  25pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
  26pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
  27pub const HOVER_POPOVER_GAP: Pixels = px(10.);
  28
  29actions!(Hover);
  30
  31/// Bindable action which uses the most recent selection head to trigger a hover
  32pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
  33    let head = editor.selections.newest_display(cx).head();
  34    show_hover(editor, head, true, cx);
  35}
  36
  37/// The internal hover action dispatches between `show_hover` or `hide_hover`
  38/// depending on whether a point to hover over is provided.
  39pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
  40    if EditorSettings::get_global(cx).hover_popover_enabled {
  41        if let Some(point) = point {
  42            show_hover(editor, point, false, cx);
  43        } else {
  44            hide_hover(editor, cx);
  45        }
  46    }
  47}
  48
  49pub struct InlayHover {
  50    pub excerpt: ExcerptId,
  51    pub range: InlayHighlight,
  52    pub tooltip: HoverBlock,
  53}
  54
  55pub fn find_hovered_hint_part(
  56    label_parts: Vec<InlayHintLabelPart>,
  57    hint_start: InlayOffset,
  58    hovered_offset: InlayOffset,
  59) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
  60    if hovered_offset >= hint_start {
  61        let mut hovered_character = (hovered_offset - hint_start).0;
  62        let mut part_start = hint_start;
  63        for part in label_parts {
  64            let part_len = part.value.chars().count();
  65            if hovered_character > part_len {
  66                hovered_character -= part_len;
  67                part_start.0 += part_len;
  68            } else {
  69                let part_end = InlayOffset(part_start.0 + part_len);
  70                return Some((part, part_start..part_end));
  71            }
  72        }
  73    }
  74    None
  75}
  76
  77pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
  78    if EditorSettings::get_global(cx).hover_popover_enabled {
  79        if editor.pending_rename.is_some() {
  80            return;
  81        }
  82
  83        let Some(project) = editor.project.clone() else {
  84            return;
  85        };
  86
  87        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
  88            if let RangeInEditor::Inlay(range) = symbol_range {
  89                if range == &inlay_hover.range {
  90                    // Hover triggered from same location as last time. Don't show again.
  91                    return;
  92                }
  93            }
  94            hide_hover(editor, cx);
  95        }
  96
  97        let task = cx.spawn(|this, mut cx| {
  98            async move {
  99                cx.background_executor()
 100                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
 101                    .await;
 102                this.update(&mut cx, |this, _| {
 103                    this.hover_state.diagnostic_popover = None;
 104                })?;
 105
 106                let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
 107                let blocks = vec![inlay_hover.tooltip];
 108                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
 109
 110                let hover_popover = InfoPopover {
 111                    project: project.clone(),
 112                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
 113                    blocks,
 114                    parsed_content,
 115                };
 116
 117                this.update(&mut cx, |this, cx| {
 118                    // Highlight the selected symbol using a background highlight
 119                    this.highlight_inlay_background::<HoverState>(
 120                        vec![inlay_hover.range],
 121                        |theme| theme.element_hover, // todo!("use a proper background here")
 122                        cx,
 123                    );
 124                    this.hover_state.info_popover = Some(hover_popover);
 125                    cx.notify();
 126                })?;
 127
 128                anyhow::Ok(())
 129            }
 130            .log_err()
 131        });
 132
 133        editor.hover_state.info_task = Some(task);
 134    }
 135}
 136
 137/// Hides the type information popup.
 138/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 139/// selections changed.
 140pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
 141    let did_hide = editor.hover_state.info_popover.take().is_some()
 142        | editor.hover_state.diagnostic_popover.take().is_some();
 143
 144    editor.hover_state.info_task = None;
 145    editor.hover_state.triggered_from = None;
 146
 147    editor.clear_background_highlights::<HoverState>(cx);
 148
 149    if did_hide {
 150        cx.notify();
 151    }
 152
 153    did_hide
 154}
 155
 156/// Queries the LSP and shows type info and documentation
 157/// about the symbol the mouse is currently hovering over.
 158/// Triggered by the `Hover` action when the cursor may be over a symbol.
 159fn show_hover(
 160    editor: &mut Editor,
 161    point: DisplayPoint,
 162    ignore_timeout: bool,
 163    cx: &mut ViewContext<Editor>,
 164) {
 165    if editor.pending_rename.is_some() {
 166        return;
 167    }
 168
 169    let snapshot = editor.snapshot(cx);
 170    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
 171
 172    let (buffer, buffer_position) = if let Some(output) = editor
 173        .buffer
 174        .read(cx)
 175        .text_anchor_for_position(multibuffer_offset, cx)
 176    {
 177        output
 178    } else {
 179        return;
 180    };
 181
 182    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
 183        .buffer()
 184        .read(cx)
 185        .excerpt_containing(multibuffer_offset, cx)
 186    {
 187        excerpt_id
 188    } else {
 189        return;
 190    };
 191
 192    let project = if let Some(project) = editor.project.clone() {
 193        project
 194    } else {
 195        return;
 196    };
 197
 198    if !ignore_timeout {
 199        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
 200            if symbol_range
 201                .as_text_range()
 202                .map(|range| {
 203                    range
 204                        .to_offset(&snapshot.buffer_snapshot)
 205                        .contains(&multibuffer_offset)
 206                })
 207                .unwrap_or(false)
 208            {
 209                // Hover triggered from same location as last time. Don't show again.
 210                return;
 211            } else {
 212                hide_hover(editor, cx);
 213            }
 214        }
 215    }
 216
 217    // Get input anchor
 218    let anchor = snapshot
 219        .buffer_snapshot
 220        .anchor_at(multibuffer_offset, Bias::Left);
 221
 222    // Don't request again if the location is the same as the previous request
 223    if let Some(triggered_from) = &editor.hover_state.triggered_from {
 224        if triggered_from
 225            .cmp(&anchor, &snapshot.buffer_snapshot)
 226            .is_eq()
 227        {
 228            return;
 229        }
 230    }
 231
 232    let task = cx.spawn(|this, mut cx| {
 233        async move {
 234            // If we need to delay, delay a set amount initially before making the lsp request
 235            let delay = if !ignore_timeout {
 236                // Construct delay task to wait for later
 237                let total_delay = Some(
 238                    cx.background_executor()
 239                        .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
 240                );
 241
 242                cx.background_executor()
 243                    .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
 244                    .await;
 245                total_delay
 246            } else {
 247                None
 248            };
 249
 250            // query the LSP for hover info
 251            let hover_request = cx.update(|_, cx| {
 252                project.update(cx, |project, cx| {
 253                    project.hover(&buffer, buffer_position, cx)
 254                })
 255            })?;
 256
 257            if let Some(delay) = delay {
 258                delay.await;
 259            }
 260
 261            // If there's a diagnostic, assign it on the hover state and notify
 262            let local_diagnostic = snapshot
 263                .buffer_snapshot
 264                .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
 265                // Find the entry with the most specific range
 266                .min_by_key(|entry| entry.range.end - entry.range.start)
 267                .map(|entry| DiagnosticEntry {
 268                    diagnostic: entry.diagnostic,
 269                    range: entry.range.to_anchors(&snapshot.buffer_snapshot),
 270                });
 271
 272            // Pull the primary diagnostic out so we can jump to it if the popover is clicked
 273            let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
 274                snapshot
 275                    .buffer_snapshot
 276                    .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
 277                    .find(|diagnostic| diagnostic.diagnostic.is_primary)
 278                    .map(|entry| DiagnosticEntry {
 279                        diagnostic: entry.diagnostic,
 280                        range: entry.range.to_anchors(&snapshot.buffer_snapshot),
 281                    })
 282            });
 283
 284            this.update(&mut cx, |this, _| {
 285                this.hover_state.diagnostic_popover =
 286                    local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
 287                        local_diagnostic,
 288                        primary_diagnostic,
 289                    });
 290            })?;
 291
 292            let hover_result = hover_request.await.ok().flatten();
 293            let hover_popover = match hover_result {
 294                Some(hover_result) if !hover_result.is_empty() => {
 295                    // Create symbol range of anchors for highlighting and filtering of future requests.
 296                    let range = if let Some(range) = hover_result.range {
 297                        let start = snapshot
 298                            .buffer_snapshot
 299                            .anchor_in_excerpt(excerpt_id.clone(), range.start);
 300                        let end = snapshot
 301                            .buffer_snapshot
 302                            .anchor_in_excerpt(excerpt_id.clone(), range.end);
 303
 304                        start..end
 305                    } else {
 306                        anchor..anchor
 307                    };
 308
 309                    let language_registry =
 310                        project.update(&mut cx, |p, _| p.languages().clone())?;
 311                    let blocks = hover_result.contents;
 312                    let language = hover_result.language;
 313                    let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
 314
 315                    Some(InfoPopover {
 316                        project: project.clone(),
 317                        symbol_range: RangeInEditor::Text(range),
 318                        blocks,
 319                        parsed_content,
 320                    })
 321                }
 322
 323                _ => None,
 324            };
 325
 326            this.update(&mut cx, |this, cx| {
 327                if let Some(symbol_range) = hover_popover
 328                    .as_ref()
 329                    .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
 330                {
 331                    // Highlight the selected symbol using a background highlight
 332                    this.highlight_background::<HoverState>(
 333                        vec![symbol_range],
 334                        |theme| theme.element_hover, // todo! update theme
 335                        cx,
 336                    );
 337                } else {
 338                    this.clear_background_highlights::<HoverState>(cx);
 339                }
 340
 341                this.hover_state.info_popover = hover_popover;
 342                cx.notify();
 343            })?;
 344
 345            Ok::<_, anyhow::Error>(())
 346        }
 347        .log_err()
 348    });
 349
 350    editor.hover_state.info_task = Some(task);
 351}
 352
 353async fn parse_blocks(
 354    blocks: &[HoverBlock],
 355    language_registry: &Arc<LanguageRegistry>,
 356    language: Option<Arc<Language>>,
 357) -> markdown::ParsedMarkdown {
 358    let mut text = String::new();
 359    let mut highlights = Vec::new();
 360    let mut region_ranges = Vec::new();
 361    let mut regions = Vec::new();
 362
 363    for block in blocks {
 364        match &block.kind {
 365            HoverBlockKind::PlainText => {
 366                markdown::new_paragraph(&mut text, &mut Vec::new());
 367                text.push_str(&block.text);
 368            }
 369
 370            HoverBlockKind::Markdown => {
 371                markdown::parse_markdown_block(
 372                    &block.text,
 373                    language_registry,
 374                    language.clone(),
 375                    &mut text,
 376                    &mut highlights,
 377                    &mut region_ranges,
 378                    &mut regions,
 379                )
 380                .await
 381            }
 382
 383            HoverBlockKind::Code { language } => {
 384                if let Some(language) = language_registry
 385                    .language_for_name(language)
 386                    .now_or_never()
 387                    .and_then(Result::ok)
 388                {
 389                    markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
 390                } else {
 391                    text.push_str(&block.text);
 392                }
 393            }
 394        }
 395    }
 396
 397    ParsedMarkdown {
 398        text: text.trim().to_string(),
 399        highlights,
 400        region_ranges,
 401        regions,
 402    }
 403}
 404
 405#[derive(Default)]
 406pub struct HoverState {
 407    pub info_popover: Option<InfoPopover>,
 408    pub diagnostic_popover: Option<DiagnosticPopover>,
 409    pub triggered_from: Option<Anchor>,
 410    pub info_task: Option<Task<Option<()>>>,
 411}
 412
 413impl HoverState {
 414    pub fn visible(&self) -> bool {
 415        self.info_popover.is_some() || self.diagnostic_popover.is_some()
 416    }
 417
 418    pub fn render(
 419        &mut self,
 420        snapshot: &EditorSnapshot,
 421        style: &EditorStyle,
 422        visible_rows: Range<u32>,
 423        max_size: Size<Pixels>,
 424        workspace: Option<WeakView<Workspace>>,
 425        cx: &mut ViewContext<Editor>,
 426    ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
 427        // If there is a diagnostic, position the popovers based on that.
 428        // Otherwise use the start of the hover range
 429        let anchor = self
 430            .diagnostic_popover
 431            .as_ref()
 432            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
 433            .or_else(|| {
 434                self.info_popover
 435                    .as_ref()
 436                    .map(|info_popover| match &info_popover.symbol_range {
 437                        RangeInEditor::Text(range) => &range.start,
 438                        RangeInEditor::Inlay(range) => &range.inlay_position,
 439                    })
 440            })?;
 441        let point = anchor.to_display_point(&snapshot.display_snapshot);
 442
 443        // Don't render if the relevant point isn't on screen
 444        if !self.visible() || !visible_rows.contains(&point.row()) {
 445            return None;
 446        }
 447
 448        let mut elements = Vec::new();
 449
 450        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
 451            elements.push(diagnostic_popover.render(style, max_size, cx));
 452        }
 453        if let Some(info_popover) = self.info_popover.as_mut() {
 454            elements.push(info_popover.render(style, max_size, workspace, cx));
 455        }
 456
 457        Some((point, elements))
 458    }
 459}
 460
 461#[derive(Debug, Clone)]
 462pub struct InfoPopover {
 463    pub project: Model<Project>,
 464    symbol_range: RangeInEditor,
 465    pub blocks: Vec<HoverBlock>,
 466    parsed_content: ParsedMarkdown,
 467}
 468
 469impl InfoPopover {
 470    pub fn render(
 471        &mut self,
 472        style: &EditorStyle,
 473        max_size: Size<Pixels>,
 474        workspace: Option<WeakView<Workspace>>,
 475        cx: &mut ViewContext<Editor>,
 476    ) -> AnyElement {
 477        div()
 478            .id("info_popover")
 479            .overflow_y_scroll()
 480            .bg(gpui::red())
 481            .max_w(max_size.width)
 482            .max_h(max_size.height)
 483            // Prevent a mouse move on the popover from being propagated to the editor,
 484            // because that would dismiss the popover.
 485            .on_mouse_move(|_, cx| cx.stop_propagation())
 486            .child(crate::render_parsed_markdown(
 487                "content",
 488                &self.parsed_content,
 489                style,
 490                workspace,
 491                cx,
 492            ))
 493            .into_any_element()
 494    }
 495}
 496
 497#[derive(Debug, Clone)]
 498pub struct DiagnosticPopover {
 499    local_diagnostic: DiagnosticEntry<Anchor>,
 500    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
 501}
 502
 503impl DiagnosticPopover {
 504    pub fn render(
 505        &self,
 506        style: &EditorStyle,
 507        max_size: Size<Pixels>,
 508        cx: &mut ViewContext<Editor>,
 509    ) -> AnyElement {
 510        let text = match &self.local_diagnostic.diagnostic.source {
 511            Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
 512            None => self.local_diagnostic.diagnostic.message.clone(),
 513        };
 514
 515        let container_bg = crate::diagnostic_style(
 516            self.local_diagnostic.diagnostic.severity,
 517            true,
 518            &style.diagnostic_style,
 519        );
 520
 521        div()
 522            .id("diagnostic")
 523            .overflow_y_scroll()
 524            .bg(container_bg)
 525            .max_w(max_size.width)
 526            .max_h(max_size.height)
 527            .cursor(CursorStyle::PointingHand)
 528            .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
 529            // Prevent a mouse move on the popover from being propagated to the editor,
 530            // because that would dismiss the popover.
 531            .on_mouse_move(|_, cx| cx.stop_propagation())
 532            // Prevent a mouse down on the popover from being propagated to the editor,
 533            // because that would move the cursor.
 534            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
 535            .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
 536            .child(SharedString::from(text))
 537            .into_any_element()
 538    }
 539
 540    pub fn activation_info(&self) -> (usize, Anchor) {
 541        let entry = self
 542            .primary_diagnostic
 543            .as_ref()
 544            .unwrap_or(&self.local_diagnostic);
 545
 546        (entry.diagnostic.group_id, entry.range.start.clone())
 547    }
 548}
 549
 550#[cfg(test)]
 551mod tests {
 552    use super::*;
 553    use crate::{
 554        editor_tests::init_test,
 555        element::PointForPosition,
 556        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
 557        link_go_to_definition::update_inlay_link_and_hover_points,
 558        test::editor_lsp_test_context::EditorLspTestContext,
 559        InlayId,
 560    };
 561    use collections::BTreeSet;
 562    use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
 563    use indoc::indoc;
 564    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
 565    use lsp::LanguageServerId;
 566    use project::{HoverBlock, HoverBlockKind};
 567    use smol::stream::StreamExt;
 568    use unindent::Unindent;
 569    use util::test::marked_text_ranges;
 570
 571    #[gpui::test]
 572    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
 573        init_test(cx, |_| {});
 574
 575        let mut cx = EditorLspTestContext::new_rust(
 576            lsp::ServerCapabilities {
 577                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 578                ..Default::default()
 579            },
 580            cx,
 581        )
 582        .await;
 583
 584        // Basic hover delays and then pops without moving the mouse
 585        cx.set_state(indoc! {"
 586            fn ˇtest() { println!(); }
 587        "});
 588        let hover_point = cx.display_point(indoc! {"
 589            fn test() { printˇln!(); }
 590        "});
 591
 592        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 593        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 594
 595        // After delay, hover should be visible.
 596        let symbol_range = cx.lsp_range(indoc! {"
 597            fn test() { «println!»(); }
 598        "});
 599        let mut requests =
 600            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 601                Ok(Some(lsp::Hover {
 602                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 603                        kind: lsp::MarkupKind::Markdown,
 604                        value: "some basic docs".to_string(),
 605                    }),
 606                    range: Some(symbol_range),
 607                }))
 608            });
 609        cx.background_executor
 610            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 611        requests.next().await;
 612
 613        cx.editor(|editor, _| {
 614            assert!(editor.hover_state.visible());
 615            assert_eq!(
 616                editor.hover_state.info_popover.clone().unwrap().blocks,
 617                vec![HoverBlock {
 618                    text: "some basic docs".to_string(),
 619                    kind: HoverBlockKind::Markdown,
 620                },]
 621            )
 622        });
 623
 624        // Mouse moved with no hover response dismisses
 625        let hover_point = cx.display_point(indoc! {"
 626            fn teˇst() { println!(); }
 627        "});
 628        let mut request = cx
 629            .lsp
 630            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
 631        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 632        cx.background_executor
 633            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 634        request.next().await;
 635        cx.editor(|editor, _| {
 636            assert!(!editor.hover_state.visible());
 637        });
 638    }
 639
 640    #[gpui::test]
 641    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
 642        init_test(cx, |_| {});
 643
 644        let mut cx = EditorLspTestContext::new_rust(
 645            lsp::ServerCapabilities {
 646                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 647                ..Default::default()
 648            },
 649            cx,
 650        )
 651        .await;
 652
 653        // Hover with keyboard has no delay
 654        cx.set_state(indoc! {"
 655            fˇn test() { println!(); }
 656        "});
 657        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 658        let symbol_range = cx.lsp_range(indoc! {"
 659            «fn» test() { println!(); }
 660        "});
 661        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 662            Ok(Some(lsp::Hover {
 663                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 664                    kind: lsp::MarkupKind::Markdown,
 665                    value: "some other basic docs".to_string(),
 666                }),
 667                range: Some(symbol_range),
 668            }))
 669        })
 670        .next()
 671        .await;
 672
 673        cx.condition(|editor, _| editor.hover_state.visible()).await;
 674        cx.editor(|editor, _| {
 675            assert_eq!(
 676                editor.hover_state.info_popover.clone().unwrap().blocks,
 677                vec![HoverBlock {
 678                    text: "some other basic docs".to_string(),
 679                    kind: HoverBlockKind::Markdown,
 680                }]
 681            )
 682        });
 683    }
 684
 685    #[gpui::test]
 686    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
 687        init_test(cx, |_| {});
 688
 689        let mut cx = EditorLspTestContext::new_rust(
 690            lsp::ServerCapabilities {
 691                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 692                ..Default::default()
 693            },
 694            cx,
 695        )
 696        .await;
 697
 698        // Hover with keyboard has no delay
 699        cx.set_state(indoc! {"
 700            fˇn test() { println!(); }
 701        "});
 702        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 703        let symbol_range = cx.lsp_range(indoc! {"
 704            «fn» test() { println!(); }
 705        "});
 706        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 707            Ok(Some(lsp::Hover {
 708                contents: lsp::HoverContents::Array(vec![
 709                    lsp::MarkedString::String("regular text for hover to show".to_string()),
 710                    lsp::MarkedString::String("".to_string()),
 711                    lsp::MarkedString::LanguageString(lsp::LanguageString {
 712                        language: "Rust".to_string(),
 713                        value: "".to_string(),
 714                    }),
 715                ]),
 716                range: Some(symbol_range),
 717            }))
 718        })
 719        .next()
 720        .await;
 721
 722        cx.condition(|editor, _| editor.hover_state.visible()).await;
 723        cx.editor(|editor, _| {
 724            assert_eq!(
 725                editor.hover_state.info_popover.clone().unwrap().blocks,
 726                vec![HoverBlock {
 727                    text: "regular text for hover to show".to_string(),
 728                    kind: HoverBlockKind::Markdown,
 729                }],
 730                "No empty string hovers should be shown"
 731            );
 732        });
 733    }
 734
 735    #[gpui::test]
 736    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
 737        init_test(cx, |_| {});
 738
 739        let mut cx = EditorLspTestContext::new_rust(
 740            lsp::ServerCapabilities {
 741                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 742                ..Default::default()
 743            },
 744            cx,
 745        )
 746        .await;
 747
 748        // Hover with keyboard has no delay
 749        cx.set_state(indoc! {"
 750            fˇn test() { println!(); }
 751        "});
 752        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 753        let symbol_range = cx.lsp_range(indoc! {"
 754            «fn» test() { println!(); }
 755        "});
 756
 757        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
 758        let markdown_string = format!("\n```rust\n{code_str}```");
 759
 760        let closure_markdown_string = markdown_string.clone();
 761        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
 762            let future_markdown_string = closure_markdown_string.clone();
 763            async move {
 764                Ok(Some(lsp::Hover {
 765                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 766                        kind: lsp::MarkupKind::Markdown,
 767                        value: future_markdown_string,
 768                    }),
 769                    range: Some(symbol_range),
 770                }))
 771            }
 772        })
 773        .next()
 774        .await;
 775
 776        cx.condition(|editor, _| editor.hover_state.visible()).await;
 777        cx.editor(|editor, _| {
 778            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
 779            assert_eq!(
 780                blocks,
 781                vec![HoverBlock {
 782                    text: markdown_string,
 783                    kind: HoverBlockKind::Markdown,
 784                }],
 785            );
 786
 787            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 788            assert_eq!(
 789                rendered.text,
 790                code_str.trim(),
 791                "Should not have extra line breaks at end of rendered hover"
 792            );
 793        });
 794    }
 795
 796    #[gpui::test]
 797    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
 798        init_test(cx, |_| {});
 799
 800        let mut cx = EditorLspTestContext::new_rust(
 801            lsp::ServerCapabilities {
 802                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 803                ..Default::default()
 804            },
 805            cx,
 806        )
 807        .await;
 808
 809        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
 810        // info popover once request completes
 811        cx.set_state(indoc! {"
 812            fn teˇst() { println!(); }
 813        "});
 814
 815        // Send diagnostic to client
 816        let range = cx.text_anchor_range(indoc! {"
 817            fn «test»() { println!(); }
 818        "});
 819        cx.update_buffer(|buffer, cx| {
 820            let snapshot = buffer.text_snapshot();
 821            let set = DiagnosticSet::from_sorted_entries(
 822                vec![DiagnosticEntry {
 823                    range,
 824                    diagnostic: Diagnostic {
 825                        message: "A test diagnostic message.".to_string(),
 826                        ..Default::default()
 827                    },
 828                }],
 829                &snapshot,
 830            );
 831            buffer.update_diagnostics(LanguageServerId(0), set, cx);
 832        });
 833
 834        // Hover pops diagnostic immediately
 835        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 836        cx.background_executor.run_until_parked();
 837
 838        cx.editor(|Editor { hover_state, .. }, _| {
 839            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
 840        });
 841
 842        // Info Popover shows after request responded to
 843        let range = cx.lsp_range(indoc! {"
 844            fn «test»() { println!(); }
 845        "});
 846        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 847            Ok(Some(lsp::Hover {
 848                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 849                    kind: lsp::MarkupKind::Markdown,
 850                    value: "some new docs".to_string(),
 851                }),
 852                range: Some(range),
 853            }))
 854        });
 855        cx.background_executor
 856            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 857
 858        cx.background_executor.run_until_parked();
 859        cx.editor(|Editor { hover_state, .. }, _| {
 860            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
 861        });
 862    }
 863
 864    #[gpui::test]
 865    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
 866        init_test(cx, |_| {});
 867
 868        let editor = cx.add_window(|cx| Editor::single_line(cx));
 869        editor
 870            .update(cx, |editor, cx| {
 871                let style = editor.style.clone().unwrap();
 872
 873                struct Row {
 874                    blocks: Vec<HoverBlock>,
 875                    expected_marked_text: String,
 876                    expected_styles: Vec<HighlightStyle>,
 877                }
 878
 879                let rows = &[
 880                    // Strong emphasis
 881                    Row {
 882                        blocks: vec![HoverBlock {
 883                            text: "one **two** three".to_string(),
 884                            kind: HoverBlockKind::Markdown,
 885                        }],
 886                        expected_marked_text: "one «two» three".to_string(),
 887                        expected_styles: vec![HighlightStyle {
 888                            font_weight: Some(FontWeight::BOLD),
 889                            ..Default::default()
 890                        }],
 891                    },
 892                    // Links
 893                    Row {
 894                        blocks: vec![HoverBlock {
 895                            text: "one [two](https://the-url) three".to_string(),
 896                            kind: HoverBlockKind::Markdown,
 897                        }],
 898                        expected_marked_text: "one «two» three".to_string(),
 899                        expected_styles: vec![HighlightStyle {
 900                            underline: Some(UnderlineStyle {
 901                                thickness: 1.0.into(),
 902                                ..Default::default()
 903                            }),
 904                            ..Default::default()
 905                        }],
 906                    },
 907                    // Lists
 908                    Row {
 909                        blocks: vec![HoverBlock {
 910                            text: "
 911                            lists:
 912                            * one
 913                                - a
 914                                - b
 915                            * two
 916                                - [c](https://the-url)
 917                                - d"
 918                            .unindent(),
 919                            kind: HoverBlockKind::Markdown,
 920                        }],
 921                        expected_marked_text: "
 922                        lists:
 923                        - one
 924                          - a
 925                          - b
 926                        - two
 927                          - «c»
 928                          - d"
 929                        .unindent(),
 930                        expected_styles: vec![HighlightStyle {
 931                            underline: Some(UnderlineStyle {
 932                                thickness: 1.0.into(),
 933                                ..Default::default()
 934                            }),
 935                            ..Default::default()
 936                        }],
 937                    },
 938                    // Multi-paragraph list items
 939                    Row {
 940                        blocks: vec![HoverBlock {
 941                            text: "
 942                            * one two
 943                              three
 944
 945                            * four five
 946                                * six seven
 947                                  eight
 948
 949                                  nine
 950                                * ten
 951                            * six"
 952                                .unindent(),
 953                            kind: HoverBlockKind::Markdown,
 954                        }],
 955                        expected_marked_text: "
 956                        - one two three
 957                        - four five
 958                          - six seven eight
 959
 960                            nine
 961                          - ten
 962                        - six"
 963                            .unindent(),
 964                        expected_styles: vec![HighlightStyle {
 965                            underline: Some(UnderlineStyle {
 966                                thickness: 1.0.into(),
 967                                ..Default::default()
 968                            }),
 969                            ..Default::default()
 970                        }],
 971                    },
 972                ];
 973
 974                for Row {
 975                    blocks,
 976                    expected_marked_text,
 977                    expected_styles,
 978                } in &rows[0..]
 979                {
 980                    let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 981
 982                    let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
 983                    let expected_highlights = ranges
 984                        .into_iter()
 985                        .zip(expected_styles.iter().cloned())
 986                        .collect::<Vec<_>>();
 987                    assert_eq!(
 988                        rendered.text, expected_text,
 989                        "wrong text for input {blocks:?}"
 990                    );
 991
 992                    let rendered_highlights: Vec<_> = rendered
 993                        .highlights
 994                        .iter()
 995                        .filter_map(|(range, highlight)| {
 996                            let highlight = highlight.to_highlight_style(&style.syntax)?;
 997                            Some((range.clone(), highlight))
 998                        })
 999                        .collect();
1000
1001                    assert_eq!(
1002                        rendered_highlights, expected_highlights,
1003                        "wrong highlights for input {blocks:?}"
1004                    );
1005                }
1006            })
1007            .unwrap();
1008    }
1009
1010    #[gpui::test]
1011    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1012        init_test(cx, |settings| {
1013            settings.defaults.inlay_hints = Some(InlayHintSettings {
1014                enabled: true,
1015                show_type_hints: true,
1016                show_parameter_hints: true,
1017                show_other_hints: true,
1018            })
1019        });
1020
1021        let mut cx = EditorLspTestContext::new_rust(
1022            lsp::ServerCapabilities {
1023                inlay_hint_provider: Some(lsp::OneOf::Right(
1024                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1025                        resolve_provider: Some(true),
1026                        ..Default::default()
1027                    }),
1028                )),
1029                ..Default::default()
1030            },
1031            cx,
1032        )
1033        .await;
1034
1035        cx.set_state(indoc! {"
1036            struct TestStruct;
1037
1038            // ==================
1039
1040            struct TestNewType<T>(T);
1041
1042            fn main() {
1043                let variableˇ = TestNewType(TestStruct);
1044            }
1045        "});
1046
1047        let hint_start_offset = cx.ranges(indoc! {"
1048            struct TestStruct;
1049
1050            // ==================
1051
1052            struct TestNewType<T>(T);
1053
1054            fn main() {
1055                let variableˇ = TestNewType(TestStruct);
1056            }
1057        "})[0]
1058            .start;
1059        let hint_position = cx.to_lsp(hint_start_offset);
1060        let new_type_target_range = cx.lsp_range(indoc! {"
1061            struct TestStruct;
1062
1063            // ==================
1064
1065            struct «TestNewType»<T>(T);
1066
1067            fn main() {
1068                let variable = TestNewType(TestStruct);
1069            }
1070        "});
1071        let struct_target_range = cx.lsp_range(indoc! {"
1072            struct «TestStruct»;
1073
1074            // ==================
1075
1076            struct TestNewType<T>(T);
1077
1078            fn main() {
1079                let variable = TestNewType(TestStruct);
1080            }
1081        "});
1082
1083        let uri = cx.buffer_lsp_url.clone();
1084        let new_type_label = "TestNewType";
1085        let struct_label = "TestStruct";
1086        let entire_hint_label = ": TestNewType<TestStruct>";
1087        let closure_uri = uri.clone();
1088        cx.lsp
1089            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1090                let task_uri = closure_uri.clone();
1091                async move {
1092                    assert_eq!(params.text_document.uri, task_uri);
1093                    Ok(Some(vec![lsp::InlayHint {
1094                        position: hint_position,
1095                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1096                            value: entire_hint_label.to_string(),
1097                            ..Default::default()
1098                        }]),
1099                        kind: Some(lsp::InlayHintKind::TYPE),
1100                        text_edits: None,
1101                        tooltip: None,
1102                        padding_left: Some(false),
1103                        padding_right: Some(false),
1104                        data: None,
1105                    }]))
1106                }
1107            })
1108            .next()
1109            .await;
1110        cx.background_executor.run_until_parked();
1111        cx.update_editor(|editor, cx| {
1112            let expected_layers = vec![entire_hint_label.to_string()];
1113            assert_eq!(expected_layers, cached_hint_labels(editor));
1114            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1115        });
1116
1117        let inlay_range = cx
1118            .ranges(indoc! {"
1119                struct TestStruct;
1120
1121                // ==================
1122
1123                struct TestNewType<T>(T);
1124
1125                fn main() {
1126                    let variable« »= TestNewType(TestStruct);
1127                }
1128        "})
1129            .get(0)
1130            .cloned()
1131            .unwrap();
1132        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1133            let snapshot = editor.snapshot(cx);
1134            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1135            let next_valid = inlay_range.end.to_display_point(&snapshot);
1136            assert_eq!(previous_valid.row(), next_valid.row());
1137            assert!(previous_valid.column() < next_valid.column());
1138            let exact_unclipped = DisplayPoint::new(
1139                previous_valid.row(),
1140                previous_valid.column()
1141                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1142                        as u32,
1143            );
1144            PointForPosition {
1145                previous_valid,
1146                next_valid,
1147                exact_unclipped,
1148                column_overshoot_after_line_end: 0,
1149            }
1150        });
1151        cx.update_editor(|editor, cx| {
1152            update_inlay_link_and_hover_points(
1153                &editor.snapshot(cx),
1154                new_type_hint_part_hover_position,
1155                editor,
1156                true,
1157                false,
1158                cx,
1159            );
1160        });
1161
1162        let resolve_closure_uri = uri.clone();
1163        cx.lsp
1164            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1165                move |mut hint_to_resolve, _| {
1166                    let mut resolved_hint_positions = BTreeSet::new();
1167                    let task_uri = resolve_closure_uri.clone();
1168                    async move {
1169                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1170                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1171
1172                        // `: TestNewType<TestStruct>`
1173                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1174                            lsp::InlayHintLabelPart {
1175                                value: ": ".to_string(),
1176                                ..Default::default()
1177                            },
1178                            lsp::InlayHintLabelPart {
1179                                value: new_type_label.to_string(),
1180                                location: Some(lsp::Location {
1181                                    uri: task_uri.clone(),
1182                                    range: new_type_target_range,
1183                                }),
1184                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1185                                    "A tooltip for `{new_type_label}`"
1186                                ))),
1187                                ..Default::default()
1188                            },
1189                            lsp::InlayHintLabelPart {
1190                                value: "<".to_string(),
1191                                ..Default::default()
1192                            },
1193                            lsp::InlayHintLabelPart {
1194                                value: struct_label.to_string(),
1195                                location: Some(lsp::Location {
1196                                    uri: task_uri,
1197                                    range: struct_target_range,
1198                                }),
1199                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1200                                    lsp::MarkupContent {
1201                                        kind: lsp::MarkupKind::Markdown,
1202                                        value: format!("A tooltip for `{struct_label}`"),
1203                                    },
1204                                )),
1205                                ..Default::default()
1206                            },
1207                            lsp::InlayHintLabelPart {
1208                                value: ">".to_string(),
1209                                ..Default::default()
1210                            },
1211                        ]);
1212
1213                        Ok(hint_to_resolve)
1214                    }
1215                },
1216            )
1217            .next()
1218            .await;
1219        cx.background_executor.run_until_parked();
1220
1221        cx.update_editor(|editor, cx| {
1222            update_inlay_link_and_hover_points(
1223                &editor.snapshot(cx),
1224                new_type_hint_part_hover_position,
1225                editor,
1226                true,
1227                false,
1228                cx,
1229            );
1230        });
1231        cx.background_executor
1232            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1233        cx.background_executor.run_until_parked();
1234        cx.update_editor(|editor, cx| {
1235            let hover_state = &editor.hover_state;
1236            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1237            let popover = hover_state.info_popover.as_ref().unwrap();
1238            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1239            assert_eq!(
1240                popover.symbol_range,
1241                RangeInEditor::Inlay(InlayHighlight {
1242                    inlay: InlayId::Hint(0),
1243                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1244                    range: ": ".len()..": ".len() + new_type_label.len(),
1245                }),
1246                "Popover range should match the new type label part"
1247            );
1248            assert_eq!(
1249                popover.parsed_content.text,
1250                format!("A tooltip for `{new_type_label}`"),
1251                "Rendered text should not anyhow alter backticks"
1252            );
1253        });
1254
1255        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1256            let snapshot = editor.snapshot(cx);
1257            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1258            let next_valid = inlay_range.end.to_display_point(&snapshot);
1259            assert_eq!(previous_valid.row(), next_valid.row());
1260            assert!(previous_valid.column() < next_valid.column());
1261            let exact_unclipped = DisplayPoint::new(
1262                previous_valid.row(),
1263                previous_valid.column()
1264                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1265                        as u32,
1266            );
1267            PointForPosition {
1268                previous_valid,
1269                next_valid,
1270                exact_unclipped,
1271                column_overshoot_after_line_end: 0,
1272            }
1273        });
1274        cx.update_editor(|editor, cx| {
1275            update_inlay_link_and_hover_points(
1276                &editor.snapshot(cx),
1277                struct_hint_part_hover_position,
1278                editor,
1279                true,
1280                false,
1281                cx,
1282            );
1283        });
1284        cx.background_executor
1285            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1286        cx.background_executor.run_until_parked();
1287        cx.update_editor(|editor, cx| {
1288            let hover_state = &editor.hover_state;
1289            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1290            let popover = hover_state.info_popover.as_ref().unwrap();
1291            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1292            assert_eq!(
1293                popover.symbol_range,
1294                RangeInEditor::Inlay(InlayHighlight {
1295                    inlay: InlayId::Hint(0),
1296                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1297                    range: ": ".len() + new_type_label.len() + "<".len()
1298                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1299                }),
1300                "Popover range should match the struct label part"
1301            );
1302            assert_eq!(
1303                popover.parsed_content.text,
1304                format!("A tooltip for {struct_label}"),
1305                "Rendered markdown element should remove backticks from text"
1306            );
1307        });
1308    }
1309}