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) = ¤t_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 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}