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