hover_popover.rs

   1use crate::{
   2    display_map::{InlayOffset, ToDisplayPoint},
   3    link_go_to_definition::{DocumentRange, InlayRange},
   4    Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
   5    ExcerptId, RangeToAnchorExt,
   6};
   7use futures::FutureExt;
   8use gpui::{
   9    actions,
  10    elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
  11    fonts::{HighlightStyle, Underline, Weight},
  12    platform::{CursorStyle, MouseButton},
  13    AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
  14};
  15use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
  16use project::{HoverBlock, HoverBlockKind, Project};
  17use std::{ops::Range, sync::Arc, time::Duration};
  18use util::TryFutureExt;
  19
  20pub const HOVER_DELAY_MILLIS: u64 = 350;
  21pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
  22
  23pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
  24pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
  25pub const HOVER_POPOVER_GAP: f32 = 10.;
  26
  27actions!(editor, [Hover]);
  28
  29pub fn init(cx: &mut AppContext) {
  30    cx.add_action(hover);
  31}
  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_display(cx).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, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
  42    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
  43        if let Some(point) = point {
  44            show_hover(editor, point, false, cx);
  45        } else {
  46            hide_hover(editor, cx);
  47        }
  48    }
  49}
  50
  51pub struct InlayHover {
  52    pub excerpt: ExcerptId,
  53    pub triggered_from: InlayOffset,
  54    pub range: InlayRange,
  55    pub tooltip: HoverBlock,
  56}
  57
  58pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
  59    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
  60        if editor.pending_rename.is_some() {
  61            return;
  62        }
  63
  64        let Some(project) = editor.project.clone() else {
  65            return;
  66        };
  67
  68        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
  69            if let DocumentRange::Inlay(range) = symbol_range {
  70                if (range.highlight_start..=range.highlight_end)
  71                    .contains(&inlay_hover.triggered_from)
  72                {
  73                    // Hover triggered from same location as last time. Don't show again.
  74                    return;
  75                }
  76            }
  77            hide_hover(editor, cx);
  78        }
  79
  80        let snapshot = editor.snapshot(cx);
  81        // Don't request again if the location is the same as the previous request
  82        if let Some(triggered_from) = editor.hover_state.triggered_from {
  83            if inlay_hover.triggered_from
  84                == snapshot
  85                    .display_snapshot
  86                    .anchor_to_inlay_offset(triggered_from)
  87            {
  88                return;
  89            }
  90        }
  91
  92        let task = cx.spawn(|this, mut cx| {
  93            async move {
  94                cx.background()
  95                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
  96                    .await;
  97                this.update(&mut cx, |this, _| {
  98                    this.hover_state.diagnostic_popover = None;
  99                })?;
 100
 101                let hover_popover = InfoPopover {
 102                    project: project.clone(),
 103                    symbol_range: DocumentRange::Inlay(inlay_hover.range),
 104                    blocks: vec![inlay_hover.tooltip],
 105                    language: None,
 106                    rendered_content: None,
 107                };
 108
 109                this.update(&mut cx, |this, cx| {
 110                    // Highlight the selected symbol using a background highlight
 111                    this.highlight_inlay_background::<HoverState>(
 112                        vec![inlay_hover.range],
 113                        |theme| theme.editor.hover_popover.highlight,
 114                        cx,
 115                    );
 116                    this.hover_state.info_popover = Some(hover_popover);
 117                    cx.notify();
 118                })?;
 119
 120                anyhow::Ok(())
 121            }
 122            .log_err()
 123        });
 124
 125        editor.hover_state.info_task = Some(task);
 126    }
 127}
 128
 129/// Hides the type information popup.
 130/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 131/// selections changed.
 132pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
 133    let did_hide = editor.hover_state.info_popover.take().is_some()
 134        | editor.hover_state.diagnostic_popover.take().is_some();
 135
 136    editor.hover_state.info_task = None;
 137    editor.hover_state.triggered_from = None;
 138
 139    editor.clear_background_highlights::<HoverState>(cx);
 140
 141    if did_hide {
 142        cx.notify();
 143    }
 144
 145    did_hide
 146}
 147
 148/// Queries the LSP and shows type info and documentation
 149/// about the symbol the mouse is currently hovering over.
 150/// Triggered by the `Hover` action when the cursor may be over a symbol.
 151fn show_hover(
 152    editor: &mut Editor,
 153    point: DisplayPoint,
 154    ignore_timeout: bool,
 155    cx: &mut ViewContext<Editor>,
 156) {
 157    if editor.pending_rename.is_some() {
 158        return;
 159    }
 160
 161    let snapshot = editor.snapshot(cx);
 162    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
 163
 164    let (buffer, buffer_position) = if let Some(output) = editor
 165        .buffer
 166        .read(cx)
 167        .text_anchor_for_position(multibuffer_offset, cx)
 168    {
 169        output
 170    } else {
 171        return;
 172    };
 173
 174    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
 175        .buffer()
 176        .read(cx)
 177        .excerpt_containing(multibuffer_offset, cx)
 178    {
 179        excerpt_id
 180    } else {
 181        return;
 182    };
 183
 184    let project = if let Some(project) = editor.project.clone() {
 185        project
 186    } else {
 187        return;
 188    };
 189
 190    if !ignore_timeout {
 191        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
 192            if symbol_range
 193                .as_text_range()
 194                .map(|range| {
 195                    range
 196                        .to_offset(&snapshot.buffer_snapshot)
 197                        .contains(&multibuffer_offset)
 198                })
 199                .unwrap_or(false)
 200            {
 201                // Hover triggered from same location as last time. Don't show again.
 202                return;
 203            } else {
 204                hide_hover(editor, cx);
 205            }
 206        }
 207    }
 208
 209    // Get input anchor
 210    let anchor = snapshot
 211        .buffer_snapshot
 212        .anchor_at(multibuffer_offset, Bias::Left);
 213
 214    // Don't request again if the location is the same as the previous request
 215    if let Some(triggered_from) = &editor.hover_state.triggered_from {
 216        if triggered_from
 217            .cmp(&anchor, &snapshot.buffer_snapshot)
 218            .is_eq()
 219        {
 220            return;
 221        }
 222    }
 223
 224    let task = cx.spawn(|this, mut cx| {
 225        async move {
 226            // If we need to delay, delay a set amount initially before making the lsp request
 227            let delay = if !ignore_timeout {
 228                // Construct delay task to wait for later
 229                let total_delay = Some(
 230                    cx.background()
 231                        .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
 232                );
 233
 234                cx.background()
 235                    .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
 236                    .await;
 237                total_delay
 238            } else {
 239                None
 240            };
 241
 242            // query the LSP for hover info
 243            let hover_request = cx.update(|cx| {
 244                project.update(cx, |project, cx| {
 245                    project.hover(&buffer, buffer_position, cx)
 246                })
 247            });
 248
 249            if let Some(delay) = delay {
 250                delay.await;
 251            }
 252
 253            // If there's a diagnostic, assign it on the hover state and notify
 254            let local_diagnostic = snapshot
 255                .buffer_snapshot
 256                .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
 257                // Find the entry with the most specific range
 258                .min_by_key(|entry| entry.range.end - entry.range.start)
 259                .map(|entry| DiagnosticEntry {
 260                    diagnostic: entry.diagnostic,
 261                    range: entry.range.to_anchors(&snapshot.buffer_snapshot),
 262                });
 263
 264            // Pull the primary diagnostic out so we can jump to it if the popover is clicked
 265            let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
 266                snapshot
 267                    .buffer_snapshot
 268                    .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
 269                    .find(|diagnostic| diagnostic.diagnostic.is_primary)
 270                    .map(|entry| DiagnosticEntry {
 271                        diagnostic: entry.diagnostic,
 272                        range: entry.range.to_anchors(&snapshot.buffer_snapshot),
 273                    })
 274            });
 275
 276            this.update(&mut cx, |this, _| {
 277                this.hover_state.diagnostic_popover =
 278                    local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
 279                        local_diagnostic,
 280                        primary_diagnostic,
 281                    });
 282            })?;
 283
 284            // Construct new hover popover from hover request
 285            let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
 286                if hover_result.is_empty() {
 287                    return None;
 288                }
 289
 290                // Create symbol range of anchors for highlighting and filtering
 291                // of future requests.
 292                let range = if let Some(range) = hover_result.range {
 293                    let start = snapshot
 294                        .buffer_snapshot
 295                        .anchor_in_excerpt(excerpt_id.clone(), range.start);
 296                    let end = snapshot
 297                        .buffer_snapshot
 298                        .anchor_in_excerpt(excerpt_id.clone(), range.end);
 299
 300                    start..end
 301                } else {
 302                    anchor..anchor
 303                };
 304
 305                Some(InfoPopover {
 306                    project: project.clone(),
 307                    symbol_range: DocumentRange::Text(range),
 308                    blocks: hover_result.contents,
 309                    language: hover_result.language,
 310                    rendered_content: None,
 311                })
 312            });
 313
 314            this.update(&mut cx, |this, cx| {
 315                if let Some(symbol_range) = hover_popover
 316                    .as_ref()
 317                    .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
 318                {
 319                    // Highlight the selected symbol using a background highlight
 320                    this.highlight_background::<HoverState>(
 321                        vec![symbol_range],
 322                        |theme| theme.editor.hover_popover.highlight,
 323                        cx,
 324                    );
 325                } else {
 326                    this.clear_background_highlights::<HoverState>(cx);
 327                }
 328
 329                this.hover_state.info_popover = hover_popover;
 330                cx.notify();
 331            })?;
 332
 333            Ok::<_, anyhow::Error>(())
 334        }
 335        .log_err()
 336    });
 337
 338    editor.hover_state.info_task = Some(task);
 339}
 340
 341fn render_blocks(
 342    theme_id: usize,
 343    blocks: &[HoverBlock],
 344    language_registry: &Arc<LanguageRegistry>,
 345    language: Option<&Arc<Language>>,
 346    style: &EditorStyle,
 347) -> RenderedInfo {
 348    let mut text = String::new();
 349    let mut highlights = Vec::new();
 350    let mut region_ranges = Vec::new();
 351    let mut regions = Vec::new();
 352
 353    for block in blocks {
 354        match &block.kind {
 355            HoverBlockKind::PlainText => {
 356                new_paragraph(&mut text, &mut Vec::new());
 357                text.push_str(&block.text);
 358            }
 359            HoverBlockKind::Markdown => {
 360                use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 361
 362                let mut bold_depth = 0;
 363                let mut italic_depth = 0;
 364                let mut link_url = None;
 365                let mut current_language = None;
 366                let mut list_stack = Vec::new();
 367
 368                for event in Parser::new_ext(&block.text, Options::all()) {
 369                    let prev_len = text.len();
 370                    match event {
 371                        Event::Text(t) => {
 372                            if let Some(language) = &current_language {
 373                                render_code(
 374                                    &mut text,
 375                                    &mut highlights,
 376                                    t.as_ref(),
 377                                    language,
 378                                    style,
 379                                );
 380                            } else {
 381                                text.push_str(t.as_ref());
 382
 383                                let mut style = HighlightStyle::default();
 384                                if bold_depth > 0 {
 385                                    style.weight = Some(Weight::BOLD);
 386                                }
 387                                if italic_depth > 0 {
 388                                    style.italic = Some(true);
 389                                }
 390                                if let Some(link_url) = link_url.clone() {
 391                                    region_ranges.push(prev_len..text.len());
 392                                    regions.push(RenderedRegion {
 393                                        link_url: Some(link_url),
 394                                        code: false,
 395                                    });
 396                                    style.underline = Some(Underline {
 397                                        thickness: 1.0.into(),
 398                                        ..Default::default()
 399                                    });
 400                                }
 401
 402                                if style != HighlightStyle::default() {
 403                                    let mut new_highlight = true;
 404                                    if let Some((last_range, last_style)) = highlights.last_mut() {
 405                                        if last_range.end == prev_len && last_style == &style {
 406                                            last_range.end = text.len();
 407                                            new_highlight = false;
 408                                        }
 409                                    }
 410                                    if new_highlight {
 411                                        highlights.push((prev_len..text.len(), style));
 412                                    }
 413                                }
 414                            }
 415                        }
 416                        Event::Code(t) => {
 417                            text.push_str(t.as_ref());
 418                            region_ranges.push(prev_len..text.len());
 419                            if link_url.is_some() {
 420                                highlights.push((
 421                                    prev_len..text.len(),
 422                                    HighlightStyle {
 423                                        underline: Some(Underline {
 424                                            thickness: 1.0.into(),
 425                                            ..Default::default()
 426                                        }),
 427                                        ..Default::default()
 428                                    },
 429                                ));
 430                            }
 431                            regions.push(RenderedRegion {
 432                                code: true,
 433                                link_url: link_url.clone(),
 434                            });
 435                        }
 436                        Event::Start(tag) => match tag {
 437                            Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
 438                            Tag::Heading(_, _, _) => {
 439                                new_paragraph(&mut text, &mut list_stack);
 440                                bold_depth += 1;
 441                            }
 442                            Tag::CodeBlock(kind) => {
 443                                new_paragraph(&mut text, &mut list_stack);
 444                                current_language = if let CodeBlockKind::Fenced(language) = kind {
 445                                    language_registry
 446                                        .language_for_name(language.as_ref())
 447                                        .now_or_never()
 448                                        .and_then(Result::ok)
 449                                } else {
 450                                    language.cloned()
 451                                }
 452                            }
 453                            Tag::Emphasis => italic_depth += 1,
 454                            Tag::Strong => bold_depth += 1,
 455                            Tag::Link(_, url, _) => link_url = Some(url.to_string()),
 456                            Tag::List(number) => {
 457                                list_stack.push((number, false));
 458                            }
 459                            Tag::Item => {
 460                                let len = list_stack.len();
 461                                if let Some((list_number, has_content)) = list_stack.last_mut() {
 462                                    *has_content = false;
 463                                    if !text.is_empty() && !text.ends_with('\n') {
 464                                        text.push('\n');
 465                                    }
 466                                    for _ in 0..len - 1 {
 467                                        text.push_str("  ");
 468                                    }
 469                                    if let Some(number) = list_number {
 470                                        text.push_str(&format!("{}. ", number));
 471                                        *number += 1;
 472                                        *has_content = false;
 473                                    } else {
 474                                        text.push_str("- ");
 475                                    }
 476                                }
 477                            }
 478                            _ => {}
 479                        },
 480                        Event::End(tag) => match tag {
 481                            Tag::Heading(_, _, _) => bold_depth -= 1,
 482                            Tag::CodeBlock(_) => current_language = None,
 483                            Tag::Emphasis => italic_depth -= 1,
 484                            Tag::Strong => bold_depth -= 1,
 485                            Tag::Link(_, _, _) => link_url = None,
 486                            Tag::List(_) => drop(list_stack.pop()),
 487                            _ => {}
 488                        },
 489                        Event::HardBreak => text.push('\n'),
 490                        Event::SoftBreak => text.push(' '),
 491                        _ => {}
 492                    }
 493                }
 494            }
 495            HoverBlockKind::Code { language } => {
 496                if let Some(language) = language_registry
 497                    .language_for_name(language)
 498                    .now_or_never()
 499                    .and_then(Result::ok)
 500                {
 501                    render_code(&mut text, &mut highlights, &block.text, &language, style);
 502                } else {
 503                    text.push_str(&block.text);
 504                }
 505            }
 506        }
 507    }
 508
 509    RenderedInfo {
 510        theme_id,
 511        text: text.trim().to_string(),
 512        highlights,
 513        region_ranges,
 514        regions,
 515    }
 516}
 517
 518fn render_code(
 519    text: &mut String,
 520    highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
 521    content: &str,
 522    language: &Arc<Language>,
 523    style: &EditorStyle,
 524) {
 525    let prev_len = text.len();
 526    text.push_str(content);
 527    for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
 528        if let Some(style) = highlight_id.style(&style.syntax) {
 529            highlights.push((prev_len + range.start..prev_len + range.end, style));
 530        }
 531    }
 532}
 533
 534fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
 535    let mut is_subsequent_paragraph_of_list = false;
 536    if let Some((_, has_content)) = list_stack.last_mut() {
 537        if *has_content {
 538            is_subsequent_paragraph_of_list = true;
 539        } else {
 540            *has_content = true;
 541            return;
 542        }
 543    }
 544
 545    if !text.is_empty() {
 546        if !text.ends_with('\n') {
 547            text.push('\n');
 548        }
 549        text.push('\n');
 550    }
 551    for _ in 0..list_stack.len().saturating_sub(1) {
 552        text.push_str("  ");
 553    }
 554    if is_subsequent_paragraph_of_list {
 555        text.push_str("  ");
 556    }
 557}
 558
 559#[derive(Default)]
 560pub struct HoverState {
 561    pub info_popover: Option<InfoPopover>,
 562    pub diagnostic_popover: Option<DiagnosticPopover>,
 563    pub triggered_from: Option<Anchor>,
 564    pub info_task: Option<Task<Option<()>>>,
 565}
 566
 567impl HoverState {
 568    pub fn visible(&self) -> bool {
 569        self.info_popover.is_some() || self.diagnostic_popover.is_some()
 570    }
 571
 572    pub fn render(
 573        &mut self,
 574        snapshot: &EditorSnapshot,
 575        style: &EditorStyle,
 576        visible_rows: Range<u32>,
 577        cx: &mut ViewContext<Editor>,
 578    ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
 579        // If there is a diagnostic, position the popovers based on that.
 580        // Otherwise use the start of the hover range
 581        let anchor = self
 582            .diagnostic_popover
 583            .as_ref()
 584            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
 585            .or_else(|| {
 586                self.info_popover
 587                    .as_ref()
 588                    .map(|info_popover| match &info_popover.symbol_range {
 589                        DocumentRange::Text(range) => &range.start,
 590                        DocumentRange::Inlay(range) => &range.inlay_position,
 591                    })
 592            })?;
 593        let point = anchor.to_display_point(&snapshot.display_snapshot);
 594
 595        // Don't render if the relevant point isn't on screen
 596        if !self.visible() || !visible_rows.contains(&point.row()) {
 597            return None;
 598        }
 599
 600        let mut elements = Vec::new();
 601
 602        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
 603            elements.push(diagnostic_popover.render(style, cx));
 604        }
 605        if let Some(info_popover) = self.info_popover.as_mut() {
 606            elements.push(info_popover.render(style, cx));
 607        }
 608
 609        Some((point, elements))
 610    }
 611}
 612
 613#[derive(Debug, Clone)]
 614pub struct InfoPopover {
 615    pub project: ModelHandle<Project>,
 616    symbol_range: DocumentRange,
 617    pub blocks: Vec<HoverBlock>,
 618    language: Option<Arc<Language>>,
 619    rendered_content: Option<RenderedInfo>,
 620}
 621
 622#[derive(Debug, Clone)]
 623struct RenderedInfo {
 624    theme_id: usize,
 625    text: String,
 626    highlights: Vec<(Range<usize>, HighlightStyle)>,
 627    region_ranges: Vec<Range<usize>>,
 628    regions: Vec<RenderedRegion>,
 629}
 630
 631#[derive(Debug, Clone)]
 632struct RenderedRegion {
 633    code: bool,
 634    link_url: Option<String>,
 635}
 636
 637impl InfoPopover {
 638    pub fn render(
 639        &mut self,
 640        style: &EditorStyle,
 641        cx: &mut ViewContext<Editor>,
 642    ) -> AnyElement<Editor> {
 643        if let Some(rendered) = &self.rendered_content {
 644            if rendered.theme_id != style.theme_id {
 645                self.rendered_content = None;
 646            }
 647        }
 648
 649        let rendered_content = self.rendered_content.get_or_insert_with(|| {
 650            render_blocks(
 651                style.theme_id,
 652                &self.blocks,
 653                self.project.read(cx).languages(),
 654                self.language.as_ref(),
 655                style,
 656            )
 657        });
 658
 659        MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
 660            let mut region_id = 0;
 661            let view_id = cx.view_id();
 662
 663            let code_span_background_color = style.document_highlight_read_background;
 664            let regions = rendered_content.regions.clone();
 665            Flex::column()
 666                .scrollable::<HoverBlock>(1, None, cx)
 667                .with_child(
 668                    Text::new(rendered_content.text.clone(), style.text.clone())
 669                        .with_highlights(rendered_content.highlights.clone())
 670                        .with_custom_runs(
 671                            rendered_content.region_ranges.clone(),
 672                            move |ix, bounds, scene, _| {
 673                                region_id += 1;
 674                                let region = regions[ix].clone();
 675                                if let Some(url) = region.link_url {
 676                                    scene.push_cursor_region(CursorRegion {
 677                                        bounds,
 678                                        style: CursorStyle::PointingHand,
 679                                    });
 680                                    scene.push_mouse_region(
 681                                        MouseRegion::new::<Self>(view_id, region_id, bounds)
 682                                            .on_click::<Editor, _>(
 683                                                MouseButton::Left,
 684                                                move |_, _, cx| cx.platform().open_url(&url),
 685                                            ),
 686                                    );
 687                                }
 688                                if region.code {
 689                                    scene.push_quad(gpui::Quad {
 690                                        bounds,
 691                                        background: Some(code_span_background_color),
 692                                        border: Default::default(),
 693                                        corner_radii: (2.0).into(),
 694                                    });
 695                                }
 696                            },
 697                        )
 698                        .with_soft_wrap(true),
 699                )
 700                .contained()
 701                .with_style(style.hover_popover.container)
 702        })
 703        .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
 704        .with_cursor_style(CursorStyle::Arrow)
 705        .with_padding(Padding {
 706            bottom: HOVER_POPOVER_GAP,
 707            top: HOVER_POPOVER_GAP,
 708            ..Default::default()
 709        })
 710        .into_any()
 711    }
 712}
 713
 714#[derive(Debug, Clone)]
 715pub struct DiagnosticPopover {
 716    local_diagnostic: DiagnosticEntry<Anchor>,
 717    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
 718}
 719
 720impl DiagnosticPopover {
 721    pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
 722        enum PrimaryDiagnostic {}
 723
 724        let mut text_style = style.hover_popover.prose.clone();
 725        text_style.font_size = style.text.font_size;
 726        let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
 727
 728        let text = match &self.local_diagnostic.diagnostic.source {
 729            Some(source) => Text::new(
 730                format!("{source}: {}", self.local_diagnostic.diagnostic.message),
 731                text_style,
 732            )
 733            .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
 734
 735            None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
 736        };
 737
 738        let container_style = match self.local_diagnostic.diagnostic.severity {
 739            DiagnosticSeverity::HINT => style.hover_popover.info_container,
 740            DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
 741            DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
 742            DiagnosticSeverity::ERROR => style.hover_popover.error_container,
 743            _ => style.hover_popover.container,
 744        };
 745
 746        let tooltip_style = theme::current(cx).tooltip.clone();
 747
 748        MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
 749            text.with_soft_wrap(true)
 750                .contained()
 751                .with_style(container_style)
 752        })
 753        .with_padding(Padding {
 754            top: HOVER_POPOVER_GAP,
 755            bottom: HOVER_POPOVER_GAP,
 756            ..Default::default()
 757        })
 758        .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
 759        .on_click(MouseButton::Left, |_, this, cx| {
 760            this.go_to_diagnostic(&Default::default(), cx)
 761        })
 762        .with_cursor_style(CursorStyle::PointingHand)
 763        .with_tooltip::<PrimaryDiagnostic>(
 764            0,
 765            "Go To Diagnostic".to_string(),
 766            Some(Box::new(crate::GoToDiagnostic)),
 767            tooltip_style,
 768            cx,
 769        )
 770        .into_any()
 771    }
 772
 773    pub fn activation_info(&self) -> (usize, Anchor) {
 774        let entry = self
 775            .primary_diagnostic
 776            .as_ref()
 777            .unwrap_or(&self.local_diagnostic);
 778
 779        (entry.diagnostic.group_id, entry.range.start.clone())
 780    }
 781}
 782
 783#[cfg(test)]
 784mod tests {
 785    use super::*;
 786    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
 787    use gpui::fonts::Weight;
 788    use indoc::indoc;
 789    use language::{Diagnostic, DiagnosticSet};
 790    use lsp::LanguageServerId;
 791    use project::{HoverBlock, HoverBlockKind};
 792    use smol::stream::StreamExt;
 793    use unindent::Unindent;
 794    use util::test::marked_text_ranges;
 795
 796    #[gpui::test]
 797    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
 798        init_test(cx, |_| {});
 799
 800        let mut cx = EditorLspTestContext::new_rust(
 801            lsp::ServerCapabilities {
 802                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 803                ..Default::default()
 804            },
 805            cx,
 806        )
 807        .await;
 808
 809        // Basic hover delays and then pops without moving the mouse
 810        cx.set_state(indoc! {"
 811            fn ˇtest() { println!(); }
 812        "});
 813        let hover_point = cx.display_point(indoc! {"
 814            fn test() { printˇln!(); }
 815        "});
 816
 817        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 818        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 819
 820        // After delay, hover should be visible.
 821        let symbol_range = cx.lsp_range(indoc! {"
 822            fn test() { «println!»(); }
 823        "});
 824        let mut requests =
 825            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 826                Ok(Some(lsp::Hover {
 827                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 828                        kind: lsp::MarkupKind::Markdown,
 829                        value: "some basic docs".to_string(),
 830                    }),
 831                    range: Some(symbol_range),
 832                }))
 833            });
 834        cx.foreground()
 835            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 836        requests.next().await;
 837
 838        cx.editor(|editor, _| {
 839            assert!(editor.hover_state.visible());
 840            assert_eq!(
 841                editor.hover_state.info_popover.clone().unwrap().blocks,
 842                vec![HoverBlock {
 843                    text: "some basic docs".to_string(),
 844                    kind: HoverBlockKind::Markdown,
 845                },]
 846            )
 847        });
 848
 849        // Mouse moved with no hover response dismisses
 850        let hover_point = cx.display_point(indoc! {"
 851            fn teˇst() { println!(); }
 852        "});
 853        let mut request = cx
 854            .lsp
 855            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
 856        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
 857        cx.foreground()
 858            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
 859        request.next().await;
 860        cx.editor(|editor, _| {
 861            assert!(!editor.hover_state.visible());
 862        });
 863    }
 864
 865    #[gpui::test]
 866    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
 867        init_test(cx, |_| {});
 868
 869        let mut cx = EditorLspTestContext::new_rust(
 870            lsp::ServerCapabilities {
 871                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 872                ..Default::default()
 873            },
 874            cx,
 875        )
 876        .await;
 877
 878        // Hover with keyboard has no delay
 879        cx.set_state(indoc! {"
 880            fˇn test() { println!(); }
 881        "});
 882        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 883        let symbol_range = cx.lsp_range(indoc! {"
 884            «fn» test() { println!(); }
 885        "});
 886        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 887            Ok(Some(lsp::Hover {
 888                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 889                    kind: lsp::MarkupKind::Markdown,
 890                    value: "some other basic docs".to_string(),
 891                }),
 892                range: Some(symbol_range),
 893            }))
 894        })
 895        .next()
 896        .await;
 897
 898        cx.condition(|editor, _| editor.hover_state.visible()).await;
 899        cx.editor(|editor, _| {
 900            assert_eq!(
 901                editor.hover_state.info_popover.clone().unwrap().blocks,
 902                vec![HoverBlock {
 903                    text: "some other basic docs".to_string(),
 904                    kind: HoverBlockKind::Markdown,
 905                }]
 906            )
 907        });
 908    }
 909
 910    #[gpui::test]
 911    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
 912        init_test(cx, |_| {});
 913
 914        let mut cx = EditorLspTestContext::new_rust(
 915            lsp::ServerCapabilities {
 916                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 917                ..Default::default()
 918            },
 919            cx,
 920        )
 921        .await;
 922
 923        // Hover with keyboard has no delay
 924        cx.set_state(indoc! {"
 925            fˇn test() { println!(); }
 926        "});
 927        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 928        let symbol_range = cx.lsp_range(indoc! {"
 929            «fn» test() { println!(); }
 930        "});
 931        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
 932            Ok(Some(lsp::Hover {
 933                contents: lsp::HoverContents::Array(vec![
 934                    lsp::MarkedString::String("regular text for hover to show".to_string()),
 935                    lsp::MarkedString::String("".to_string()),
 936                    lsp::MarkedString::LanguageString(lsp::LanguageString {
 937                        language: "Rust".to_string(),
 938                        value: "".to_string(),
 939                    }),
 940                ]),
 941                range: Some(symbol_range),
 942            }))
 943        })
 944        .next()
 945        .await;
 946
 947        cx.condition(|editor, _| editor.hover_state.visible()).await;
 948        cx.editor(|editor, _| {
 949            assert_eq!(
 950                editor.hover_state.info_popover.clone().unwrap().blocks,
 951                vec![HoverBlock {
 952                    text: "regular text for hover to show".to_string(),
 953                    kind: HoverBlockKind::Markdown,
 954                }],
 955                "No empty string hovers should be shown"
 956            );
 957        });
 958    }
 959
 960    #[gpui::test]
 961    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
 962        init_test(cx, |_| {});
 963
 964        let mut cx = EditorLspTestContext::new_rust(
 965            lsp::ServerCapabilities {
 966                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 967                ..Default::default()
 968            },
 969            cx,
 970        )
 971        .await;
 972
 973        // Hover with keyboard has no delay
 974        cx.set_state(indoc! {"
 975            fˇn test() { println!(); }
 976        "});
 977        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
 978        let symbol_range = cx.lsp_range(indoc! {"
 979            «fn» test() { println!(); }
 980        "});
 981
 982        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
 983        let markdown_string = format!("\n```rust\n{code_str}```");
 984
 985        let closure_markdown_string = markdown_string.clone();
 986        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
 987            let future_markdown_string = closure_markdown_string.clone();
 988            async move {
 989                Ok(Some(lsp::Hover {
 990                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
 991                        kind: lsp::MarkupKind::Markdown,
 992                        value: future_markdown_string,
 993                    }),
 994                    range: Some(symbol_range),
 995                }))
 996            }
 997        })
 998        .next()
 999        .await;
1000
1001        cx.condition(|editor, _| editor.hover_state.visible()).await;
1002        cx.editor(|editor, cx| {
1003            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
1004            assert_eq!(
1005                blocks,
1006                vec![HoverBlock {
1007                    text: markdown_string,
1008                    kind: HoverBlockKind::Markdown,
1009                }],
1010            );
1011
1012            let style = editor.style(cx);
1013            let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
1014            assert_eq!(
1015                rendered.text,
1016                code_str.trim(),
1017                "Should not have extra line breaks at end of rendered hover"
1018            );
1019        });
1020    }
1021
1022    #[gpui::test]
1023    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1024        init_test(cx, |_| {});
1025
1026        let mut cx = EditorLspTestContext::new_rust(
1027            lsp::ServerCapabilities {
1028                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1029                ..Default::default()
1030            },
1031            cx,
1032        )
1033        .await;
1034
1035        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1036        // info popover once request completes
1037        cx.set_state(indoc! {"
1038            fn teˇst() { println!(); }
1039        "});
1040
1041        // Send diagnostic to client
1042        let range = cx.text_anchor_range(indoc! {"
1043            fn «test»() { println!(); }
1044        "});
1045        cx.update_buffer(|buffer, cx| {
1046            let snapshot = buffer.text_snapshot();
1047            let set = DiagnosticSet::from_sorted_entries(
1048                vec![DiagnosticEntry {
1049                    range,
1050                    diagnostic: Diagnostic {
1051                        message: "A test diagnostic message.".to_string(),
1052                        ..Default::default()
1053                    },
1054                }],
1055                &snapshot,
1056            );
1057            buffer.update_diagnostics(LanguageServerId(0), set, cx);
1058        });
1059
1060        // Hover pops diagnostic immediately
1061        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1062        cx.foreground().run_until_parked();
1063
1064        cx.editor(|Editor { hover_state, .. }, _| {
1065            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
1066        });
1067
1068        // Info Popover shows after request responded to
1069        let range = cx.lsp_range(indoc! {"
1070            fn «test»() { println!(); }
1071        "});
1072        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1073            Ok(Some(lsp::Hover {
1074                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1075                    kind: lsp::MarkupKind::Markdown,
1076                    value: "some new docs".to_string(),
1077                }),
1078                range: Some(range),
1079            }))
1080        });
1081        cx.foreground()
1082            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1083
1084        cx.foreground().run_until_parked();
1085        cx.editor(|Editor { hover_state, .. }, _| {
1086            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1087        });
1088    }
1089
1090    #[gpui::test]
1091    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
1092        init_test(cx, |_| {});
1093
1094        cx.add_window(|cx| {
1095            let editor = Editor::single_line(None, cx);
1096            let style = editor.style(cx);
1097
1098            struct Row {
1099                blocks: Vec<HoverBlock>,
1100                expected_marked_text: String,
1101                expected_styles: Vec<HighlightStyle>,
1102            }
1103
1104            let rows = &[
1105                // Strong emphasis
1106                Row {
1107                    blocks: vec![HoverBlock {
1108                        text: "one **two** three".to_string(),
1109                        kind: HoverBlockKind::Markdown,
1110                    }],
1111                    expected_marked_text: "one «two» three".to_string(),
1112                    expected_styles: vec![HighlightStyle {
1113                        weight: Some(Weight::BOLD),
1114                        ..Default::default()
1115                    }],
1116                },
1117                // Links
1118                Row {
1119                    blocks: vec![HoverBlock {
1120                        text: "one [two](the-url) three".to_string(),
1121                        kind: HoverBlockKind::Markdown,
1122                    }],
1123                    expected_marked_text: "one «two» three".to_string(),
1124                    expected_styles: vec![HighlightStyle {
1125                        underline: Some(Underline {
1126                            thickness: 1.0.into(),
1127                            ..Default::default()
1128                        }),
1129                        ..Default::default()
1130                    }],
1131                },
1132                // Lists
1133                Row {
1134                    blocks: vec![HoverBlock {
1135                        text: "
1136                            lists:
1137                            * one
1138                                - a
1139                                - b
1140                            * two
1141                                - [c](the-url)
1142                                - d"
1143                        .unindent(),
1144                        kind: HoverBlockKind::Markdown,
1145                    }],
1146                    expected_marked_text: "
1147                        lists:
1148                        - one
1149                          - a
1150                          - b
1151                        - two
1152                          - «c»
1153                          - d"
1154                    .unindent(),
1155                    expected_styles: vec![HighlightStyle {
1156                        underline: Some(Underline {
1157                            thickness: 1.0.into(),
1158                            ..Default::default()
1159                        }),
1160                        ..Default::default()
1161                    }],
1162                },
1163                // Multi-paragraph list items
1164                Row {
1165                    blocks: vec![HoverBlock {
1166                        text: "
1167                            * one two
1168                              three
1169
1170                            * four five
1171                                * six seven
1172                                  eight
1173
1174                                  nine
1175                                * ten
1176                            * six"
1177                            .unindent(),
1178                        kind: HoverBlockKind::Markdown,
1179                    }],
1180                    expected_marked_text: "
1181                        - one two three
1182                        - four five
1183                          - six seven eight
1184
1185                            nine
1186                          - ten
1187                        - six"
1188                        .unindent(),
1189                    expected_styles: vec![HighlightStyle {
1190                        underline: Some(Underline {
1191                            thickness: 1.0.into(),
1192                            ..Default::default()
1193                        }),
1194                        ..Default::default()
1195                    }],
1196                },
1197            ];
1198
1199            for Row {
1200                blocks,
1201                expected_marked_text,
1202                expected_styles,
1203            } in &rows[0..]
1204            {
1205                let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
1206
1207                let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
1208                let expected_highlights = ranges
1209                    .into_iter()
1210                    .zip(expected_styles.iter().cloned())
1211                    .collect::<Vec<_>>();
1212                assert_eq!(
1213                    rendered.text, expected_text,
1214                    "wrong text for input {blocks:?}"
1215                );
1216                assert_eq!(
1217                    rendered.highlights, expected_highlights,
1218                    "wrong highlights for input {blocks:?}"
1219                );
1220            }
1221
1222            editor
1223        });
1224    }
1225}