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