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) {
 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 ui_font_family = settings.ui_font.family.clone();
 522            let buffer_font_family = settings.buffer_font.family.clone();
 523
 524            let mut base_text_style = cx.text_style();
 525            base_text_style.refine(&TextStyleRefinement {
 526                font_family: Some(ui_font_family.clone()),
 527                color: Some(cx.theme().colors().editor_foreground),
 528                ..Default::default()
 529            });
 530
 531            let markdown_style = MarkdownStyle {
 532                base_text_style,
 533                code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
 534                inline_code: TextStyleRefinement {
 535                    background_color: Some(cx.theme().colors().background),
 536                    font_family: Some(buffer_font_family),
 537                    ..Default::default()
 538                },
 539                rule_color: Color::Muted.color(cx),
 540                block_quote_border_color: Color::Muted.color(cx),
 541                block_quote: TextStyleRefinement {
 542                    color: Some(Color::Muted.color(cx)),
 543                    ..Default::default()
 544                },
 545                link: TextStyleRefinement {
 546                    color: Some(cx.theme().colors().editor_foreground),
 547                    underline: Some(gpui::UnderlineStyle {
 548                        thickness: px(1.),
 549                        color: Some(cx.theme().colors().editor_foreground),
 550                        wavy: false,
 551                    }),
 552                    ..Default::default()
 553                },
 554                syntax: cx.theme().syntax().clone(),
 555                selection_background_color: { cx.theme().players().local().selection },
 556                break_style: Default::default(),
 557                heading: StyleRefinement::default()
 558                    .font_weight(FontWeight::BOLD)
 559                    .text_base()
 560                    .mt(rems(1.))
 561                    .mb_0(),
 562            };
 563
 564            Markdown::new(
 565                combined_text,
 566                markdown_style.clone(),
 567                Some(language_registry.clone()),
 568                cx,
 569                fallback_language_name,
 570            )
 571        })
 572        .ok();
 573
 574    rendered_block
 575}
 576
 577#[derive(Default, Debug)]
 578pub struct HoverState {
 579    pub info_popovers: Vec<InfoPopover>,
 580    pub diagnostic_popover: Option<DiagnosticPopover>,
 581    pub triggered_from: Option<Anchor>,
 582    pub info_task: Option<Task<Option<()>>>,
 583}
 584
 585impl HoverState {
 586    pub fn visible(&self) -> bool {
 587        !self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
 588    }
 589
 590    pub fn render(
 591        &mut self,
 592        snapshot: &EditorSnapshot,
 593        visible_rows: Range<DisplayRow>,
 594        max_size: Size<Pixels>,
 595        cx: &mut ViewContext<Editor>,
 596    ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
 597        // If there is a diagnostic, position the popovers based on that.
 598        // Otherwise use the start of the hover range
 599        let anchor = self
 600            .diagnostic_popover
 601            .as_ref()
 602            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
 603            .or_else(|| {
 604                self.info_popovers.iter().find_map(|info_popover| {
 605                    match &info_popover.symbol_range {
 606                        RangeInEditor::Text(range) => Some(&range.start),
 607                        RangeInEditor::Inlay(_) => None,
 608                    }
 609                })
 610            })
 611            .or_else(|| {
 612                self.info_popovers.iter().find_map(|info_popover| {
 613                    match &info_popover.symbol_range {
 614                        RangeInEditor::Text(_) => None,
 615                        RangeInEditor::Inlay(range) => Some(&range.inlay_position),
 616                    }
 617                })
 618            })?;
 619        let point = anchor.to_display_point(&snapshot.display_snapshot);
 620
 621        // Don't render if the relevant point isn't on screen
 622        if !self.visible() || !visible_rows.contains(&point.row()) {
 623            return None;
 624        }
 625
 626        let mut elements = Vec::new();
 627
 628        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
 629            elements.push(diagnostic_popover.render(max_size, cx));
 630        }
 631        for info_popover in &mut self.info_popovers {
 632            elements.push(info_popover.render(max_size, cx));
 633        }
 634
 635        Some((point, elements))
 636    }
 637
 638    pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
 639        let mut hover_popover_is_focused = false;
 640        for info_popover in &self.info_popovers {
 641            if let Some(markdown_view) = &info_popover.parsed_content {
 642                if markdown_view.focus_handle(cx).is_focused(cx) {
 643                    hover_popover_is_focused = true;
 644                }
 645            }
 646        }
 647        if let Some(diagnostic_popover) = &self.diagnostic_popover {
 648            if let Some(markdown_view) = &diagnostic_popover.parsed_content {
 649                if markdown_view.focus_handle(cx).is_focused(cx) {
 650                    hover_popover_is_focused = true;
 651                }
 652            }
 653        }
 654        hover_popover_is_focused
 655    }
 656}
 657
 658#[derive(Debug, Clone)]
 659
 660pub struct InfoPopover {
 661    pub symbol_range: RangeInEditor,
 662    pub parsed_content: Option<View<Markdown>>,
 663    pub scroll_handle: ScrollHandle,
 664    pub keyboard_grace: Rc<RefCell<bool>>,
 665    pub anchor: Option<Anchor>,
 666}
 667
 668impl InfoPopover {
 669    pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
 670        let keyboard_grace = Rc::clone(&self.keyboard_grace);
 671        let mut d = div()
 672            .id("info_popover")
 673            .elevation_2(cx)
 674            .overflow_y_scroll()
 675            .track_scroll(&self.scroll_handle)
 676            .max_w(max_size.width)
 677            .max_h(max_size.height)
 678            // Prevent a mouse down/move on the popover from being propagated to the editor,
 679            // because that would dismiss the popover.
 680            .on_mouse_move(|_, cx| cx.stop_propagation())
 681            .on_mouse_down(MouseButton::Left, move |_, cx| {
 682                let mut keyboard_grace = keyboard_grace.borrow_mut();
 683                *keyboard_grace = false;
 684                cx.stop_propagation();
 685            })
 686            .p_2();
 687
 688        if let Some(markdown) = &self.parsed_content {
 689            d = d.child(markdown.clone());
 690        }
 691        d.into_any_element()
 692    }
 693
 694    pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
 695        let mut current = self.scroll_handle.offset();
 696        current.y -= amount.pixels(
 697            cx.line_height(),
 698            self.scroll_handle.bounds().size.height - px(16.),
 699        ) / 2.0;
 700        cx.notify();
 701        self.scroll_handle.set_offset(current);
 702    }
 703}
 704
 705#[derive(Debug, Clone)]
 706pub struct DiagnosticPopover {
 707    local_diagnostic: DiagnosticEntry<Anchor>,
 708    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
 709    parsed_content: Option<View<Markdown>>,
 710    border_color: Option<Hsla>,
 711    background_color: Option<Hsla>,
 712    pub keyboard_grace: Rc<RefCell<bool>>,
 713    pub anchor: Option<Anchor>,
 714}
 715
 716impl DiagnosticPopover {
 717    pub fn render(&self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
 718        let keyboard_grace = Rc::clone(&self.keyboard_grace);
 719        let mut markdown_div = div().py_1().px_2();
 720        if let Some(markdown) = &self.parsed_content {
 721            markdown_div = markdown_div.child(markdown.clone());
 722        }
 723
 724        if let Some(background_color) = &self.background_color {
 725            markdown_div = markdown_div.bg(*background_color);
 726        }
 727
 728        if let Some(border_color) = &self.border_color {
 729            markdown_div = markdown_div
 730                .border_1()
 731                .border_color(*border_color)
 732                .rounded_lg();
 733        }
 734
 735        let diagnostic_div = div()
 736            .id("diagnostic")
 737            .block()
 738            .max_h(max_size.height)
 739            .overflow_y_scroll()
 740            .max_w(max_size.width)
 741            .elevation_2_borderless(cx)
 742            // Don't draw the background color if the theme
 743            // allows transparent surfaces.
 744            .when(window_is_transparent(cx), |this| {
 745                this.bg(gpui::transparent_black())
 746            })
 747            // Prevent a mouse move on the popover from being propagated to the editor,
 748            // because that would dismiss the popover.
 749            .on_mouse_move(|_, cx| cx.stop_propagation())
 750            // Prevent a mouse down on the popover from being propagated to the editor,
 751            // because that would move the cursor.
 752            .on_mouse_down(MouseButton::Left, move |_, cx| {
 753                let mut keyboard_grace = keyboard_grace.borrow_mut();
 754                *keyboard_grace = false;
 755                cx.stop_propagation();
 756            })
 757            .child(markdown_div);
 758
 759        diagnostic_div.into_any_element()
 760    }
 761
 762    pub fn activation_info(&self) -> (usize, Anchor) {
 763        let entry = self
 764            .primary_diagnostic
 765            .as_ref()
 766            .unwrap_or(&self.local_diagnostic);
 767
 768        (entry.diagnostic.group_id, entry.range.start)
 769    }
 770}
 771
 772#[cfg(test)]
 773mod tests {
 774    use super::*;
 775    use crate::{
 776        actions::ConfirmCompletion,
 777        editor_tests::{handle_completion_request, init_test},
 778        hover_links::update_inlay_link_and_hover_points,
 779        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
 780        test::editor_lsp_test_context::EditorLspTestContext,
 781        InlayId, PointForPosition,
 782    };
 783    use collections::BTreeSet;
 784    use indoc::indoc;
 785    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
 786    use lsp::LanguageServerId;
 787    use markdown::parser::MarkdownEvent;
 788    use smol::stream::StreamExt;
 789    use std::sync::atomic;
 790    use std::sync::atomic::AtomicUsize;
 791    use text::Bias;
 792
 793    impl InfoPopover {
 794        fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
 795            let mut rendered_text = String::new();
 796            if let Some(parsed_content) = self.parsed_content.clone() {
 797                let markdown = parsed_content.read(cx);
 798                let text = markdown.parsed_markdown().source().to_string();
 799                let data = markdown.parsed_markdown().events();
 800                let slice = data;
 801
 802                for (range, event) in slice.iter() {
 803                    if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
 804                        rendered_text.push_str(&text[range.clone()])
 805                    }
 806                }
 807            }
 808            rendered_text
 809        }
 810    }
 811
 812    #[gpui::test]
 813    async fn test_mouse_hover_info_popover_with_autocomplete_popover(
 814        cx: &mut gpui::TestAppContext,
 815    ) {
 816        init_test(cx, |_| {});
 817        const HOVER_DELAY_MILLIS: u64 = 350;
 818
 819        let mut cx = EditorLspTestContext::new_rust(
 820            lsp::ServerCapabilities {
 821                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 822                completion_provider: Some(lsp::CompletionOptions {
 823                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
 824                    resolve_provider: Some(true),
 825                    ..Default::default()
 826                }),
 827                ..Default::default()
 828            },
 829            cx,
 830        )
 831        .await;
 832        let counter = Arc::new(AtomicUsize::new(0));
 833        // Basic hover delays and then pops without moving the mouse
 834        cx.set_state(indoc! {"
 835                oneˇ
 836                two
 837                three
 838                fn test() { println!(); }
 839            "});
 840
 841        //prompt autocompletion menu
 842        cx.simulate_keystroke(".");
 843        handle_completion_request(
 844            &mut cx,
 845            indoc! {"
 846                        one.|<>
 847                        two
 848                        three
 849                    "},
 850            vec!["first_completion", "second_completion"],
 851            counter.clone(),
 852        )
 853        .await;
 854        cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
 855            .await;
 856        assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
 857
 858        let hover_point = cx.display_point(indoc! {"
 859                one.
 860                two
 861                three
 862                fn test() { printˇln!(); }
 863            "});
 864        cx.update_editor(|editor, cx| {
 865            let snapshot = editor.snapshot(cx);
 866            let anchor = snapshot
 867                .buffer_snapshot
 868                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
 869            hover_at(editor, Some(anchor), cx)
 870        });
 871        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 872
 873        // After delay, hover should be visible.
 874        let symbol_range = cx.lsp_range(indoc! {"
 875                one.
 876                two
 877                three
 878                fn test() { «println!»(); }
 879            "});
 880        let mut requests =
 881            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 882                Ok(Some(lsp::Hover {
 883                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 884                        kind: lsp::MarkupKind::Markdown,
 885                        value: "some basic docs".to_string(),
 886                    }),
 887                    range: Some(symbol_range),
 888                }))
 889            });
 890        cx.background_executor
 891            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 892        requests.next().await;
 893
 894        cx.editor(|editor, cx| {
 895            assert!(editor.hover_state.visible());
 896            assert_eq!(
 897                editor.hover_state.info_popovers.len(),
 898                1,
 899                "Expected exactly one hover but got: {:?}",
 900                editor.hover_state.info_popovers
 901            );
 902            let rendered_text = editor
 903                .hover_state
 904                .info_popovers
 905                .first()
 906                .unwrap()
 907                .get_rendered_text(cx);
 908            assert_eq!(rendered_text, "some basic docs".to_string())
 909        });
 910
 911        // check that the completion menu is still visible and that there still has only been 1 completion request
 912        cx.editor(|editor, _| assert!(editor.context_menu_visible()));
 913        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
 914
 915        //apply a completion and check it was successfully applied
 916        let _apply_additional_edits = cx.update_editor(|editor, cx| {
 917            editor.context_menu_next(&Default::default(), cx);
 918            editor
 919                .confirm_completion(&ConfirmCompletion::default(), cx)
 920                .unwrap()
 921        });
 922        cx.assert_editor_state(indoc! {"
 923            one.second_completionˇ
 924            two
 925            three
 926            fn test() { println!(); }
 927        "});
 928
 929        // check that the completion menu is no longer visible and that there still has only been 1 completion request
 930        cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
 931        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
 932
 933        //verify the information popover is still visible and unchanged
 934        cx.editor(|editor, cx| {
 935            assert!(editor.hover_state.visible());
 936            assert_eq!(
 937                editor.hover_state.info_popovers.len(),
 938                1,
 939                "Expected exactly one hover but got: {:?}",
 940                editor.hover_state.info_popovers
 941            );
 942            let rendered_text = editor
 943                .hover_state
 944                .info_popovers
 945                .first()
 946                .unwrap()
 947                .get_rendered_text(cx);
 948
 949            assert_eq!(rendered_text, "some basic docs".to_string())
 950        });
 951
 952        // Mouse moved with no hover response dismisses
 953        let hover_point = cx.display_point(indoc! {"
 954                one.second_completionˇ
 955                two
 956                three
 957                fn teˇst() { println!(); }
 958            "});
 959        let mut request = cx
 960            .lsp
 961            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
 962        cx.update_editor(|editor, cx| {
 963            let snapshot = editor.snapshot(cx);
 964            let anchor = snapshot
 965                .buffer_snapshot
 966                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
 967            hover_at(editor, Some(anchor), cx)
 968        });
 969        cx.background_executor
 970            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 971        request.next().await;
 972
 973        // verify that the information popover is no longer visible
 974        cx.editor(|editor, _| {
 975            assert!(!editor.hover_state.visible());
 976        });
 977    }
 978
 979    #[gpui::test]
 980    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
 981        init_test(cx, |_| {});
 982
 983        let mut cx = EditorLspTestContext::new_rust(
 984            lsp::ServerCapabilities {
 985                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 986                ..Default::default()
 987            },
 988            cx,
 989        )
 990        .await;
 991
 992        // Basic hover delays and then pops without moving the mouse
 993        cx.set_state(indoc! {"
 994            fn ˇtest() { println!(); }
 995        "});
 996        let hover_point = cx.display_point(indoc! {"
 997            fn test() { printˇln!(); }
 998        "});
 999
1000        cx.update_editor(|editor, cx| {
1001            let snapshot = editor.snapshot(cx);
1002            let anchor = snapshot
1003                .buffer_snapshot
1004                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1005            hover_at(editor, Some(anchor), cx)
1006        });
1007        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
1008
1009        // After delay, hover should be visible.
1010        let symbol_range = cx.lsp_range(indoc! {"
1011            fn test() { «println!»(); }
1012        "});
1013        let mut requests =
1014            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1015                Ok(Some(lsp::Hover {
1016                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1017                        kind: lsp::MarkupKind::Markdown,
1018                        value: "some basic docs".to_string(),
1019                    }),
1020                    range: Some(symbol_range),
1021                }))
1022            });
1023        cx.background_executor
1024            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1025        requests.next().await;
1026
1027        cx.editor(|editor, cx| {
1028            assert!(editor.hover_state.visible());
1029            assert_eq!(
1030                editor.hover_state.info_popovers.len(),
1031                1,
1032                "Expected exactly one hover but got: {:?}",
1033                editor.hover_state.info_popovers
1034            );
1035            let rendered_text = editor
1036                .hover_state
1037                .info_popovers
1038                .first()
1039                .unwrap()
1040                .get_rendered_text(cx);
1041
1042            assert_eq!(rendered_text, "some basic docs".to_string())
1043        });
1044
1045        // Mouse moved with no hover response dismisses
1046        let hover_point = cx.display_point(indoc! {"
1047            fn teˇst() { println!(); }
1048        "});
1049        let mut request = cx
1050            .lsp
1051            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1052        cx.update_editor(|editor, cx| {
1053            let snapshot = editor.snapshot(cx);
1054            let anchor = snapshot
1055                .buffer_snapshot
1056                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1057            hover_at(editor, Some(anchor), cx)
1058        });
1059        cx.background_executor
1060            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1061        request.next().await;
1062        cx.editor(|editor, _| {
1063            assert!(!editor.hover_state.visible());
1064        });
1065    }
1066
1067    #[gpui::test]
1068    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1069        init_test(cx, |_| {});
1070
1071        let mut cx = EditorLspTestContext::new_rust(
1072            lsp::ServerCapabilities {
1073                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1074                ..Default::default()
1075            },
1076            cx,
1077        )
1078        .await;
1079
1080        // Hover with keyboard has no delay
1081        cx.set_state(indoc! {"
1082            fˇn test() { println!(); }
1083        "});
1084        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1085        let symbol_range = cx.lsp_range(indoc! {"
1086            «fn» test() { println!(); }
1087        "});
1088
1089        cx.editor(|editor, _cx| {
1090            assert!(!editor.hover_state.visible());
1091
1092            assert_eq!(
1093                editor.hover_state.info_popovers.len(),
1094                0,
1095                "Expected no hovers but got but got: {:?}",
1096                editor.hover_state.info_popovers
1097            );
1098        });
1099
1100        let mut requests =
1101            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1102                Ok(Some(lsp::Hover {
1103                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1104                        kind: lsp::MarkupKind::Markdown,
1105                        value: "some other basic docs".to_string(),
1106                    }),
1107                    range: Some(symbol_range),
1108                }))
1109            });
1110
1111        requests.next().await;
1112        cx.dispatch_action(Hover);
1113
1114        cx.condition(|editor, _| editor.hover_state.visible()).await;
1115        cx.editor(|editor, cx| {
1116            assert_eq!(
1117                editor.hover_state.info_popovers.len(),
1118                1,
1119                "Expected exactly one hover but got: {:?}",
1120                editor.hover_state.info_popovers
1121            );
1122
1123            let rendered_text = editor
1124                .hover_state
1125                .info_popovers
1126                .first()
1127                .unwrap()
1128                .get_rendered_text(cx);
1129
1130            assert_eq!(rendered_text, "some other basic docs".to_string())
1131        });
1132    }
1133
1134    #[gpui::test]
1135    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1136        init_test(cx, |_| {});
1137
1138        let mut cx = EditorLspTestContext::new_rust(
1139            lsp::ServerCapabilities {
1140                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1141                ..Default::default()
1142            },
1143            cx,
1144        )
1145        .await;
1146
1147        // Hover with keyboard has no delay
1148        cx.set_state(indoc! {"
1149            fˇn test() { println!(); }
1150        "});
1151        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1152        let symbol_range = cx.lsp_range(indoc! {"
1153            «fn» test() { println!(); }
1154        "});
1155        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1156            Ok(Some(lsp::Hover {
1157                contents: lsp::HoverContents::Array(vec![
1158                    lsp::MarkedString::String("regular text for hover to show".to_string()),
1159                    lsp::MarkedString::String("".to_string()),
1160                    lsp::MarkedString::LanguageString(lsp::LanguageString {
1161                        language: "Rust".to_string(),
1162                        value: "".to_string(),
1163                    }),
1164                ]),
1165                range: Some(symbol_range),
1166            }))
1167        })
1168        .next()
1169        .await;
1170        cx.dispatch_action(Hover);
1171
1172        cx.condition(|editor, _| editor.hover_state.visible()).await;
1173        cx.editor(|editor, cx| {
1174            assert_eq!(
1175                editor.hover_state.info_popovers.len(),
1176                1,
1177                "Expected exactly one hover but got: {:?}",
1178                editor.hover_state.info_popovers
1179            );
1180            let rendered_text = editor
1181                .hover_state
1182                .info_popovers
1183                .first()
1184                .unwrap()
1185                .get_rendered_text(cx);
1186
1187            assert_eq!(
1188                rendered_text,
1189                "regular text for hover to show".to_string(),
1190                "No empty string hovers should be shown"
1191            );
1192        });
1193    }
1194
1195    #[gpui::test]
1196    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1197        init_test(cx, |_| {});
1198
1199        let mut cx = EditorLspTestContext::new_rust(
1200            lsp::ServerCapabilities {
1201                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1202                ..Default::default()
1203            },
1204            cx,
1205        )
1206        .await;
1207
1208        // Hover with keyboard has no delay
1209        cx.set_state(indoc! {"
1210            fˇn test() { println!(); }
1211        "});
1212        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1213        let symbol_range = cx.lsp_range(indoc! {"
1214            «fn» test() { println!(); }
1215        "});
1216
1217        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1218        let markdown_string = format!("\n```rust\n{code_str}```");
1219
1220        let closure_markdown_string = markdown_string.clone();
1221        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1222            let future_markdown_string = closure_markdown_string.clone();
1223            async move {
1224                Ok(Some(lsp::Hover {
1225                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1226                        kind: lsp::MarkupKind::Markdown,
1227                        value: future_markdown_string,
1228                    }),
1229                    range: Some(symbol_range),
1230                }))
1231            }
1232        })
1233        .next()
1234        .await;
1235
1236        cx.dispatch_action(Hover);
1237
1238        cx.condition(|editor, _| editor.hover_state.visible()).await;
1239        cx.editor(|editor, cx| {
1240            assert_eq!(
1241                editor.hover_state.info_popovers.len(),
1242                1,
1243                "Expected exactly one hover but got: {:?}",
1244                editor.hover_state.info_popovers
1245            );
1246            let rendered_text = editor
1247                .hover_state
1248                .info_popovers
1249                .first()
1250                .unwrap()
1251                .get_rendered_text(cx);
1252
1253            assert_eq!(
1254                rendered_text, code_str,
1255                "Should not have extra line breaks at end of rendered hover"
1256            );
1257        });
1258    }
1259
1260    #[gpui::test]
1261    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1262        init_test(cx, |_| {});
1263
1264        let mut cx = EditorLspTestContext::new_rust(
1265            lsp::ServerCapabilities {
1266                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1267                ..Default::default()
1268            },
1269            cx,
1270        )
1271        .await;
1272
1273        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1274        // info popover once request completes
1275        cx.set_state(indoc! {"
1276            fn teˇst() { println!(); }
1277        "});
1278
1279        // Send diagnostic to client
1280        let range = cx.text_anchor_range(indoc! {"
1281            fn «test»() { println!(); }
1282        "});
1283        cx.update_buffer(|buffer, cx| {
1284            let snapshot = buffer.text_snapshot();
1285            let set = DiagnosticSet::from_sorted_entries(
1286                vec![DiagnosticEntry {
1287                    range,
1288                    diagnostic: Diagnostic {
1289                        message: "A test diagnostic message.".to_string(),
1290                        ..Default::default()
1291                    },
1292                }],
1293                &snapshot,
1294            );
1295            buffer.update_diagnostics(LanguageServerId(0), set, cx);
1296        });
1297
1298        // Hover pops diagnostic immediately
1299        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1300        cx.background_executor.run_until_parked();
1301
1302        cx.editor(|Editor { hover_state, .. }, _| {
1303            assert!(
1304                hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1305            )
1306        });
1307
1308        // Info Popover shows after request responded to
1309        let range = cx.lsp_range(indoc! {"
1310            fn «test»() { println!(); }
1311        "});
1312        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1313            Ok(Some(lsp::Hover {
1314                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1315                    kind: lsp::MarkupKind::Markdown,
1316                    value: "some new docs".to_string(),
1317                }),
1318                range: Some(range),
1319            }))
1320        });
1321        cx.background_executor
1322            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1323
1324        cx.background_executor.run_until_parked();
1325        cx.editor(|Editor { hover_state, .. }, _| {
1326            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1327        });
1328    }
1329
1330    #[gpui::test]
1331    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1332        init_test(cx, |settings| {
1333            settings.defaults.inlay_hints = Some(InlayHintSettings {
1334                enabled: true,
1335                edit_debounce_ms: 0,
1336                scroll_debounce_ms: 0,
1337                show_type_hints: true,
1338                show_parameter_hints: true,
1339                show_other_hints: true,
1340                show_background: false,
1341            })
1342        });
1343
1344        let mut cx = EditorLspTestContext::new_rust(
1345            lsp::ServerCapabilities {
1346                inlay_hint_provider: Some(lsp::OneOf::Right(
1347                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1348                        resolve_provider: Some(true),
1349                        ..Default::default()
1350                    }),
1351                )),
1352                ..Default::default()
1353            },
1354            cx,
1355        )
1356        .await;
1357
1358        cx.set_state(indoc! {"
1359            struct TestStruct;
1360
1361            // ==================
1362
1363            struct TestNewType<T>(T);
1364
1365            fn main() {
1366                let variableˇ = TestNewType(TestStruct);
1367            }
1368        "});
1369
1370        let hint_start_offset = cx.ranges(indoc! {"
1371            struct TestStruct;
1372
1373            // ==================
1374
1375            struct TestNewType<T>(T);
1376
1377            fn main() {
1378                let variableˇ = TestNewType(TestStruct);
1379            }
1380        "})[0]
1381            .start;
1382        let hint_position = cx.to_lsp(hint_start_offset);
1383        let new_type_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        let struct_target_range = cx.lsp_range(indoc! {"
1395            struct «TestStruct»;
1396
1397            // ==================
1398
1399            struct TestNewType<T>(T);
1400
1401            fn main() {
1402                let variable = TestNewType(TestStruct);
1403            }
1404        "});
1405
1406        let uri = cx.buffer_lsp_url.clone();
1407        let new_type_label = "TestNewType";
1408        let struct_label = "TestStruct";
1409        let entire_hint_label = ": TestNewType<TestStruct>";
1410        let closure_uri = uri.clone();
1411        cx.lsp
1412            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1413                let task_uri = closure_uri.clone();
1414                async move {
1415                    assert_eq!(params.text_document.uri, task_uri);
1416                    Ok(Some(vec![lsp::InlayHint {
1417                        position: hint_position,
1418                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1419                            value: entire_hint_label.to_string(),
1420                            ..Default::default()
1421                        }]),
1422                        kind: Some(lsp::InlayHintKind::TYPE),
1423                        text_edits: None,
1424                        tooltip: None,
1425                        padding_left: Some(false),
1426                        padding_right: Some(false),
1427                        data: None,
1428                    }]))
1429                }
1430            })
1431            .next()
1432            .await;
1433        cx.background_executor.run_until_parked();
1434        cx.update_editor(|editor, cx| {
1435            let expected_layers = vec![entire_hint_label.to_string()];
1436            assert_eq!(expected_layers, cached_hint_labels(editor));
1437            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1438        });
1439
1440        let inlay_range = cx
1441            .ranges(indoc! {"
1442                struct TestStruct;
1443
1444                // ==================
1445
1446                struct TestNewType<T>(T);
1447
1448                fn main() {
1449                    let variable« »= TestNewType(TestStruct);
1450                }
1451        "})
1452            .first()
1453            .cloned()
1454            .unwrap();
1455        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1456            let snapshot = editor.snapshot(cx);
1457            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1458            let next_valid = inlay_range.end.to_display_point(&snapshot);
1459            assert_eq!(previous_valid.row(), next_valid.row());
1460            assert!(previous_valid.column() < next_valid.column());
1461            let exact_unclipped = DisplayPoint::new(
1462                previous_valid.row(),
1463                previous_valid.column()
1464                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1465                        as u32,
1466            );
1467            PointForPosition {
1468                previous_valid,
1469                next_valid,
1470                exact_unclipped,
1471                column_overshoot_after_line_end: 0,
1472            }
1473        });
1474        cx.update_editor(|editor, cx| {
1475            update_inlay_link_and_hover_points(
1476                &editor.snapshot(cx),
1477                new_type_hint_part_hover_position,
1478                editor,
1479                true,
1480                false,
1481                cx,
1482            );
1483        });
1484
1485        let resolve_closure_uri = uri.clone();
1486        cx.lsp
1487            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1488                move |mut hint_to_resolve, _| {
1489                    let mut resolved_hint_positions = BTreeSet::new();
1490                    let task_uri = resolve_closure_uri.clone();
1491                    async move {
1492                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1493                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1494
1495                        // `: TestNewType<TestStruct>`
1496                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1497                            lsp::InlayHintLabelPart {
1498                                value: ": ".to_string(),
1499                                ..Default::default()
1500                            },
1501                            lsp::InlayHintLabelPart {
1502                                value: new_type_label.to_string(),
1503                                location: Some(lsp::Location {
1504                                    uri: task_uri.clone(),
1505                                    range: new_type_target_range,
1506                                }),
1507                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1508                                    "A tooltip for `{new_type_label}`"
1509                                ))),
1510                                ..Default::default()
1511                            },
1512                            lsp::InlayHintLabelPart {
1513                                value: "<".to_string(),
1514                                ..Default::default()
1515                            },
1516                            lsp::InlayHintLabelPart {
1517                                value: struct_label.to_string(),
1518                                location: Some(lsp::Location {
1519                                    uri: task_uri,
1520                                    range: struct_target_range,
1521                                }),
1522                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1523                                    lsp::MarkupContent {
1524                                        kind: lsp::MarkupKind::Markdown,
1525                                        value: format!("A tooltip for `{struct_label}`"),
1526                                    },
1527                                )),
1528                                ..Default::default()
1529                            },
1530                            lsp::InlayHintLabelPart {
1531                                value: ">".to_string(),
1532                                ..Default::default()
1533                            },
1534                        ]);
1535
1536                        Ok(hint_to_resolve)
1537                    }
1538                },
1539            )
1540            .next()
1541            .await;
1542        cx.background_executor.run_until_parked();
1543
1544        cx.update_editor(|editor, cx| {
1545            update_inlay_link_and_hover_points(
1546                &editor.snapshot(cx),
1547                new_type_hint_part_hover_position,
1548                editor,
1549                true,
1550                false,
1551                cx,
1552            );
1553        });
1554        cx.background_executor
1555            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1556        cx.background_executor.run_until_parked();
1557        cx.update_editor(|editor, cx| {
1558            let hover_state = &editor.hover_state;
1559            assert!(
1560                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1561            );
1562            let popover = hover_state.info_popovers.first().cloned().unwrap();
1563            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1564            assert_eq!(
1565                popover.symbol_range,
1566                RangeInEditor::Inlay(InlayHighlight {
1567                    inlay: InlayId::Hint(0),
1568                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1569                    range: ": ".len()..": ".len() + new_type_label.len(),
1570                }),
1571                "Popover range should match the new type label part"
1572            );
1573            assert_eq!(
1574                popover.get_rendered_text(cx),
1575                format!("A tooltip for {new_type_label}"),
1576            );
1577        });
1578
1579        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1580            let snapshot = editor.snapshot(cx);
1581            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1582            let next_valid = inlay_range.end.to_display_point(&snapshot);
1583            assert_eq!(previous_valid.row(), next_valid.row());
1584            assert!(previous_valid.column() < next_valid.column());
1585            let exact_unclipped = DisplayPoint::new(
1586                previous_valid.row(),
1587                previous_valid.column()
1588                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1589                        as u32,
1590            );
1591            PointForPosition {
1592                previous_valid,
1593                next_valid,
1594                exact_unclipped,
1595                column_overshoot_after_line_end: 0,
1596            }
1597        });
1598        cx.update_editor(|editor, cx| {
1599            update_inlay_link_and_hover_points(
1600                &editor.snapshot(cx),
1601                struct_hint_part_hover_position,
1602                editor,
1603                true,
1604                false,
1605                cx,
1606            );
1607        });
1608        cx.background_executor
1609            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1610        cx.background_executor.run_until_parked();
1611        cx.update_editor(|editor, cx| {
1612            let hover_state = &editor.hover_state;
1613            assert!(
1614                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1615            );
1616            let popover = hover_state.info_popovers.first().cloned().unwrap();
1617            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1618            assert_eq!(
1619                popover.symbol_range,
1620                RangeInEditor::Inlay(InlayHighlight {
1621                    inlay: InlayId::Hint(0),
1622                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1623                    range: ": ".len() + new_type_label.len() + "<".len()
1624                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1625                }),
1626                "Popover range should match the struct label part"
1627            );
1628            assert_eq!(
1629                popover.get_rendered_text(cx),
1630                format!("A tooltip for {struct_label}"),
1631                "Rendered markdown element should remove backticks from text"
1632            );
1633        });
1634    }
1635}