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