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