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