hover_popover.rs

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