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::{StyledExt, 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!(editor, [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 .elevation_2(cx)
480 .text_ui()
481 .p_2()
482 .overflow_y_scroll()
483 .max_w(max_size.width)
484 .max_h(max_size.height)
485 // Prevent a mouse move on the popover from being propagated to the editor,
486 // because that would dismiss the popover.
487 .on_mouse_move(|_, cx| cx.stop_propagation())
488 .child(crate::render_parsed_markdown(
489 "content",
490 &self.parsed_content,
491 style,
492 workspace,
493 cx,
494 ))
495 .into_any_element()
496 }
497}
498
499#[derive(Debug, Clone)]
500pub struct DiagnosticPopover {
501 local_diagnostic: DiagnosticEntry<Anchor>,
502 primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
503}
504
505impl DiagnosticPopover {
506 pub fn render(
507 &self,
508 style: &EditorStyle,
509 max_size: Size<Pixels>,
510 cx: &mut ViewContext<Editor>,
511 ) -> AnyElement {
512 let text = match &self.local_diagnostic.diagnostic.source {
513 Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
514 None => self.local_diagnostic.diagnostic.message.clone(),
515 };
516
517 let container_bg = crate::diagnostic_style(
518 self.local_diagnostic.diagnostic.severity,
519 true,
520 &style.diagnostic_style,
521 );
522
523 div()
524 .id("diagnostic")
525 .overflow_y_scroll()
526 .bg(container_bg)
527 .max_w(max_size.width)
528 .max_h(max_size.height)
529 .cursor(CursorStyle::PointingHand)
530 .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
531 // Prevent a mouse move on the popover from being propagated to the editor,
532 // because that would dismiss the popover.
533 .on_mouse_move(|_, cx| cx.stop_propagation())
534 // Prevent a mouse down on the popover from being propagated to the editor,
535 // because that would move the cursor.
536 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
537 .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
538 .child(SharedString::from(text))
539 .into_any_element()
540 }
541
542 pub fn activation_info(&self) -> (usize, Anchor) {
543 let entry = self
544 .primary_diagnostic
545 .as_ref()
546 .unwrap_or(&self.local_diagnostic);
547
548 (entry.diagnostic.group_id, entry.range.start.clone())
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::{
556 editor_tests::init_test,
557 element::PointForPosition,
558 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
559 link_go_to_definition::update_inlay_link_and_hover_points,
560 test::editor_lsp_test_context::EditorLspTestContext,
561 InlayId,
562 };
563 use collections::BTreeSet;
564 use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
565 use indoc::indoc;
566 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
567 use lsp::LanguageServerId;
568 use project::{HoverBlock, HoverBlockKind};
569 use smol::stream::StreamExt;
570 use unindent::Unindent;
571 use util::test::marked_text_ranges;
572
573 #[gpui::test]
574 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
575 init_test(cx, |_| {});
576
577 let mut cx = EditorLspTestContext::new_rust(
578 lsp::ServerCapabilities {
579 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
580 ..Default::default()
581 },
582 cx,
583 )
584 .await;
585
586 // Basic hover delays and then pops without moving the mouse
587 cx.set_state(indoc! {"
588 fn ˇtest() { println!(); }
589 "});
590 let hover_point = cx.display_point(indoc! {"
591 fn test() { printˇln!(); }
592 "});
593
594 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
595 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
596
597 // After delay, hover should be visible.
598 let symbol_range = cx.lsp_range(indoc! {"
599 fn test() { «println!»(); }
600 "});
601 let mut requests =
602 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
603 Ok(Some(lsp::Hover {
604 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
605 kind: lsp::MarkupKind::Markdown,
606 value: "some basic docs".to_string(),
607 }),
608 range: Some(symbol_range),
609 }))
610 });
611 cx.background_executor
612 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
613 requests.next().await;
614
615 cx.editor(|editor, _| {
616 assert!(editor.hover_state.visible());
617 assert_eq!(
618 editor.hover_state.info_popover.clone().unwrap().blocks,
619 vec![HoverBlock {
620 text: "some basic docs".to_string(),
621 kind: HoverBlockKind::Markdown,
622 },]
623 )
624 });
625
626 // Mouse moved with no hover response dismisses
627 let hover_point = cx.display_point(indoc! {"
628 fn teˇst() { println!(); }
629 "});
630 let mut request = cx
631 .lsp
632 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
633 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
634 cx.background_executor
635 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
636 request.next().await;
637 cx.editor(|editor, _| {
638 assert!(!editor.hover_state.visible());
639 });
640 }
641
642 #[gpui::test]
643 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
644 init_test(cx, |_| {});
645
646 let mut cx = EditorLspTestContext::new_rust(
647 lsp::ServerCapabilities {
648 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
649 ..Default::default()
650 },
651 cx,
652 )
653 .await;
654
655 // Hover with keyboard has no delay
656 cx.set_state(indoc! {"
657 fˇn test() { println!(); }
658 "});
659 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
660 let symbol_range = cx.lsp_range(indoc! {"
661 «fn» test() { println!(); }
662 "});
663 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
664 Ok(Some(lsp::Hover {
665 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
666 kind: lsp::MarkupKind::Markdown,
667 value: "some other basic docs".to_string(),
668 }),
669 range: Some(symbol_range),
670 }))
671 })
672 .next()
673 .await;
674
675 cx.condition(|editor, _| editor.hover_state.visible()).await;
676 cx.editor(|editor, _| {
677 assert_eq!(
678 editor.hover_state.info_popover.clone().unwrap().blocks,
679 vec![HoverBlock {
680 text: "some other basic docs".to_string(),
681 kind: HoverBlockKind::Markdown,
682 }]
683 )
684 });
685 }
686
687 #[gpui::test]
688 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
689 init_test(cx, |_| {});
690
691 let mut cx = EditorLspTestContext::new_rust(
692 lsp::ServerCapabilities {
693 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
694 ..Default::default()
695 },
696 cx,
697 )
698 .await;
699
700 // Hover with keyboard has no delay
701 cx.set_state(indoc! {"
702 fˇn test() { println!(); }
703 "});
704 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
705 let symbol_range = cx.lsp_range(indoc! {"
706 «fn» test() { println!(); }
707 "});
708 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
709 Ok(Some(lsp::Hover {
710 contents: lsp::HoverContents::Array(vec![
711 lsp::MarkedString::String("regular text for hover to show".to_string()),
712 lsp::MarkedString::String("".to_string()),
713 lsp::MarkedString::LanguageString(lsp::LanguageString {
714 language: "Rust".to_string(),
715 value: "".to_string(),
716 }),
717 ]),
718 range: Some(symbol_range),
719 }))
720 })
721 .next()
722 .await;
723
724 cx.condition(|editor, _| editor.hover_state.visible()).await;
725 cx.editor(|editor, _| {
726 assert_eq!(
727 editor.hover_state.info_popover.clone().unwrap().blocks,
728 vec![HoverBlock {
729 text: "regular text for hover to show".to_string(),
730 kind: HoverBlockKind::Markdown,
731 }],
732 "No empty string hovers should be shown"
733 );
734 });
735 }
736
737 #[gpui::test]
738 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
739 init_test(cx, |_| {});
740
741 let mut cx = EditorLspTestContext::new_rust(
742 lsp::ServerCapabilities {
743 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
744 ..Default::default()
745 },
746 cx,
747 )
748 .await;
749
750 // Hover with keyboard has no delay
751 cx.set_state(indoc! {"
752 fˇn test() { println!(); }
753 "});
754 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
755 let symbol_range = cx.lsp_range(indoc! {"
756 «fn» test() { println!(); }
757 "});
758
759 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
760 let markdown_string = format!("\n```rust\n{code_str}```");
761
762 let closure_markdown_string = markdown_string.clone();
763 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
764 let future_markdown_string = closure_markdown_string.clone();
765 async move {
766 Ok(Some(lsp::Hover {
767 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
768 kind: lsp::MarkupKind::Markdown,
769 value: future_markdown_string,
770 }),
771 range: Some(symbol_range),
772 }))
773 }
774 })
775 .next()
776 .await;
777
778 cx.condition(|editor, _| editor.hover_state.visible()).await;
779 cx.editor(|editor, _| {
780 let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
781 assert_eq!(
782 blocks,
783 vec![HoverBlock {
784 text: markdown_string,
785 kind: HoverBlockKind::Markdown,
786 }],
787 );
788
789 let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
790 assert_eq!(
791 rendered.text,
792 code_str.trim(),
793 "Should not have extra line breaks at end of rendered hover"
794 );
795 });
796 }
797
798 #[gpui::test]
799 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
800 init_test(cx, |_| {});
801
802 let mut cx = EditorLspTestContext::new_rust(
803 lsp::ServerCapabilities {
804 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
805 ..Default::default()
806 },
807 cx,
808 )
809 .await;
810
811 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
812 // info popover once request completes
813 cx.set_state(indoc! {"
814 fn teˇst() { println!(); }
815 "});
816
817 // Send diagnostic to client
818 let range = cx.text_anchor_range(indoc! {"
819 fn «test»() { println!(); }
820 "});
821 cx.update_buffer(|buffer, cx| {
822 let snapshot = buffer.text_snapshot();
823 let set = DiagnosticSet::from_sorted_entries(
824 vec![DiagnosticEntry {
825 range,
826 diagnostic: Diagnostic {
827 message: "A test diagnostic message.".to_string(),
828 ..Default::default()
829 },
830 }],
831 &snapshot,
832 );
833 buffer.update_diagnostics(LanguageServerId(0), set, cx);
834 });
835
836 // Hover pops diagnostic immediately
837 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
838 cx.background_executor.run_until_parked();
839
840 cx.editor(|Editor { hover_state, .. }, _| {
841 assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
842 });
843
844 // Info Popover shows after request responded to
845 let range = cx.lsp_range(indoc! {"
846 fn «test»() { println!(); }
847 "});
848 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
849 Ok(Some(lsp::Hover {
850 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
851 kind: lsp::MarkupKind::Markdown,
852 value: "some new docs".to_string(),
853 }),
854 range: Some(range),
855 }))
856 });
857 cx.background_executor
858 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
859
860 cx.background_executor.run_until_parked();
861 cx.editor(|Editor { hover_state, .. }, _| {
862 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
863 });
864 }
865
866 #[gpui::test]
867 fn test_render_blocks(cx: &mut gpui::TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|cx| Editor::single_line(cx));
871 editor
872 .update(cx, |editor, cx| {
873 let style = editor.style.clone().unwrap();
874
875 struct Row {
876 blocks: Vec<HoverBlock>,
877 expected_marked_text: String,
878 expected_styles: Vec<HighlightStyle>,
879 }
880
881 let rows = &[
882 // Strong emphasis
883 Row {
884 blocks: vec![HoverBlock {
885 text: "one **two** three".to_string(),
886 kind: HoverBlockKind::Markdown,
887 }],
888 expected_marked_text: "one «two» three".to_string(),
889 expected_styles: vec![HighlightStyle {
890 font_weight: Some(FontWeight::BOLD),
891 ..Default::default()
892 }],
893 },
894 // Links
895 Row {
896 blocks: vec three".to_string(),
898 kind: HoverBlockKind::Markdown,
899 }],
900 expected_marked_text: "one «two» three".to_string(),
901 expected_styles: vec![HighlightStyle {
902 underline: Some(UnderlineStyle {
903 thickness: 1.0.into(),
904 ..Default::default()
905 }),
906 ..Default::default()
907 }],
908 },
909 // Lists
910 Row {
911 blocks: vec
919 - d"
920 .unindent(),
921 kind: HoverBlockKind::Markdown,
922 }],
923 expected_marked_text: "
924 lists:
925 - one
926 - a
927 - b
928 - two
929 - «c»
930 - d"
931 .unindent(),
932 expected_styles: vec![HighlightStyle {
933 underline: Some(UnderlineStyle {
934 thickness: 1.0.into(),
935 ..Default::default()
936 }),
937 ..Default::default()
938 }],
939 },
940 // Multi-paragraph list items
941 Row {
942 blocks: vec![HoverBlock {
943 text: "
944 * one two
945 three
946
947 * four five
948 * six seven
949 eight
950
951 nine
952 * ten
953 * six"
954 .unindent(),
955 kind: HoverBlockKind::Markdown,
956 }],
957 expected_marked_text: "
958 - one two three
959 - four five
960 - six seven eight
961
962 nine
963 - ten
964 - six"
965 .unindent(),
966 expected_styles: vec![HighlightStyle {
967 underline: Some(UnderlineStyle {
968 thickness: 1.0.into(),
969 ..Default::default()
970 }),
971 ..Default::default()
972 }],
973 },
974 ];
975
976 for Row {
977 blocks,
978 expected_marked_text,
979 expected_styles,
980 } in &rows[0..]
981 {
982 let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
983
984 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
985 let expected_highlights = ranges
986 .into_iter()
987 .zip(expected_styles.iter().cloned())
988 .collect::<Vec<_>>();
989 assert_eq!(
990 rendered.text, expected_text,
991 "wrong text for input {blocks:?}"
992 );
993
994 let rendered_highlights: Vec<_> = rendered
995 .highlights
996 .iter()
997 .filter_map(|(range, highlight)| {
998 let highlight = highlight.to_highlight_style(&style.syntax)?;
999 Some((range.clone(), highlight))
1000 })
1001 .collect();
1002
1003 assert_eq!(
1004 rendered_highlights, expected_highlights,
1005 "wrong highlights for input {blocks:?}"
1006 );
1007 }
1008 })
1009 .unwrap();
1010 }
1011
1012 #[gpui::test]
1013 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1014 init_test(cx, |settings| {
1015 settings.defaults.inlay_hints = Some(InlayHintSettings {
1016 enabled: true,
1017 show_type_hints: true,
1018 show_parameter_hints: true,
1019 show_other_hints: true,
1020 })
1021 });
1022
1023 let mut cx = EditorLspTestContext::new_rust(
1024 lsp::ServerCapabilities {
1025 inlay_hint_provider: Some(lsp::OneOf::Right(
1026 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1027 resolve_provider: Some(true),
1028 ..Default::default()
1029 }),
1030 )),
1031 ..Default::default()
1032 },
1033 cx,
1034 )
1035 .await;
1036
1037 cx.set_state(indoc! {"
1038 struct TestStruct;
1039
1040 // ==================
1041
1042 struct TestNewType<T>(T);
1043
1044 fn main() {
1045 let variableˇ = TestNewType(TestStruct);
1046 }
1047 "});
1048
1049 let hint_start_offset = cx.ranges(indoc! {"
1050 struct TestStruct;
1051
1052 // ==================
1053
1054 struct TestNewType<T>(T);
1055
1056 fn main() {
1057 let variableˇ = TestNewType(TestStruct);
1058 }
1059 "})[0]
1060 .start;
1061 let hint_position = cx.to_lsp(hint_start_offset);
1062 let new_type_target_range = cx.lsp_range(indoc! {"
1063 struct TestStruct;
1064
1065 // ==================
1066
1067 struct «TestNewType»<T>(T);
1068
1069 fn main() {
1070 let variable = TestNewType(TestStruct);
1071 }
1072 "});
1073 let struct_target_range = cx.lsp_range(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 uri = cx.buffer_lsp_url.clone();
1086 let new_type_label = "TestNewType";
1087 let struct_label = "TestStruct";
1088 let entire_hint_label = ": TestNewType<TestStruct>";
1089 let closure_uri = uri.clone();
1090 cx.lsp
1091 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1092 let task_uri = closure_uri.clone();
1093 async move {
1094 assert_eq!(params.text_document.uri, task_uri);
1095 Ok(Some(vec![lsp::InlayHint {
1096 position: hint_position,
1097 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1098 value: entire_hint_label.to_string(),
1099 ..Default::default()
1100 }]),
1101 kind: Some(lsp::InlayHintKind::TYPE),
1102 text_edits: None,
1103 tooltip: None,
1104 padding_left: Some(false),
1105 padding_right: Some(false),
1106 data: None,
1107 }]))
1108 }
1109 })
1110 .next()
1111 .await;
1112 cx.background_executor.run_until_parked();
1113 cx.update_editor(|editor, cx| {
1114 let expected_layers = vec![entire_hint_label.to_string()];
1115 assert_eq!(expected_layers, cached_hint_labels(editor));
1116 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1117 });
1118
1119 let inlay_range = cx
1120 .ranges(indoc! {"
1121 struct TestStruct;
1122
1123 // ==================
1124
1125 struct TestNewType<T>(T);
1126
1127 fn main() {
1128 let variable« »= TestNewType(TestStruct);
1129 }
1130 "})
1131 .get(0)
1132 .cloned()
1133 .unwrap();
1134 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1135 let snapshot = editor.snapshot(cx);
1136 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1137 let next_valid = inlay_range.end.to_display_point(&snapshot);
1138 assert_eq!(previous_valid.row(), next_valid.row());
1139 assert!(previous_valid.column() < next_valid.column());
1140 let exact_unclipped = DisplayPoint::new(
1141 previous_valid.row(),
1142 previous_valid.column()
1143 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1144 as u32,
1145 );
1146 PointForPosition {
1147 previous_valid,
1148 next_valid,
1149 exact_unclipped,
1150 column_overshoot_after_line_end: 0,
1151 }
1152 });
1153 cx.update_editor(|editor, cx| {
1154 update_inlay_link_and_hover_points(
1155 &editor.snapshot(cx),
1156 new_type_hint_part_hover_position,
1157 editor,
1158 true,
1159 false,
1160 cx,
1161 );
1162 });
1163
1164 let resolve_closure_uri = uri.clone();
1165 cx.lsp
1166 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1167 move |mut hint_to_resolve, _| {
1168 let mut resolved_hint_positions = BTreeSet::new();
1169 let task_uri = resolve_closure_uri.clone();
1170 async move {
1171 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1172 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1173
1174 // `: TestNewType<TestStruct>`
1175 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1176 lsp::InlayHintLabelPart {
1177 value: ": ".to_string(),
1178 ..Default::default()
1179 },
1180 lsp::InlayHintLabelPart {
1181 value: new_type_label.to_string(),
1182 location: Some(lsp::Location {
1183 uri: task_uri.clone(),
1184 range: new_type_target_range,
1185 }),
1186 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1187 "A tooltip for `{new_type_label}`"
1188 ))),
1189 ..Default::default()
1190 },
1191 lsp::InlayHintLabelPart {
1192 value: "<".to_string(),
1193 ..Default::default()
1194 },
1195 lsp::InlayHintLabelPart {
1196 value: struct_label.to_string(),
1197 location: Some(lsp::Location {
1198 uri: task_uri,
1199 range: struct_target_range,
1200 }),
1201 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1202 lsp::MarkupContent {
1203 kind: lsp::MarkupKind::Markdown,
1204 value: format!("A tooltip for `{struct_label}`"),
1205 },
1206 )),
1207 ..Default::default()
1208 },
1209 lsp::InlayHintLabelPart {
1210 value: ">".to_string(),
1211 ..Default::default()
1212 },
1213 ]);
1214
1215 Ok(hint_to_resolve)
1216 }
1217 },
1218 )
1219 .next()
1220 .await;
1221 cx.background_executor.run_until_parked();
1222
1223 cx.update_editor(|editor, cx| {
1224 update_inlay_link_and_hover_points(
1225 &editor.snapshot(cx),
1226 new_type_hint_part_hover_position,
1227 editor,
1228 true,
1229 false,
1230 cx,
1231 );
1232 });
1233 cx.background_executor
1234 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1235 cx.background_executor.run_until_parked();
1236 cx.update_editor(|editor, cx| {
1237 let hover_state = &editor.hover_state;
1238 assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1239 let popover = hover_state.info_popover.as_ref().unwrap();
1240 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1241 assert_eq!(
1242 popover.symbol_range,
1243 RangeInEditor::Inlay(InlayHighlight {
1244 inlay: InlayId::Hint(0),
1245 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1246 range: ": ".len()..": ".len() + new_type_label.len(),
1247 }),
1248 "Popover range should match the new type label part"
1249 );
1250 assert_eq!(
1251 popover.parsed_content.text,
1252 format!("A tooltip for `{new_type_label}`"),
1253 "Rendered text should not anyhow alter backticks"
1254 );
1255 });
1256
1257 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1258 let snapshot = editor.snapshot(cx);
1259 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1260 let next_valid = inlay_range.end.to_display_point(&snapshot);
1261 assert_eq!(previous_valid.row(), next_valid.row());
1262 assert!(previous_valid.column() < next_valid.column());
1263 let exact_unclipped = DisplayPoint::new(
1264 previous_valid.row(),
1265 previous_valid.column()
1266 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1267 as u32,
1268 );
1269 PointForPosition {
1270 previous_valid,
1271 next_valid,
1272 exact_unclipped,
1273 column_overshoot_after_line_end: 0,
1274 }
1275 });
1276 cx.update_editor(|editor, cx| {
1277 update_inlay_link_and_hover_points(
1278 &editor.snapshot(cx),
1279 struct_hint_part_hover_position,
1280 editor,
1281 true,
1282 false,
1283 cx,
1284 );
1285 });
1286 cx.background_executor
1287 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1288 cx.background_executor.run_until_parked();
1289 cx.update_editor(|editor, cx| {
1290 let hover_state = &editor.hover_state;
1291 assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1292 let popover = hover_state.info_popover.as_ref().unwrap();
1293 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1294 assert_eq!(
1295 popover.symbol_range,
1296 RangeInEditor::Inlay(InlayHighlight {
1297 inlay: InlayId::Hint(0),
1298 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1299 range: ": ".len() + new_type_label.len() + "<".len()
1300 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1301 }),
1302 "Popover range should match the struct label part"
1303 );
1304 assert_eq!(
1305 popover.parsed_content.text,
1306 format!("A tooltip for {struct_label}"),
1307 "Rendered markdown element should remove backticks from text"
1308 );
1309 });
1310 }
1311}