1use crate::{
2 display_map::{InlayOffset, ToDisplayPoint},
3 link_go_to_definition::{DocumentRange, InlayRange},
4 markdown::{self, RenderedRegion},
5 Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
6 ExcerptId, RangeToAnchorExt,
7};
8use futures::FutureExt;
9use gpui::{
10 actions,
11 elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
12 fonts::HighlightStyle,
13 platform::{CursorStyle, MouseButton},
14 AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
15};
16use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
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 rendered_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 rendered_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
364fn render_blocks(
365 theme_id: usize,
366 blocks: &[HoverBlock],
367 language_registry: &Arc<LanguageRegistry>,
368 language: &Option<Arc<Language>>,
369 style: &EditorStyle,
370) -> RenderedInfo {
371 let mut text = String::new();
372 let mut highlights = Vec::new();
373 let mut region_ranges = Vec::new();
374 let mut regions = Vec::new();
375
376 for block in blocks {
377 match &block.kind {
378 HoverBlockKind::PlainText => {
379 markdown::new_paragraph(&mut text, &mut Vec::new());
380 text.push_str(&block.text);
381 }
382
383 HoverBlockKind::Markdown => markdown::render_markdown_block(
384 &block.text,
385 language_registry,
386 language,
387 style,
388 &mut text,
389 &mut highlights,
390 &mut region_ranges,
391 &mut regions,
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::render_code(
401 &mut text,
402 &mut highlights,
403 &block.text,
404 &language,
405 style,
406 );
407 } else {
408 text.push_str(&block.text);
409 }
410 }
411 }
412 }
413
414 RenderedInfo {
415 theme_id,
416 text: text.trim().to_string(),
417 highlights,
418 region_ranges,
419 regions,
420 }
421}
422
423#[derive(Default)]
424pub struct HoverState {
425 pub info_popover: Option<InfoPopover>,
426 pub diagnostic_popover: Option<DiagnosticPopover>,
427 pub triggered_from: Option<Anchor>,
428 pub info_task: Option<Task<Option<()>>>,
429}
430
431impl HoverState {
432 pub fn visible(&self) -> bool {
433 self.info_popover.is_some() || self.diagnostic_popover.is_some()
434 }
435
436 pub fn render(
437 &mut self,
438 snapshot: &EditorSnapshot,
439 style: &EditorStyle,
440 visible_rows: Range<u32>,
441 cx: &mut ViewContext<Editor>,
442 ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
443 // If there is a diagnostic, position the popovers based on that.
444 // Otherwise use the start of the hover range
445 let anchor = self
446 .diagnostic_popover
447 .as_ref()
448 .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
449 .or_else(|| {
450 self.info_popover
451 .as_ref()
452 .map(|info_popover| match &info_popover.symbol_range {
453 DocumentRange::Text(range) => &range.start,
454 DocumentRange::Inlay(range) => &range.inlay_position,
455 })
456 })?;
457 let point = anchor.to_display_point(&snapshot.display_snapshot);
458
459 // Don't render if the relevant point isn't on screen
460 if !self.visible() || !visible_rows.contains(&point.row()) {
461 return None;
462 }
463
464 let mut elements = Vec::new();
465
466 if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
467 elements.push(diagnostic_popover.render(style, cx));
468 }
469 if let Some(info_popover) = self.info_popover.as_mut() {
470 elements.push(info_popover.render(style, cx));
471 }
472
473 Some((point, elements))
474 }
475}
476
477#[derive(Debug, Clone)]
478pub struct InfoPopover {
479 pub project: ModelHandle<Project>,
480 symbol_range: DocumentRange,
481 pub blocks: Vec<HoverBlock>,
482 language: Option<Arc<Language>>,
483 rendered_content: Option<RenderedInfo>,
484}
485
486#[derive(Debug, Clone)]
487struct RenderedInfo {
488 theme_id: usize,
489 text: String,
490 highlights: Vec<(Range<usize>, HighlightStyle)>,
491 region_ranges: Vec<Range<usize>>,
492 regions: Vec<RenderedRegion>,
493}
494
495impl InfoPopover {
496 pub fn render(
497 &mut self,
498 style: &EditorStyle,
499 cx: &mut ViewContext<Editor>,
500 ) -> AnyElement<Editor> {
501 if let Some(rendered) = &self.rendered_content {
502 if rendered.theme_id != style.theme_id {
503 self.rendered_content = None;
504 }
505 }
506
507 let rendered_content = self.rendered_content.get_or_insert_with(|| {
508 render_blocks(
509 style.theme_id,
510 &self.blocks,
511 self.project.read(cx).languages(),
512 &self.language,
513 style,
514 )
515 });
516
517 MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
518 let mut region_id = 0;
519 let view_id = cx.view_id();
520
521 let code_span_background_color = style.document_highlight_read_background;
522 let regions = rendered_content.regions.clone();
523 Flex::column()
524 .scrollable::<HoverBlock>(1, None, cx)
525 .with_child(
526 Text::new(rendered_content.text.clone(), style.text.clone())
527 .with_highlights(rendered_content.highlights.clone())
528 .with_custom_runs(
529 rendered_content.region_ranges.clone(),
530 move |ix, bounds, scene, _| {
531 region_id += 1;
532 let region = regions[ix].clone();
533 if let Some(url) = region.link_url {
534 scene.push_cursor_region(CursorRegion {
535 bounds,
536 style: CursorStyle::PointingHand,
537 });
538 scene.push_mouse_region(
539 MouseRegion::new::<Self>(view_id, region_id, bounds)
540 .on_click::<Editor, _>(
541 MouseButton::Left,
542 move |_, _, cx| cx.platform().open_url(&url),
543 ),
544 );
545 }
546 if region.code {
547 scene.push_quad(gpui::Quad {
548 bounds,
549 background: Some(code_span_background_color),
550 border: Default::default(),
551 corner_radii: (2.0).into(),
552 });
553 }
554 },
555 )
556 .with_soft_wrap(true),
557 )
558 .contained()
559 .with_style(style.hover_popover.container)
560 })
561 .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
562 .with_cursor_style(CursorStyle::Arrow)
563 .with_padding(Padding {
564 bottom: HOVER_POPOVER_GAP,
565 top: HOVER_POPOVER_GAP,
566 ..Default::default()
567 })
568 .into_any()
569 }
570}
571
572#[derive(Debug, Clone)]
573pub struct DiagnosticPopover {
574 local_diagnostic: DiagnosticEntry<Anchor>,
575 primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
576}
577
578impl DiagnosticPopover {
579 pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
580 enum PrimaryDiagnostic {}
581
582 let mut text_style = style.hover_popover.prose.clone();
583 text_style.font_size = style.text.font_size;
584 let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
585
586 let text = match &self.local_diagnostic.diagnostic.source {
587 Some(source) => Text::new(
588 format!("{source}: {}", self.local_diagnostic.diagnostic.message),
589 text_style,
590 )
591 .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
592
593 None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
594 };
595
596 let container_style = match self.local_diagnostic.diagnostic.severity {
597 DiagnosticSeverity::HINT => style.hover_popover.info_container,
598 DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
599 DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
600 DiagnosticSeverity::ERROR => style.hover_popover.error_container,
601 _ => style.hover_popover.container,
602 };
603
604 let tooltip_style = theme::current(cx).tooltip.clone();
605
606 MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
607 text.with_soft_wrap(true)
608 .contained()
609 .with_style(container_style)
610 })
611 .with_padding(Padding {
612 top: HOVER_POPOVER_GAP,
613 bottom: HOVER_POPOVER_GAP,
614 ..Default::default()
615 })
616 .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
617 .on_click(MouseButton::Left, |_, this, cx| {
618 this.go_to_diagnostic(&Default::default(), cx)
619 })
620 .with_cursor_style(CursorStyle::PointingHand)
621 .with_tooltip::<PrimaryDiagnostic>(
622 0,
623 "Go To Diagnostic".to_string(),
624 Some(Box::new(crate::GoToDiagnostic)),
625 tooltip_style,
626 cx,
627 )
628 .into_any()
629 }
630
631 pub fn activation_info(&self) -> (usize, Anchor) {
632 let entry = self
633 .primary_diagnostic
634 .as_ref()
635 .unwrap_or(&self.local_diagnostic);
636
637 (entry.diagnostic.group_id, entry.range.start.clone())
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use crate::{
645 editor_tests::init_test,
646 element::PointForPosition,
647 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
648 link_go_to_definition::update_inlay_link_and_hover_points,
649 test::editor_lsp_test_context::EditorLspTestContext,
650 };
651 use collections::BTreeSet;
652 use gpui::fonts::{Underline, Weight};
653 use indoc::indoc;
654 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
655 use lsp::LanguageServerId;
656 use project::{HoverBlock, HoverBlockKind};
657 use smol::stream::StreamExt;
658 use unindent::Unindent;
659 use util::test::marked_text_ranges;
660
661 #[gpui::test]
662 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
663 init_test(cx, |_| {});
664
665 let mut cx = EditorLspTestContext::new_rust(
666 lsp::ServerCapabilities {
667 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
668 ..Default::default()
669 },
670 cx,
671 )
672 .await;
673
674 // Basic hover delays and then pops without moving the mouse
675 cx.set_state(indoc! {"
676 fn ˇtest() { println!(); }
677 "});
678 let hover_point = cx.display_point(indoc! {"
679 fn test() { printˇln!(); }
680 "});
681
682 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
683 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
684
685 // After delay, hover should be visible.
686 let symbol_range = cx.lsp_range(indoc! {"
687 fn test() { «println!»(); }
688 "});
689 let mut requests =
690 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
691 Ok(Some(lsp::Hover {
692 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
693 kind: lsp::MarkupKind::Markdown,
694 value: "some basic docs".to_string(),
695 }),
696 range: Some(symbol_range),
697 }))
698 });
699 cx.foreground()
700 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
701 requests.next().await;
702
703 cx.editor(|editor, _| {
704 assert!(editor.hover_state.visible());
705 assert_eq!(
706 editor.hover_state.info_popover.clone().unwrap().blocks,
707 vec![HoverBlock {
708 text: "some basic docs".to_string(),
709 kind: HoverBlockKind::Markdown,
710 },]
711 )
712 });
713
714 // Mouse moved with no hover response dismisses
715 let hover_point = cx.display_point(indoc! {"
716 fn teˇst() { println!(); }
717 "});
718 let mut request = cx
719 .lsp
720 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
721 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
722 cx.foreground()
723 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
724 request.next().await;
725 cx.editor(|editor, _| {
726 assert!(!editor.hover_state.visible());
727 });
728 }
729
730 #[gpui::test]
731 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
732 init_test(cx, |_| {});
733
734 let mut cx = EditorLspTestContext::new_rust(
735 lsp::ServerCapabilities {
736 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
737 ..Default::default()
738 },
739 cx,
740 )
741 .await;
742
743 // Hover with keyboard has no delay
744 cx.set_state(indoc! {"
745 fˇn test() { println!(); }
746 "});
747 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
748 let symbol_range = cx.lsp_range(indoc! {"
749 «fn» test() { println!(); }
750 "});
751 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
752 Ok(Some(lsp::Hover {
753 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
754 kind: lsp::MarkupKind::Markdown,
755 value: "some other basic docs".to_string(),
756 }),
757 range: Some(symbol_range),
758 }))
759 })
760 .next()
761 .await;
762
763 cx.condition(|editor, _| editor.hover_state.visible()).await;
764 cx.editor(|editor, _| {
765 assert_eq!(
766 editor.hover_state.info_popover.clone().unwrap().blocks,
767 vec![HoverBlock {
768 text: "some other basic docs".to_string(),
769 kind: HoverBlockKind::Markdown,
770 }]
771 )
772 });
773 }
774
775 #[gpui::test]
776 async fn test_empty_hovers_filtered(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 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
797 Ok(Some(lsp::Hover {
798 contents: lsp::HoverContents::Array(vec![
799 lsp::MarkedString::String("regular text for hover to show".to_string()),
800 lsp::MarkedString::String("".to_string()),
801 lsp::MarkedString::LanguageString(lsp::LanguageString {
802 language: "Rust".to_string(),
803 value: "".to_string(),
804 }),
805 ]),
806 range: Some(symbol_range),
807 }))
808 })
809 .next()
810 .await;
811
812 cx.condition(|editor, _| editor.hover_state.visible()).await;
813 cx.editor(|editor, _| {
814 assert_eq!(
815 editor.hover_state.info_popover.clone().unwrap().blocks,
816 vec![HoverBlock {
817 text: "regular text for hover to show".to_string(),
818 kind: HoverBlockKind::Markdown,
819 }],
820 "No empty string hovers should be shown"
821 );
822 });
823 }
824
825 #[gpui::test]
826 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
827 init_test(cx, |_| {});
828
829 let mut cx = EditorLspTestContext::new_rust(
830 lsp::ServerCapabilities {
831 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
832 ..Default::default()
833 },
834 cx,
835 )
836 .await;
837
838 // Hover with keyboard has no delay
839 cx.set_state(indoc! {"
840 fˇn test() { println!(); }
841 "});
842 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
843 let symbol_range = cx.lsp_range(indoc! {"
844 «fn» test() { println!(); }
845 "});
846
847 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
848 let markdown_string = format!("\n```rust\n{code_str}```");
849
850 let closure_markdown_string = markdown_string.clone();
851 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
852 let future_markdown_string = closure_markdown_string.clone();
853 async move {
854 Ok(Some(lsp::Hover {
855 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
856 kind: lsp::MarkupKind::Markdown,
857 value: future_markdown_string,
858 }),
859 range: Some(symbol_range),
860 }))
861 }
862 })
863 .next()
864 .await;
865
866 cx.condition(|editor, _| editor.hover_state.visible()).await;
867 cx.editor(|editor, cx| {
868 let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
869 assert_eq!(
870 blocks,
871 vec![HoverBlock {
872 text: markdown_string,
873 kind: HoverBlockKind::Markdown,
874 }],
875 );
876
877 let style = editor.style(cx);
878 let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style);
879 assert_eq!(
880 rendered.text,
881 code_str.trim(),
882 "Should not have extra line breaks at end of rendered hover"
883 );
884 });
885 }
886
887 #[gpui::test]
888 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
889 init_test(cx, |_| {});
890
891 let mut cx = EditorLspTestContext::new_rust(
892 lsp::ServerCapabilities {
893 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
894 ..Default::default()
895 },
896 cx,
897 )
898 .await;
899
900 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
901 // info popover once request completes
902 cx.set_state(indoc! {"
903 fn teˇst() { println!(); }
904 "});
905
906 // Send diagnostic to client
907 let range = cx.text_anchor_range(indoc! {"
908 fn «test»() { println!(); }
909 "});
910 cx.update_buffer(|buffer, cx| {
911 let snapshot = buffer.text_snapshot();
912 let set = DiagnosticSet::from_sorted_entries(
913 vec![DiagnosticEntry {
914 range,
915 diagnostic: Diagnostic {
916 message: "A test diagnostic message.".to_string(),
917 ..Default::default()
918 },
919 }],
920 &snapshot,
921 );
922 buffer.update_diagnostics(LanguageServerId(0), set, cx);
923 });
924
925 // Hover pops diagnostic immediately
926 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
927 cx.foreground().run_until_parked();
928
929 cx.editor(|Editor { hover_state, .. }, _| {
930 assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
931 });
932
933 // Info Popover shows after request responded to
934 let range = cx.lsp_range(indoc! {"
935 fn «test»() { println!(); }
936 "});
937 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
938 Ok(Some(lsp::Hover {
939 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
940 kind: lsp::MarkupKind::Markdown,
941 value: "some new docs".to_string(),
942 }),
943 range: Some(range),
944 }))
945 });
946 cx.foreground()
947 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
948
949 cx.foreground().run_until_parked();
950 cx.editor(|Editor { hover_state, .. }, _| {
951 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
952 });
953 }
954
955 #[gpui::test]
956 fn test_render_blocks(cx: &mut gpui::TestAppContext) {
957 init_test(cx, |_| {});
958
959 cx.add_window(|cx| {
960 let editor = Editor::single_line(None, cx);
961 let style = editor.style(cx);
962
963 struct Row {
964 blocks: Vec<HoverBlock>,
965 expected_marked_text: String,
966 expected_styles: Vec<HighlightStyle>,
967 }
968
969 let rows = &[
970 // Strong emphasis
971 Row {
972 blocks: vec![HoverBlock {
973 text: "one **two** three".to_string(),
974 kind: HoverBlockKind::Markdown,
975 }],
976 expected_marked_text: "one «two» three".to_string(),
977 expected_styles: vec![HighlightStyle {
978 weight: Some(Weight::BOLD),
979 ..Default::default()
980 }],
981 },
982 // Links
983 Row {
984 blocks: vec three".to_string(),
986 kind: HoverBlockKind::Markdown,
987 }],
988 expected_marked_text: "one «two» three".to_string(),
989 expected_styles: vec![HighlightStyle {
990 underline: Some(Underline {
991 thickness: 1.0.into(),
992 ..Default::default()
993 }),
994 ..Default::default()
995 }],
996 },
997 // Lists
998 Row {
999 blocks: vec
1007 - d"
1008 .unindent(),
1009 kind: HoverBlockKind::Markdown,
1010 }],
1011 expected_marked_text: "
1012 lists:
1013 - one
1014 - a
1015 - b
1016 - two
1017 - «c»
1018 - d"
1019 .unindent(),
1020 expected_styles: vec![HighlightStyle {
1021 underline: Some(Underline {
1022 thickness: 1.0.into(),
1023 ..Default::default()
1024 }),
1025 ..Default::default()
1026 }],
1027 },
1028 // Multi-paragraph list items
1029 Row {
1030 blocks: vec![HoverBlock {
1031 text: "
1032 * one two
1033 three
1034
1035 * four five
1036 * six seven
1037 eight
1038
1039 nine
1040 * ten
1041 * six"
1042 .unindent(),
1043 kind: HoverBlockKind::Markdown,
1044 }],
1045 expected_marked_text: "
1046 - one two three
1047 - four five
1048 - six seven eight
1049
1050 nine
1051 - ten
1052 - six"
1053 .unindent(),
1054 expected_styles: vec![HighlightStyle {
1055 underline: Some(Underline {
1056 thickness: 1.0.into(),
1057 ..Default::default()
1058 }),
1059 ..Default::default()
1060 }],
1061 },
1062 ];
1063
1064 for Row {
1065 blocks,
1066 expected_marked_text,
1067 expected_styles,
1068 } in &rows[0..]
1069 {
1070 let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style);
1071
1072 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
1073 let expected_highlights = ranges
1074 .into_iter()
1075 .zip(expected_styles.iter().cloned())
1076 .collect::<Vec<_>>();
1077 assert_eq!(
1078 rendered.text, expected_text,
1079 "wrong text for input {blocks:?}"
1080 );
1081 assert_eq!(
1082 rendered.highlights, expected_highlights,
1083 "wrong highlights for input {blocks:?}"
1084 );
1085 }
1086
1087 editor
1088 });
1089 }
1090
1091 #[gpui::test]
1092 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1093 init_test(cx, |settings| {
1094 settings.defaults.inlay_hints = Some(InlayHintSettings {
1095 enabled: true,
1096 show_type_hints: true,
1097 show_parameter_hints: true,
1098 show_other_hints: true,
1099 })
1100 });
1101
1102 let mut cx = EditorLspTestContext::new_rust(
1103 lsp::ServerCapabilities {
1104 inlay_hint_provider: Some(lsp::OneOf::Right(
1105 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1106 resolve_provider: Some(true),
1107 ..Default::default()
1108 }),
1109 )),
1110 ..Default::default()
1111 },
1112 cx,
1113 )
1114 .await;
1115
1116 cx.set_state(indoc! {"
1117 struct TestStruct;
1118
1119 // ==================
1120
1121 struct TestNewType<T>(T);
1122
1123 fn main() {
1124 let variableˇ = TestNewType(TestStruct);
1125 }
1126 "});
1127
1128 let hint_start_offset = cx.ranges(indoc! {"
1129 struct TestStruct;
1130
1131 // ==================
1132
1133 struct TestNewType<T>(T);
1134
1135 fn main() {
1136 let variableˇ = TestNewType(TestStruct);
1137 }
1138 "})[0]
1139 .start;
1140 let hint_position = cx.to_lsp(hint_start_offset);
1141 let new_type_target_range = cx.lsp_range(indoc! {"
1142 struct TestStruct;
1143
1144 // ==================
1145
1146 struct «TestNewType»<T>(T);
1147
1148 fn main() {
1149 let variable = TestNewType(TestStruct);
1150 }
1151 "});
1152 let struct_target_range = cx.lsp_range(indoc! {"
1153 struct «TestStruct»;
1154
1155 // ==================
1156
1157 struct TestNewType<T>(T);
1158
1159 fn main() {
1160 let variable = TestNewType(TestStruct);
1161 }
1162 "});
1163
1164 let uri = cx.buffer_lsp_url.clone();
1165 let new_type_label = "TestNewType";
1166 let struct_label = "TestStruct";
1167 let entire_hint_label = ": TestNewType<TestStruct>";
1168 let closure_uri = uri.clone();
1169 cx.lsp
1170 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1171 let task_uri = closure_uri.clone();
1172 async move {
1173 assert_eq!(params.text_document.uri, task_uri);
1174 Ok(Some(vec![lsp::InlayHint {
1175 position: hint_position,
1176 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1177 value: entire_hint_label.to_string(),
1178 ..Default::default()
1179 }]),
1180 kind: Some(lsp::InlayHintKind::TYPE),
1181 text_edits: None,
1182 tooltip: None,
1183 padding_left: Some(false),
1184 padding_right: Some(false),
1185 data: None,
1186 }]))
1187 }
1188 })
1189 .next()
1190 .await;
1191 cx.foreground().run_until_parked();
1192 cx.update_editor(|editor, cx| {
1193 let expected_layers = vec![entire_hint_label.to_string()];
1194 assert_eq!(expected_layers, cached_hint_labels(editor));
1195 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1196 });
1197
1198 let inlay_range = cx
1199 .ranges(indoc! {"
1200 struct TestStruct;
1201
1202 // ==================
1203
1204 struct TestNewType<T>(T);
1205
1206 fn main() {
1207 let variable« »= TestNewType(TestStruct);
1208 }
1209 "})
1210 .get(0)
1211 .cloned()
1212 .unwrap();
1213 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1214 let snapshot = editor.snapshot(cx);
1215 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1216 let next_valid = inlay_range.end.to_display_point(&snapshot);
1217 assert_eq!(previous_valid.row(), next_valid.row());
1218 assert!(previous_valid.column() < next_valid.column());
1219 let exact_unclipped = DisplayPoint::new(
1220 previous_valid.row(),
1221 previous_valid.column()
1222 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1223 as u32,
1224 );
1225 PointForPosition {
1226 previous_valid,
1227 next_valid,
1228 exact_unclipped,
1229 column_overshoot_after_line_end: 0,
1230 }
1231 });
1232 cx.update_editor(|editor, cx| {
1233 update_inlay_link_and_hover_points(
1234 &editor.snapshot(cx),
1235 new_type_hint_part_hover_position,
1236 editor,
1237 true,
1238 false,
1239 cx,
1240 );
1241 });
1242
1243 let resolve_closure_uri = uri.clone();
1244 cx.lsp
1245 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1246 move |mut hint_to_resolve, _| {
1247 let mut resolved_hint_positions = BTreeSet::new();
1248 let task_uri = resolve_closure_uri.clone();
1249 async move {
1250 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1251 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1252
1253 // `: TestNewType<TestStruct>`
1254 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1255 lsp::InlayHintLabelPart {
1256 value: ": ".to_string(),
1257 ..Default::default()
1258 },
1259 lsp::InlayHintLabelPart {
1260 value: new_type_label.to_string(),
1261 location: Some(lsp::Location {
1262 uri: task_uri.clone(),
1263 range: new_type_target_range,
1264 }),
1265 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1266 "A tooltip for `{new_type_label}`"
1267 ))),
1268 ..Default::default()
1269 },
1270 lsp::InlayHintLabelPart {
1271 value: "<".to_string(),
1272 ..Default::default()
1273 },
1274 lsp::InlayHintLabelPart {
1275 value: struct_label.to_string(),
1276 location: Some(lsp::Location {
1277 uri: task_uri,
1278 range: struct_target_range,
1279 }),
1280 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1281 lsp::MarkupContent {
1282 kind: lsp::MarkupKind::Markdown,
1283 value: format!("A tooltip for `{struct_label}`"),
1284 },
1285 )),
1286 ..Default::default()
1287 },
1288 lsp::InlayHintLabelPart {
1289 value: ">".to_string(),
1290 ..Default::default()
1291 },
1292 ]);
1293
1294 Ok(hint_to_resolve)
1295 }
1296 },
1297 )
1298 .next()
1299 .await;
1300 cx.foreground().run_until_parked();
1301
1302 cx.update_editor(|editor, cx| {
1303 update_inlay_link_and_hover_points(
1304 &editor.snapshot(cx),
1305 new_type_hint_part_hover_position,
1306 editor,
1307 true,
1308 false,
1309 cx,
1310 );
1311 });
1312 cx.foreground()
1313 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1314 cx.foreground().run_until_parked();
1315 cx.update_editor(|editor, cx| {
1316 let snapshot = editor.snapshot(cx);
1317 let hover_state = &editor.hover_state;
1318 assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1319 let popover = hover_state.info_popover.as_ref().unwrap();
1320 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1321 let entire_inlay_start = snapshot.display_point_to_inlay_offset(
1322 inlay_range.start.to_display_point(&snapshot),
1323 Bias::Left,
1324 );
1325
1326 let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
1327 assert_eq!(
1328 popover.symbol_range,
1329 DocumentRange::Inlay(InlayRange {
1330 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1331 highlight_start: expected_new_type_label_start,
1332 highlight_end: InlayOffset(
1333 expected_new_type_label_start.0 + new_type_label.len()
1334 ),
1335 }),
1336 "Popover range should match the new type label part"
1337 );
1338 assert_eq!(
1339 popover
1340 .rendered_content
1341 .as_ref()
1342 .expect("should have label text for new type hint")
1343 .text,
1344 format!("A tooltip for `{new_type_label}`"),
1345 "Rendered text should not anyhow alter backticks"
1346 );
1347 });
1348
1349 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1350 let snapshot = editor.snapshot(cx);
1351 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1352 let next_valid = inlay_range.end.to_display_point(&snapshot);
1353 assert_eq!(previous_valid.row(), next_valid.row());
1354 assert!(previous_valid.column() < next_valid.column());
1355 let exact_unclipped = DisplayPoint::new(
1356 previous_valid.row(),
1357 previous_valid.column()
1358 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1359 as u32,
1360 );
1361 PointForPosition {
1362 previous_valid,
1363 next_valid,
1364 exact_unclipped,
1365 column_overshoot_after_line_end: 0,
1366 }
1367 });
1368 cx.update_editor(|editor, cx| {
1369 update_inlay_link_and_hover_points(
1370 &editor.snapshot(cx),
1371 struct_hint_part_hover_position,
1372 editor,
1373 true,
1374 false,
1375 cx,
1376 );
1377 });
1378 cx.foreground()
1379 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1380 cx.foreground().run_until_parked();
1381 cx.update_editor(|editor, cx| {
1382 let snapshot = editor.snapshot(cx);
1383 let hover_state = &editor.hover_state;
1384 assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1385 let popover = hover_state.info_popover.as_ref().unwrap();
1386 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1387 let entire_inlay_start = snapshot.display_point_to_inlay_offset(
1388 inlay_range.start.to_display_point(&snapshot),
1389 Bias::Left,
1390 );
1391 let expected_struct_label_start =
1392 InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
1393 assert_eq!(
1394 popover.symbol_range,
1395 DocumentRange::Inlay(InlayRange {
1396 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1397 highlight_start: expected_struct_label_start,
1398 highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
1399 }),
1400 "Popover range should match the struct label part"
1401 );
1402 assert_eq!(
1403 popover
1404 .rendered_content
1405 .as_ref()
1406 .expect("should have label text for struct hint")
1407 .text,
1408 format!("A tooltip for {struct_label}"),
1409 "Rendered markdown element should remove backticks from text"
1410 );
1411 });
1412 }
1413}