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