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