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