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 snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
293 let hover_popover = match hover_result {
294 Some(hover_result) if !hover_result.is_empty() => {
295 // Create symbol range of anchors for highlighting and filtering of future requests.
296 let range = if let Some(range) = hover_result.range {
297 let start = snapshot
298 .buffer_snapshot
299 .anchor_in_excerpt(excerpt_id.clone(), range.start);
300 let end = snapshot
301 .buffer_snapshot
302 .anchor_in_excerpt(excerpt_id.clone(), range.end);
303
304 start..end
305 } else {
306 anchor..anchor
307 };
308
309 let language_registry =
310 project.update(&mut cx, |p, _| p.languages().clone())?;
311 let blocks = hover_result.contents;
312 let language = hover_result.language;
313 let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
314
315 Some(InfoPopover {
316 project: project.clone(),
317 symbol_range: RangeInEditor::Text(range),
318 blocks,
319 parsed_content,
320 })
321 }
322
323 _ => None,
324 };
325
326 this.update(&mut cx, |this, cx| {
327 if let Some(symbol_range) = hover_popover
328 .as_ref()
329 .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
330 {
331 // Highlight the selected symbol using a background highlight
332 this.highlight_background::<HoverState>(
333 vec![symbol_range],
334 |theme| theme.element_hover, // todo! update theme
335 cx,
336 );
337 } else {
338 this.clear_background_highlights::<HoverState>(cx);
339 }
340
341 this.hover_state.info_popover = hover_popover;
342 cx.notify();
343 cx.refresh();
344 })?;
345
346 Ok::<_, anyhow::Error>(())
347 }
348 .log_err()
349 });
350
351 editor.hover_state.info_task = Some(task);
352}
353
354async fn parse_blocks(
355 blocks: &[HoverBlock],
356 language_registry: &Arc<LanguageRegistry>,
357 language: Option<Arc<Language>>,
358) -> markdown::ParsedMarkdown {
359 let mut text = String::new();
360 let mut highlights = Vec::new();
361 let mut region_ranges = Vec::new();
362 let mut regions = Vec::new();
363
364 for block in blocks {
365 match &block.kind {
366 HoverBlockKind::PlainText => {
367 markdown::new_paragraph(&mut text, &mut Vec::new());
368 text.push_str(&block.text);
369 }
370
371 HoverBlockKind::Markdown => {
372 markdown::parse_markdown_block(
373 &block.text,
374 language_registry,
375 language.clone(),
376 &mut text,
377 &mut highlights,
378 &mut region_ranges,
379 &mut regions,
380 )
381 .await
382 }
383
384 HoverBlockKind::Code { language } => {
385 if let Some(language) = language_registry
386 .language_for_name(language)
387 .now_or_never()
388 .and_then(Result::ok)
389 {
390 markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
391 } else {
392 text.push_str(&block.text);
393 }
394 }
395 }
396 }
397
398 let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
399 if leading_space > 0 {
400 highlights = highlights
401 .into_iter()
402 .map(|(range, style)| {
403 (
404 range.start.saturating_sub(leading_space)
405 ..range.end.saturating_sub(leading_space),
406 style,
407 )
408 })
409 .collect();
410 region_ranges = region_ranges
411 .into_iter()
412 .map(|range| {
413 range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
414 })
415 .collect();
416 }
417
418 ParsedMarkdown {
419 text: text.trim().to_string(),
420 highlights,
421 region_ranges,
422 regions,
423 }
424}
425
426#[derive(Default)]
427pub struct HoverState {
428 pub info_popover: Option<InfoPopover>,
429 pub diagnostic_popover: Option<DiagnosticPopover>,
430 pub triggered_from: Option<Anchor>,
431 pub info_task: Option<Task<Option<()>>>,
432}
433
434impl HoverState {
435 pub fn visible(&self) -> bool {
436 self.info_popover.is_some() || self.diagnostic_popover.is_some()
437 }
438
439 pub fn render(
440 &mut self,
441 snapshot: &EditorSnapshot,
442 style: &EditorStyle,
443 visible_rows: Range<u32>,
444 max_size: Size<Pixels>,
445 workspace: Option<WeakView<Workspace>>,
446 cx: &mut ViewContext<Editor>,
447 ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
448 // If there is a diagnostic, position the popovers based on that.
449 // Otherwise use the start of the hover range
450 let anchor = self
451 .diagnostic_popover
452 .as_ref()
453 .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
454 .or_else(|| {
455 self.info_popover
456 .as_ref()
457 .map(|info_popover| match &info_popover.symbol_range {
458 RangeInEditor::Text(range) => &range.start,
459 RangeInEditor::Inlay(range) => &range.inlay_position,
460 })
461 })?;
462 let point = anchor.to_display_point(&snapshot.display_snapshot);
463
464 // Don't render if the relevant point isn't on screen
465 if !self.visible() || !visible_rows.contains(&point.row()) {
466 return None;
467 }
468
469 let mut elements = Vec::new();
470
471 if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
472 elements.push(diagnostic_popover.render(style, max_size, cx));
473 }
474 if let Some(info_popover) = self.info_popover.as_mut() {
475 elements.push(info_popover.render(style, max_size, workspace, cx));
476 }
477
478 Some((point, elements))
479 }
480}
481
482#[derive(Debug, Clone)]
483pub struct InfoPopover {
484 pub project: Model<Project>,
485 symbol_range: RangeInEditor,
486 pub blocks: Vec<HoverBlock>,
487 parsed_content: ParsedMarkdown,
488}
489
490impl InfoPopover {
491 pub fn render(
492 &mut self,
493 style: &EditorStyle,
494 max_size: Size<Pixels>,
495 workspace: Option<WeakView<Workspace>>,
496 cx: &mut ViewContext<Editor>,
497 ) -> AnyElement {
498 div()
499 .id("info_popover")
500 .elevation_2(cx)
501 .p_2()
502 .overflow_y_scroll()
503 .max_w(max_size.width)
504 .max_h(max_size.height)
505 // Prevent a mouse move on the popover from being propagated to the editor,
506 // because that would dismiss the popover.
507 .on_mouse_move(|_, cx| cx.stop_propagation())
508 .child(crate::render_parsed_markdown(
509 "content",
510 &self.parsed_content,
511 style,
512 workspace,
513 cx,
514 ))
515 .into_any_element()
516 }
517}
518
519#[derive(Debug, Clone)]
520pub struct DiagnosticPopover {
521 local_diagnostic: DiagnosticEntry<Anchor>,
522 primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
523}
524
525impl DiagnosticPopover {
526 pub fn render(
527 &self,
528 style: &EditorStyle,
529 max_size: Size<Pixels>,
530 cx: &mut ViewContext<Editor>,
531 ) -> AnyElement {
532 let text = match &self.local_diagnostic.diagnostic.source {
533 Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
534 None => self.local_diagnostic.diagnostic.message.clone(),
535 };
536
537 let status_colors = cx.theme().status();
538
539 struct DiagnosticColors {
540 pub background: Hsla,
541 pub border: Hsla,
542 }
543
544 let diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
545 DiagnosticSeverity::ERROR => DiagnosticColors {
546 background: status_colors.error_background,
547 border: status_colors.error_border,
548 },
549 DiagnosticSeverity::WARNING => DiagnosticColors {
550 background: status_colors.warning_background,
551 border: status_colors.warning_border,
552 },
553 DiagnosticSeverity::INFORMATION => DiagnosticColors {
554 background: status_colors.info_background,
555 border: status_colors.info_border,
556 },
557 DiagnosticSeverity::HINT => DiagnosticColors {
558 background: status_colors.hint_background,
559 border: status_colors.hint_border,
560 },
561 _ => DiagnosticColors {
562 background: status_colors.ignored_background,
563 border: status_colors.ignored_border,
564 },
565 };
566
567 div()
568 .id("diagnostic")
569 .elevation_2(cx)
570 .overflow_y_scroll()
571 .px_2()
572 .py_1()
573 .bg(diagnostic_colors.background)
574 .text_color(style.text.color)
575 .border_1()
576 .border_color(diagnostic_colors.border)
577 .rounded_md()
578 .max_w(max_size.width)
579 .max_h(max_size.height)
580 .cursor(CursorStyle::PointingHand)
581 .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
582 // Prevent a mouse move on the popover from being propagated to the editor,
583 // because that would dismiss the popover.
584 .on_mouse_move(|_, cx| cx.stop_propagation())
585 // Prevent a mouse down on the popover from being propagated to the editor,
586 // because that would move the cursor.
587 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
588 .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
589 .child(SharedString::from(text))
590 .into_any_element()
591 }
592
593 pub fn activation_info(&self) -> (usize, Anchor) {
594 let entry = self
595 .primary_diagnostic
596 .as_ref()
597 .unwrap_or(&self.local_diagnostic);
598
599 (entry.diagnostic.group_id, entry.range.start.clone())
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::{
607 editor_tests::init_test,
608 element::PointForPosition,
609 hover_links::update_inlay_link_and_hover_points,
610 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
611 test::editor_lsp_test_context::EditorLspTestContext,
612 InlayId,
613 };
614 use collections::BTreeSet;
615 use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
616 use indoc::indoc;
617 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
618 use lsp::LanguageServerId;
619 use project::{HoverBlock, HoverBlockKind};
620 use smol::stream::StreamExt;
621 use unindent::Unindent;
622 use util::test::marked_text_ranges;
623
624 #[gpui::test]
625 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
626 init_test(cx, |_| {});
627
628 let mut cx = EditorLspTestContext::new_rust(
629 lsp::ServerCapabilities {
630 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
631 ..Default::default()
632 },
633 cx,
634 )
635 .await;
636
637 // Basic hover delays and then pops without moving the mouse
638 cx.set_state(indoc! {"
639 fn ˇtest() { println!(); }
640 "});
641 let hover_point = cx.display_point(indoc! {"
642 fn test() { printˇln!(); }
643 "});
644
645 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
646 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
647
648 // After delay, hover should be visible.
649 let symbol_range = cx.lsp_range(indoc! {"
650 fn test() { «println!»(); }
651 "});
652 let mut requests =
653 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
654 Ok(Some(lsp::Hover {
655 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
656 kind: lsp::MarkupKind::Markdown,
657 value: "some basic docs".to_string(),
658 }),
659 range: Some(symbol_range),
660 }))
661 });
662 cx.background_executor
663 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
664 requests.next().await;
665
666 cx.editor(|editor, _| {
667 assert!(editor.hover_state.visible());
668 assert_eq!(
669 editor.hover_state.info_popover.clone().unwrap().blocks,
670 vec![HoverBlock {
671 text: "some basic docs".to_string(),
672 kind: HoverBlockKind::Markdown,
673 },]
674 )
675 });
676
677 // Mouse moved with no hover response dismisses
678 let hover_point = cx.display_point(indoc! {"
679 fn teˇst() { println!(); }
680 "});
681 let mut request = cx
682 .lsp
683 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
684 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
685 cx.background_executor
686 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
687 request.next().await;
688 cx.editor(|editor, _| {
689 assert!(!editor.hover_state.visible());
690 });
691 }
692
693 #[gpui::test]
694 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
695 init_test(cx, |_| {});
696
697 let mut cx = EditorLspTestContext::new_rust(
698 lsp::ServerCapabilities {
699 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
700 ..Default::default()
701 },
702 cx,
703 )
704 .await;
705
706 // Hover with keyboard has no delay
707 cx.set_state(indoc! {"
708 fˇn test() { println!(); }
709 "});
710 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
711 let symbol_range = cx.lsp_range(indoc! {"
712 «fn» test() { println!(); }
713 "});
714 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
715 Ok(Some(lsp::Hover {
716 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
717 kind: lsp::MarkupKind::Markdown,
718 value: "some other basic docs".to_string(),
719 }),
720 range: Some(symbol_range),
721 }))
722 })
723 .next()
724 .await;
725
726 cx.condition(|editor, _| editor.hover_state.visible()).await;
727 cx.editor(|editor, _| {
728 assert_eq!(
729 editor.hover_state.info_popover.clone().unwrap().blocks,
730 vec![HoverBlock {
731 text: "some other basic docs".to_string(),
732 kind: HoverBlockKind::Markdown,
733 }]
734 )
735 });
736 }
737
738 #[gpui::test]
739 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
740 init_test(cx, |_| {});
741
742 let mut cx = EditorLspTestContext::new_rust(
743 lsp::ServerCapabilities {
744 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
745 ..Default::default()
746 },
747 cx,
748 )
749 .await;
750
751 // Hover with keyboard has no delay
752 cx.set_state(indoc! {"
753 fˇn test() { println!(); }
754 "});
755 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
756 let symbol_range = cx.lsp_range(indoc! {"
757 «fn» test() { println!(); }
758 "});
759 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
760 Ok(Some(lsp::Hover {
761 contents: lsp::HoverContents::Array(vec![
762 lsp::MarkedString::String("regular text for hover to show".to_string()),
763 lsp::MarkedString::String("".to_string()),
764 lsp::MarkedString::LanguageString(lsp::LanguageString {
765 language: "Rust".to_string(),
766 value: "".to_string(),
767 }),
768 ]),
769 range: Some(symbol_range),
770 }))
771 })
772 .next()
773 .await;
774
775 cx.condition(|editor, _| editor.hover_state.visible()).await;
776 cx.editor(|editor, _| {
777 assert_eq!(
778 editor.hover_state.info_popover.clone().unwrap().blocks,
779 vec![HoverBlock {
780 text: "regular text for hover to show".to_string(),
781 kind: HoverBlockKind::Markdown,
782 }],
783 "No empty string hovers should be shown"
784 );
785 });
786 }
787
788 #[gpui::test]
789 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
790 init_test(cx, |_| {});
791
792 let mut cx = EditorLspTestContext::new_rust(
793 lsp::ServerCapabilities {
794 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
795 ..Default::default()
796 },
797 cx,
798 )
799 .await;
800
801 // Hover with keyboard has no delay
802 cx.set_state(indoc! {"
803 fˇn test() { println!(); }
804 "});
805 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
806 let symbol_range = cx.lsp_range(indoc! {"
807 «fn» test() { println!(); }
808 "});
809
810 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
811 let markdown_string = format!("\n```rust\n{code_str}```");
812
813 let closure_markdown_string = markdown_string.clone();
814 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
815 let future_markdown_string = closure_markdown_string.clone();
816 async move {
817 Ok(Some(lsp::Hover {
818 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
819 kind: lsp::MarkupKind::Markdown,
820 value: future_markdown_string,
821 }),
822 range: Some(symbol_range),
823 }))
824 }
825 })
826 .next()
827 .await;
828
829 cx.condition(|editor, _| editor.hover_state.visible()).await;
830 cx.editor(|editor, _| {
831 let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
832 assert_eq!(
833 blocks,
834 vec![HoverBlock {
835 text: markdown_string,
836 kind: HoverBlockKind::Markdown,
837 }],
838 );
839
840 let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
841 assert_eq!(
842 rendered.text,
843 code_str.trim(),
844 "Should not have extra line breaks at end of rendered hover"
845 );
846 });
847 }
848
849 #[gpui::test]
850 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
851 init_test(cx, |_| {});
852
853 let mut cx = EditorLspTestContext::new_rust(
854 lsp::ServerCapabilities {
855 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
856 ..Default::default()
857 },
858 cx,
859 )
860 .await;
861
862 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
863 // info popover once request completes
864 cx.set_state(indoc! {"
865 fn teˇst() { println!(); }
866 "});
867
868 // Send diagnostic to client
869 let range = cx.text_anchor_range(indoc! {"
870 fn «test»() { println!(); }
871 "});
872 cx.update_buffer(|buffer, cx| {
873 let snapshot = buffer.text_snapshot();
874 let set = DiagnosticSet::from_sorted_entries(
875 vec![DiagnosticEntry {
876 range,
877 diagnostic: Diagnostic {
878 message: "A test diagnostic message.".to_string(),
879 ..Default::default()
880 },
881 }],
882 &snapshot,
883 );
884 buffer.update_diagnostics(LanguageServerId(0), set, cx);
885 });
886
887 // Hover pops diagnostic immediately
888 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
889 cx.background_executor.run_until_parked();
890
891 cx.editor(|Editor { hover_state, .. }, _| {
892 assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
893 });
894
895 // Info Popover shows after request responded to
896 let range = cx.lsp_range(indoc! {"
897 fn «test»() { println!(); }
898 "});
899 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
900 Ok(Some(lsp::Hover {
901 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
902 kind: lsp::MarkupKind::Markdown,
903 value: "some new docs".to_string(),
904 }),
905 range: Some(range),
906 }))
907 });
908 cx.background_executor
909 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
910
911 cx.background_executor.run_until_parked();
912 cx.editor(|Editor { hover_state, .. }, _| {
913 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
914 });
915 }
916
917 #[gpui::test]
918 fn test_render_blocks(cx: &mut gpui::TestAppContext) {
919 init_test(cx, |_| {});
920
921 let editor = cx.add_window(|cx| Editor::single_line(cx));
922 editor
923 .update(cx, |editor, _cx| {
924 let style = editor.style.clone().unwrap();
925
926 struct Row {
927 blocks: Vec<HoverBlock>,
928 expected_marked_text: String,
929 expected_styles: Vec<HighlightStyle>,
930 }
931
932 let rows = &[
933 // Strong emphasis
934 Row {
935 blocks: vec![HoverBlock {
936 text: "one **two** three".to_string(),
937 kind: HoverBlockKind::Markdown,
938 }],
939 expected_marked_text: "one «two» three".to_string(),
940 expected_styles: vec![HighlightStyle {
941 font_weight: Some(FontWeight::BOLD),
942 ..Default::default()
943 }],
944 },
945 // Links
946 Row {
947 blocks: vec three".to_string(),
949 kind: HoverBlockKind::Markdown,
950 }],
951 expected_marked_text: "one «two» three".to_string(),
952 expected_styles: vec![HighlightStyle {
953 underline: Some(UnderlineStyle {
954 thickness: 1.0.into(),
955 ..Default::default()
956 }),
957 ..Default::default()
958 }],
959 },
960 // Lists
961 Row {
962 blocks: vec
970 - d"
971 .unindent(),
972 kind: HoverBlockKind::Markdown,
973 }],
974 expected_marked_text: "
975 lists:
976 - one
977 - a
978 - b
979 - two
980 - «c»
981 - d"
982 .unindent(),
983 expected_styles: vec![HighlightStyle {
984 underline: Some(UnderlineStyle {
985 thickness: 1.0.into(),
986 ..Default::default()
987 }),
988 ..Default::default()
989 }],
990 },
991 // Multi-paragraph list items
992 Row {
993 blocks: vec![HoverBlock {
994 text: "
995 * one two
996 three
997
998 * four five
999 * six seven
1000 eight
1001
1002 nine
1003 * ten
1004 * six"
1005 .unindent(),
1006 kind: HoverBlockKind::Markdown,
1007 }],
1008 expected_marked_text: "
1009 - one two three
1010 - four five
1011 - six seven eight
1012
1013 nine
1014 - ten
1015 - six"
1016 .unindent(),
1017 expected_styles: vec![HighlightStyle {
1018 underline: Some(UnderlineStyle {
1019 thickness: 1.0.into(),
1020 ..Default::default()
1021 }),
1022 ..Default::default()
1023 }],
1024 },
1025 ];
1026
1027 for Row {
1028 blocks,
1029 expected_marked_text,
1030 expected_styles,
1031 } in &rows[0..]
1032 {
1033 let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
1034
1035 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
1036 let expected_highlights = ranges
1037 .into_iter()
1038 .zip(expected_styles.iter().cloned())
1039 .collect::<Vec<_>>();
1040 assert_eq!(
1041 rendered.text, expected_text,
1042 "wrong text for input {blocks:?}"
1043 );
1044
1045 let rendered_highlights: Vec<_> = rendered
1046 .highlights
1047 .iter()
1048 .filter_map(|(range, highlight)| {
1049 let highlight = highlight.to_highlight_style(&style.syntax)?;
1050 Some((range.clone(), highlight))
1051 })
1052 .collect();
1053
1054 assert_eq!(
1055 rendered_highlights, expected_highlights,
1056 "wrong highlights for input {blocks:?}"
1057 );
1058 }
1059 })
1060 .unwrap();
1061 }
1062
1063 #[gpui::test]
1064 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1065 init_test(cx, |settings| {
1066 settings.defaults.inlay_hints = Some(InlayHintSettings {
1067 enabled: true,
1068 show_type_hints: true,
1069 show_parameter_hints: true,
1070 show_other_hints: true,
1071 })
1072 });
1073
1074 let mut cx = EditorLspTestContext::new_rust(
1075 lsp::ServerCapabilities {
1076 inlay_hint_provider: Some(lsp::OneOf::Right(
1077 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1078 resolve_provider: Some(true),
1079 ..Default::default()
1080 }),
1081 )),
1082 ..Default::default()
1083 },
1084 cx,
1085 )
1086 .await;
1087
1088 cx.set_state(indoc! {"
1089 struct TestStruct;
1090
1091 // ==================
1092
1093 struct TestNewType<T>(T);
1094
1095 fn main() {
1096 let variableˇ = TestNewType(TestStruct);
1097 }
1098 "});
1099
1100 let hint_start_offset = cx.ranges(indoc! {"
1101 struct TestStruct;
1102
1103 // ==================
1104
1105 struct TestNewType<T>(T);
1106
1107 fn main() {
1108 let variableˇ = TestNewType(TestStruct);
1109 }
1110 "})[0]
1111 .start;
1112 let hint_position = cx.to_lsp(hint_start_offset);
1113 let new_type_target_range = cx.lsp_range(indoc! {"
1114 struct TestStruct;
1115
1116 // ==================
1117
1118 struct «TestNewType»<T>(T);
1119
1120 fn main() {
1121 let variable = TestNewType(TestStruct);
1122 }
1123 "});
1124 let struct_target_range = cx.lsp_range(indoc! {"
1125 struct «TestStruct»;
1126
1127 // ==================
1128
1129 struct TestNewType<T>(T);
1130
1131 fn main() {
1132 let variable = TestNewType(TestStruct);
1133 }
1134 "});
1135
1136 let uri = cx.buffer_lsp_url.clone();
1137 let new_type_label = "TestNewType";
1138 let struct_label = "TestStruct";
1139 let entire_hint_label = ": TestNewType<TestStruct>";
1140 let closure_uri = uri.clone();
1141 cx.lsp
1142 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1143 let task_uri = closure_uri.clone();
1144 async move {
1145 assert_eq!(params.text_document.uri, task_uri);
1146 Ok(Some(vec![lsp::InlayHint {
1147 position: hint_position,
1148 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1149 value: entire_hint_label.to_string(),
1150 ..Default::default()
1151 }]),
1152 kind: Some(lsp::InlayHintKind::TYPE),
1153 text_edits: None,
1154 tooltip: None,
1155 padding_left: Some(false),
1156 padding_right: Some(false),
1157 data: None,
1158 }]))
1159 }
1160 })
1161 .next()
1162 .await;
1163 cx.background_executor.run_until_parked();
1164 cx.update_editor(|editor, cx| {
1165 let expected_layers = vec![entire_hint_label.to_string()];
1166 assert_eq!(expected_layers, cached_hint_labels(editor));
1167 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1168 });
1169
1170 let inlay_range = cx
1171 .ranges(indoc! {"
1172 struct TestStruct;
1173
1174 // ==================
1175
1176 struct TestNewType<T>(T);
1177
1178 fn main() {
1179 let variable« »= TestNewType(TestStruct);
1180 }
1181 "})
1182 .get(0)
1183 .cloned()
1184 .unwrap();
1185 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1186 let snapshot = editor.snapshot(cx);
1187 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1188 let next_valid = inlay_range.end.to_display_point(&snapshot);
1189 assert_eq!(previous_valid.row(), next_valid.row());
1190 assert!(previous_valid.column() < next_valid.column());
1191 let exact_unclipped = DisplayPoint::new(
1192 previous_valid.row(),
1193 previous_valid.column()
1194 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1195 as u32,
1196 );
1197 PointForPosition {
1198 previous_valid,
1199 next_valid,
1200 exact_unclipped,
1201 column_overshoot_after_line_end: 0,
1202 }
1203 });
1204 cx.update_editor(|editor, cx| {
1205 update_inlay_link_and_hover_points(
1206 &editor.snapshot(cx),
1207 new_type_hint_part_hover_position,
1208 editor,
1209 true,
1210 false,
1211 cx,
1212 );
1213 });
1214
1215 let resolve_closure_uri = uri.clone();
1216 cx.lsp
1217 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1218 move |mut hint_to_resolve, _| {
1219 let mut resolved_hint_positions = BTreeSet::new();
1220 let task_uri = resolve_closure_uri.clone();
1221 async move {
1222 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1223 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1224
1225 // `: TestNewType<TestStruct>`
1226 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1227 lsp::InlayHintLabelPart {
1228 value: ": ".to_string(),
1229 ..Default::default()
1230 },
1231 lsp::InlayHintLabelPart {
1232 value: new_type_label.to_string(),
1233 location: Some(lsp::Location {
1234 uri: task_uri.clone(),
1235 range: new_type_target_range,
1236 }),
1237 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1238 "A tooltip for `{new_type_label}`"
1239 ))),
1240 ..Default::default()
1241 },
1242 lsp::InlayHintLabelPart {
1243 value: "<".to_string(),
1244 ..Default::default()
1245 },
1246 lsp::InlayHintLabelPart {
1247 value: struct_label.to_string(),
1248 location: Some(lsp::Location {
1249 uri: task_uri,
1250 range: struct_target_range,
1251 }),
1252 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1253 lsp::MarkupContent {
1254 kind: lsp::MarkupKind::Markdown,
1255 value: format!("A tooltip for `{struct_label}`"),
1256 },
1257 )),
1258 ..Default::default()
1259 },
1260 lsp::InlayHintLabelPart {
1261 value: ">".to_string(),
1262 ..Default::default()
1263 },
1264 ]);
1265
1266 Ok(hint_to_resolve)
1267 }
1268 },
1269 )
1270 .next()
1271 .await;
1272 cx.background_executor.run_until_parked();
1273
1274 cx.update_editor(|editor, cx| {
1275 update_inlay_link_and_hover_points(
1276 &editor.snapshot(cx),
1277 new_type_hint_part_hover_position,
1278 editor,
1279 true,
1280 false,
1281 cx,
1282 );
1283 });
1284 cx.background_executor
1285 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1286 cx.background_executor.run_until_parked();
1287 cx.update_editor(|editor, cx| {
1288 let hover_state = &editor.hover_state;
1289 assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1290 let popover = hover_state.info_popover.as_ref().unwrap();
1291 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1292 assert_eq!(
1293 popover.symbol_range,
1294 RangeInEditor::Inlay(InlayHighlight {
1295 inlay: InlayId::Hint(0),
1296 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1297 range: ": ".len()..": ".len() + new_type_label.len(),
1298 }),
1299 "Popover range should match the new type label part"
1300 );
1301 assert_eq!(
1302 popover.parsed_content.text,
1303 format!("A tooltip for `{new_type_label}`"),
1304 "Rendered text should not anyhow alter backticks"
1305 );
1306 });
1307
1308 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1309 let snapshot = editor.snapshot(cx);
1310 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1311 let next_valid = inlay_range.end.to_display_point(&snapshot);
1312 assert_eq!(previous_valid.row(), next_valid.row());
1313 assert!(previous_valid.column() < next_valid.column());
1314 let exact_unclipped = DisplayPoint::new(
1315 previous_valid.row(),
1316 previous_valid.column()
1317 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1318 as u32,
1319 );
1320 PointForPosition {
1321 previous_valid,
1322 next_valid,
1323 exact_unclipped,
1324 column_overshoot_after_line_end: 0,
1325 }
1326 });
1327 cx.update_editor(|editor, cx| {
1328 update_inlay_link_and_hover_points(
1329 &editor.snapshot(cx),
1330 struct_hint_part_hover_position,
1331 editor,
1332 true,
1333 false,
1334 cx,
1335 );
1336 });
1337 cx.background_executor
1338 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1339 cx.background_executor.run_until_parked();
1340 cx.update_editor(|editor, cx| {
1341 let hover_state = &editor.hover_state;
1342 assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1343 let popover = hover_state.info_popover.as_ref().unwrap();
1344 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1345 assert_eq!(
1346 popover.symbol_range,
1347 RangeInEditor::Inlay(InlayHighlight {
1348 inlay: InlayId::Hint(0),
1349 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1350 range: ": ".len() + new_type_label.len() + "<".len()
1351 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1352 }),
1353 "Popover range should match the struct label part"
1354 );
1355 assert_eq!(
1356 popover.parsed_content.text,
1357 format!("A tooltip for {struct_label}"),
1358 "Rendered markdown element should remove backticks from text"
1359 );
1360 });
1361 }
1362}