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