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