hover_popover.rs

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