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