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