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