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