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