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