hover_popover.rs

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