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