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