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