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: cx.theme().colors().border,
 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(true),
 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 _apply_additional_edits = cx.update_editor(|editor, cx| {
 906            editor.context_menu_next(&Default::default(), cx);
 907            editor
 908                .confirm_completion(&ConfirmCompletion::default(), cx)
 909                .unwrap()
 910        });
 911        cx.assert_editor_state(indoc! {"
 912            one.second_completionˇ
 913            two
 914            three
 915            fn test() { println!(); }
 916        "});
 917
 918        // check that the completion menu is no longer visible and that there still has only been 1 completion request
 919        cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
 920        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
 921
 922        //verify the information popover is still visible and unchanged
 923        cx.editor(|editor, cx| {
 924            assert!(editor.hover_state.visible());
 925            assert_eq!(
 926                editor.hover_state.info_popovers.len(),
 927                1,
 928                "Expected exactly one hover but got: {:?}",
 929                editor.hover_state.info_popovers
 930            );
 931            let rendered_text = editor
 932                .hover_state
 933                .info_popovers
 934                .first()
 935                .unwrap()
 936                .get_rendered_text(cx);
 937
 938            assert_eq!(rendered_text, "some basic docs".to_string())
 939        });
 940
 941        // Mouse moved with no hover response dismisses
 942        let hover_point = cx.display_point(indoc! {"
 943                one.second_completionˇ
 944                two
 945                three
 946                fn teˇst() { println!(); }
 947            "});
 948        let mut request = cx
 949            .lsp
 950            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
 951        cx.update_editor(|editor, cx| {
 952            let snapshot = editor.snapshot(cx);
 953            let anchor = snapshot
 954                .buffer_snapshot
 955                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
 956            hover_at(editor, Some(anchor), cx)
 957        });
 958        cx.background_executor
 959            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 960        request.next().await;
 961
 962        // verify that the information popover is no longer visible
 963        cx.editor(|editor, _| {
 964            assert!(!editor.hover_state.visible());
 965        });
 966    }
 967
 968    #[gpui::test]
 969    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
 970        init_test(cx, |_| {});
 971
 972        let mut cx = EditorLspTestContext::new_rust(
 973            lsp::ServerCapabilities {
 974                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 975                ..Default::default()
 976            },
 977            cx,
 978        )
 979        .await;
 980
 981        // Basic hover delays and then pops without moving the mouse
 982        cx.set_state(indoc! {"
 983            fn ˇtest() { println!(); }
 984        "});
 985        let hover_point = cx.display_point(indoc! {"
 986            fn test() { printˇln!(); }
 987        "});
 988
 989        cx.update_editor(|editor, cx| {
 990            let snapshot = editor.snapshot(cx);
 991            let anchor = snapshot
 992                .buffer_snapshot
 993                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
 994            hover_at(editor, Some(anchor), cx)
 995        });
 996        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 997
 998        // After delay, hover should be visible.
 999        let symbol_range = cx.lsp_range(indoc! {"
1000            fn test() { «println!»(); }
1001        "});
1002        let mut requests =
1003            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1004                Ok(Some(lsp::Hover {
1005                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1006                        kind: lsp::MarkupKind::Markdown,
1007                        value: "some basic docs".to_string(),
1008                    }),
1009                    range: Some(symbol_range),
1010                }))
1011            });
1012        cx.background_executor
1013            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1014        requests.next().await;
1015
1016        cx.editor(|editor, cx| {
1017            assert!(editor.hover_state.visible());
1018            assert_eq!(
1019                editor.hover_state.info_popovers.len(),
1020                1,
1021                "Expected exactly one hover but got: {:?}",
1022                editor.hover_state.info_popovers
1023            );
1024            let rendered_text = editor
1025                .hover_state
1026                .info_popovers
1027                .first()
1028                .unwrap()
1029                .get_rendered_text(cx);
1030
1031            assert_eq!(rendered_text, "some basic docs".to_string())
1032        });
1033
1034        // Mouse moved with no hover response dismisses
1035        let hover_point = cx.display_point(indoc! {"
1036            fn teˇst() { println!(); }
1037        "});
1038        let mut request = cx
1039            .lsp
1040            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1041        cx.update_editor(|editor, cx| {
1042            let snapshot = editor.snapshot(cx);
1043            let anchor = snapshot
1044                .buffer_snapshot
1045                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1046            hover_at(editor, Some(anchor), cx)
1047        });
1048        cx.background_executor
1049            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1050        request.next().await;
1051        cx.editor(|editor, _| {
1052            assert!(!editor.hover_state.visible());
1053        });
1054    }
1055
1056    #[gpui::test]
1057    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1058        init_test(cx, |_| {});
1059
1060        let mut cx = EditorLspTestContext::new_rust(
1061            lsp::ServerCapabilities {
1062                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1063                ..Default::default()
1064            },
1065            cx,
1066        )
1067        .await;
1068
1069        // Hover with keyboard has no delay
1070        cx.set_state(indoc! {"
1071            fˇn test() { println!(); }
1072        "});
1073        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1074        let symbol_range = cx.lsp_range(indoc! {"
1075            «fn» test() { println!(); }
1076        "});
1077
1078        cx.editor(|editor, _cx| {
1079            assert!(!editor.hover_state.visible());
1080
1081            assert_eq!(
1082                editor.hover_state.info_popovers.len(),
1083                0,
1084                "Expected no hovers but got but got: {:?}",
1085                editor.hover_state.info_popovers
1086            );
1087        });
1088
1089        let mut requests =
1090            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1091                Ok(Some(lsp::Hover {
1092                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1093                        kind: lsp::MarkupKind::Markdown,
1094                        value: "some other basic docs".to_string(),
1095                    }),
1096                    range: Some(symbol_range),
1097                }))
1098            });
1099
1100        requests.next().await;
1101        cx.dispatch_action(Hover);
1102
1103        cx.condition(|editor, _| editor.hover_state.visible()).await;
1104        cx.editor(|editor, cx| {
1105            assert_eq!(
1106                editor.hover_state.info_popovers.len(),
1107                1,
1108                "Expected exactly one hover but got: {:?}",
1109                editor.hover_state.info_popovers
1110            );
1111
1112            let rendered_text = editor
1113                .hover_state
1114                .info_popovers
1115                .first()
1116                .unwrap()
1117                .get_rendered_text(cx);
1118
1119            assert_eq!(rendered_text, "some other basic docs".to_string())
1120        });
1121    }
1122
1123    #[gpui::test]
1124    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1125        init_test(cx, |_| {});
1126
1127        let mut cx = EditorLspTestContext::new_rust(
1128            lsp::ServerCapabilities {
1129                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1130                ..Default::default()
1131            },
1132            cx,
1133        )
1134        .await;
1135
1136        // Hover with keyboard has no delay
1137        cx.set_state(indoc! {"
1138            fˇn test() { println!(); }
1139        "});
1140        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1141        let symbol_range = cx.lsp_range(indoc! {"
1142            «fn» test() { println!(); }
1143        "});
1144        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1145            Ok(Some(lsp::Hover {
1146                contents: lsp::HoverContents::Array(vec![
1147                    lsp::MarkedString::String("regular text for hover to show".to_string()),
1148                    lsp::MarkedString::String("".to_string()),
1149                    lsp::MarkedString::LanguageString(lsp::LanguageString {
1150                        language: "Rust".to_string(),
1151                        value: "".to_string(),
1152                    }),
1153                ]),
1154                range: Some(symbol_range),
1155            }))
1156        })
1157        .next()
1158        .await;
1159        cx.dispatch_action(Hover);
1160
1161        cx.condition(|editor, _| editor.hover_state.visible()).await;
1162        cx.editor(|editor, cx| {
1163            assert_eq!(
1164                editor.hover_state.info_popovers.len(),
1165                1,
1166                "Expected exactly one hover but got: {:?}",
1167                editor.hover_state.info_popovers
1168            );
1169            let rendered_text = editor
1170                .hover_state
1171                .info_popovers
1172                .first()
1173                .unwrap()
1174                .get_rendered_text(cx);
1175
1176            assert_eq!(
1177                rendered_text,
1178                "regular text for hover to show".to_string(),
1179                "No empty string hovers should be shown"
1180            );
1181        });
1182    }
1183
1184    #[gpui::test]
1185    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1186        init_test(cx, |_| {});
1187
1188        let mut cx = EditorLspTestContext::new_rust(
1189            lsp::ServerCapabilities {
1190                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1191                ..Default::default()
1192            },
1193            cx,
1194        )
1195        .await;
1196
1197        // Hover with keyboard has no delay
1198        cx.set_state(indoc! {"
1199            fˇn test() { println!(); }
1200        "});
1201        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1202        let symbol_range = cx.lsp_range(indoc! {"
1203            «fn» test() { println!(); }
1204        "});
1205
1206        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1207        let markdown_string = format!("\n```rust\n{code_str}```");
1208
1209        let closure_markdown_string = markdown_string.clone();
1210        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1211            let future_markdown_string = closure_markdown_string.clone();
1212            async move {
1213                Ok(Some(lsp::Hover {
1214                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1215                        kind: lsp::MarkupKind::Markdown,
1216                        value: future_markdown_string,
1217                    }),
1218                    range: Some(symbol_range),
1219                }))
1220            }
1221        })
1222        .next()
1223        .await;
1224
1225        cx.dispatch_action(Hover);
1226
1227        cx.condition(|editor, _| editor.hover_state.visible()).await;
1228        cx.editor(|editor, cx| {
1229            assert_eq!(
1230                editor.hover_state.info_popovers.len(),
1231                1,
1232                "Expected exactly one hover but got: {:?}",
1233                editor.hover_state.info_popovers
1234            );
1235            let rendered_text = editor
1236                .hover_state
1237                .info_popovers
1238                .first()
1239                .unwrap()
1240                .get_rendered_text(cx);
1241
1242            assert_eq!(
1243                rendered_text, code_str,
1244                "Should not have extra line breaks at end of rendered hover"
1245            );
1246        });
1247    }
1248
1249    #[gpui::test]
1250    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1251        init_test(cx, |_| {});
1252
1253        let mut cx = EditorLspTestContext::new_rust(
1254            lsp::ServerCapabilities {
1255                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1256                ..Default::default()
1257            },
1258            cx,
1259        )
1260        .await;
1261
1262        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1263        // info popover once request completes
1264        cx.set_state(indoc! {"
1265            fn teˇst() { println!(); }
1266        "});
1267
1268        // Send diagnostic to client
1269        let range = cx.text_anchor_range(indoc! {"
1270            fn «test»() { println!(); }
1271        "});
1272        cx.update_buffer(|buffer, cx| {
1273            let snapshot = buffer.text_snapshot();
1274            let set = DiagnosticSet::from_sorted_entries(
1275                vec![DiagnosticEntry {
1276                    range,
1277                    diagnostic: Diagnostic {
1278                        message: "A test diagnostic message.".to_string(),
1279                        ..Default::default()
1280                    },
1281                }],
1282                &snapshot,
1283            );
1284            buffer.update_diagnostics(LanguageServerId(0), set, cx);
1285        });
1286
1287        // Hover pops diagnostic immediately
1288        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1289        cx.background_executor.run_until_parked();
1290
1291        cx.editor(|Editor { hover_state, .. }, _| {
1292            assert!(
1293                hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1294            )
1295        });
1296
1297        // Info Popover shows after request responded to
1298        let range = cx.lsp_range(indoc! {"
1299            fn «test»() { println!(); }
1300        "});
1301        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1302            Ok(Some(lsp::Hover {
1303                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1304                    kind: lsp::MarkupKind::Markdown,
1305                    value: "some new docs".to_string(),
1306                }),
1307                range: Some(range),
1308            }))
1309        });
1310        cx.background_executor
1311            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1312
1313        cx.background_executor.run_until_parked();
1314        cx.editor(|Editor { hover_state, .. }, _| {
1315            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1316        });
1317    }
1318
1319    #[gpui::test]
1320    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1321        init_test(cx, |settings| {
1322            settings.defaults.inlay_hints = Some(InlayHintSettings {
1323                enabled: true,
1324                edit_debounce_ms: 0,
1325                scroll_debounce_ms: 0,
1326                show_type_hints: true,
1327                show_parameter_hints: true,
1328                show_other_hints: true,
1329                show_background: false,
1330            })
1331        });
1332
1333        let mut cx = EditorLspTestContext::new_rust(
1334            lsp::ServerCapabilities {
1335                inlay_hint_provider: Some(lsp::OneOf::Right(
1336                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1337                        resolve_provider: Some(true),
1338                        ..Default::default()
1339                    }),
1340                )),
1341                ..Default::default()
1342            },
1343            cx,
1344        )
1345        .await;
1346
1347        cx.set_state(indoc! {"
1348            struct TestStruct;
1349
1350            // ==================
1351
1352            struct TestNewType<T>(T);
1353
1354            fn main() {
1355                let variableˇ = TestNewType(TestStruct);
1356            }
1357        "});
1358
1359        let hint_start_offset = cx.ranges(indoc! {"
1360            struct TestStruct;
1361
1362            // ==================
1363
1364            struct TestNewType<T>(T);
1365
1366            fn main() {
1367                let variableˇ = TestNewType(TestStruct);
1368            }
1369        "})[0]
1370            .start;
1371        let hint_position = cx.to_lsp(hint_start_offset);
1372        let new_type_target_range = cx.lsp_range(indoc! {"
1373            struct TestStruct;
1374
1375            // ==================
1376
1377            struct «TestNewType»<T>(T);
1378
1379            fn main() {
1380                let variable = TestNewType(TestStruct);
1381            }
1382        "});
1383        let struct_target_range = cx.lsp_range(indoc! {"
1384            struct «TestStruct»;
1385
1386            // ==================
1387
1388            struct TestNewType<T>(T);
1389
1390            fn main() {
1391                let variable = TestNewType(TestStruct);
1392            }
1393        "});
1394
1395        let uri = cx.buffer_lsp_url.clone();
1396        let new_type_label = "TestNewType";
1397        let struct_label = "TestStruct";
1398        let entire_hint_label = ": TestNewType<TestStruct>";
1399        let closure_uri = uri.clone();
1400        cx.lsp
1401            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1402                let task_uri = closure_uri.clone();
1403                async move {
1404                    assert_eq!(params.text_document.uri, task_uri);
1405                    Ok(Some(vec![lsp::InlayHint {
1406                        position: hint_position,
1407                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1408                            value: entire_hint_label.to_string(),
1409                            ..Default::default()
1410                        }]),
1411                        kind: Some(lsp::InlayHintKind::TYPE),
1412                        text_edits: None,
1413                        tooltip: None,
1414                        padding_left: Some(false),
1415                        padding_right: Some(false),
1416                        data: None,
1417                    }]))
1418                }
1419            })
1420            .next()
1421            .await;
1422        cx.background_executor.run_until_parked();
1423        cx.update_editor(|editor, cx| {
1424            let expected_layers = vec![entire_hint_label.to_string()];
1425            assert_eq!(expected_layers, cached_hint_labels(editor));
1426            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1427        });
1428
1429        let inlay_range = cx
1430            .ranges(indoc! {"
1431                struct TestStruct;
1432
1433                // ==================
1434
1435                struct TestNewType<T>(T);
1436
1437                fn main() {
1438                    let variable« »= TestNewType(TestStruct);
1439                }
1440        "})
1441            .first()
1442            .cloned()
1443            .unwrap();
1444        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1445            let snapshot = editor.snapshot(cx);
1446            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1447            let next_valid = inlay_range.end.to_display_point(&snapshot);
1448            assert_eq!(previous_valid.row(), next_valid.row());
1449            assert!(previous_valid.column() < next_valid.column());
1450            let exact_unclipped = DisplayPoint::new(
1451                previous_valid.row(),
1452                previous_valid.column()
1453                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1454                        as u32,
1455            );
1456            PointForPosition {
1457                previous_valid,
1458                next_valid,
1459                exact_unclipped,
1460                column_overshoot_after_line_end: 0,
1461            }
1462        });
1463        cx.update_editor(|editor, cx| {
1464            update_inlay_link_and_hover_points(
1465                &editor.snapshot(cx),
1466                new_type_hint_part_hover_position,
1467                editor,
1468                true,
1469                false,
1470                cx,
1471            );
1472        });
1473
1474        let resolve_closure_uri = uri.clone();
1475        cx.lsp
1476            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1477                move |mut hint_to_resolve, _| {
1478                    let mut resolved_hint_positions = BTreeSet::new();
1479                    let task_uri = resolve_closure_uri.clone();
1480                    async move {
1481                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1482                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1483
1484                        // `: TestNewType<TestStruct>`
1485                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1486                            lsp::InlayHintLabelPart {
1487                                value: ": ".to_string(),
1488                                ..Default::default()
1489                            },
1490                            lsp::InlayHintLabelPart {
1491                                value: new_type_label.to_string(),
1492                                location: Some(lsp::Location {
1493                                    uri: task_uri.clone(),
1494                                    range: new_type_target_range,
1495                                }),
1496                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1497                                    "A tooltip for `{new_type_label}`"
1498                                ))),
1499                                ..Default::default()
1500                            },
1501                            lsp::InlayHintLabelPart {
1502                                value: "<".to_string(),
1503                                ..Default::default()
1504                            },
1505                            lsp::InlayHintLabelPart {
1506                                value: struct_label.to_string(),
1507                                location: Some(lsp::Location {
1508                                    uri: task_uri,
1509                                    range: struct_target_range,
1510                                }),
1511                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1512                                    lsp::MarkupContent {
1513                                        kind: lsp::MarkupKind::Markdown,
1514                                        value: format!("A tooltip for `{struct_label}`"),
1515                                    },
1516                                )),
1517                                ..Default::default()
1518                            },
1519                            lsp::InlayHintLabelPart {
1520                                value: ">".to_string(),
1521                                ..Default::default()
1522                            },
1523                        ]);
1524
1525                        Ok(hint_to_resolve)
1526                    }
1527                },
1528            )
1529            .next()
1530            .await;
1531        cx.background_executor.run_until_parked();
1532
1533        cx.update_editor(|editor, cx| {
1534            update_inlay_link_and_hover_points(
1535                &editor.snapshot(cx),
1536                new_type_hint_part_hover_position,
1537                editor,
1538                true,
1539                false,
1540                cx,
1541            );
1542        });
1543        cx.background_executor
1544            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1545        cx.background_executor.run_until_parked();
1546        cx.update_editor(|editor, cx| {
1547            let hover_state = &editor.hover_state;
1548            assert!(
1549                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1550            );
1551            let popover = hover_state.info_popovers.first().cloned().unwrap();
1552            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1553            assert_eq!(
1554                popover.symbol_range,
1555                RangeInEditor::Inlay(InlayHighlight {
1556                    inlay: InlayId::Hint(0),
1557                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1558                    range: ": ".len()..": ".len() + new_type_label.len(),
1559                }),
1560                "Popover range should match the new type label part"
1561            );
1562            assert_eq!(
1563                popover.get_rendered_text(cx),
1564                format!("A tooltip for {new_type_label}"),
1565            );
1566        });
1567
1568        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1569            let snapshot = editor.snapshot(cx);
1570            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1571            let next_valid = inlay_range.end.to_display_point(&snapshot);
1572            assert_eq!(previous_valid.row(), next_valid.row());
1573            assert!(previous_valid.column() < next_valid.column());
1574            let exact_unclipped = DisplayPoint::new(
1575                previous_valid.row(),
1576                previous_valid.column()
1577                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1578                        as u32,
1579            );
1580            PointForPosition {
1581                previous_valid,
1582                next_valid,
1583                exact_unclipped,
1584                column_overshoot_after_line_end: 0,
1585            }
1586        });
1587        cx.update_editor(|editor, cx| {
1588            update_inlay_link_and_hover_points(
1589                &editor.snapshot(cx),
1590                struct_hint_part_hover_position,
1591                editor,
1592                true,
1593                false,
1594                cx,
1595            );
1596        });
1597        cx.background_executor
1598            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1599        cx.background_executor.run_until_parked();
1600        cx.update_editor(|editor, cx| {
1601            let hover_state = &editor.hover_state;
1602            assert!(
1603                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1604            );
1605            let popover = hover_state.info_popovers.first().cloned().unwrap();
1606            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1607            assert_eq!(
1608                popover.symbol_range,
1609                RangeInEditor::Inlay(InlayHighlight {
1610                    inlay: InlayId::Hint(0),
1611                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1612                    range: ": ".len() + new_type_label.len() + "<".len()
1613                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1614                }),
1615                "Popover range should match the struct label part"
1616            );
1617            assert_eq!(
1618                popover.get_rendered_text(cx),
1619                format!("A tooltip for {struct_label}"),
1620                "Rendered markdown element should remove backticks from text"
1621            );
1622        });
1623    }
1624}