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