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