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