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