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