hover_popover.rs

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