1use crate::{
2 display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint},
3 hover_links::{InlayHighlight, RangeInEditor},
4 scroll::ScrollAmount,
5 Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
6 Hover,
7};
8use gpui::{
9 div, px, AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla,
10 InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
11 Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement,
12 Window,
13};
14use itertools::Itertools;
15use language::{DiagnosticEntry, Language, LanguageRegistry};
16use lsp::DiagnosticSeverity;
17use markdown::{Markdown, MarkdownStyle};
18use multi_buffer::ToOffset;
19use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
20use settings::Settings;
21use std::rc::Rc;
22use std::{borrow::Cow, cell::RefCell};
23use std::{ops::Range, sync::Arc, time::Duration};
24use theme::ThemeSettings;
25use ui::{prelude::*, theme_is_transparent, Scrollbar, ScrollbarState};
26use util::TryFutureExt;
27pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
28
29pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
30pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
31pub const HOVER_POPOVER_GAP: Pixels = px(10.);
32
33/// Bindable action which uses the most recent selection head to trigger a hover
34pub fn hover(editor: &mut Editor, _: &Hover, window: &mut Window, cx: &mut Context<Editor>) {
35 let head = editor.selections.newest_anchor().head();
36 show_hover(editor, head, true, window, cx);
37}
38
39/// The internal hover action dispatches between `show_hover` or `hide_hover`
40/// depending on whether a point to hover over is provided.
41pub fn hover_at(
42 editor: &mut Editor,
43 anchor: Option<Anchor>,
44 window: &mut Window,
45 cx: &mut Context<Editor>,
46) {
47 if EditorSettings::get_global(cx).hover_popover_enabled {
48 if show_keyboard_hover(editor, window, cx) {
49 return;
50 }
51 if let Some(anchor) = anchor {
52 show_hover(editor, anchor, false, window, cx);
53 } else {
54 hide_hover(editor, cx);
55 }
56 }
57}
58
59pub fn show_keyboard_hover(
60 editor: &mut Editor,
61 window: &mut Window,
62 cx: &mut Context<Editor>,
63) -> bool {
64 let info_popovers = editor.hover_state.info_popovers.clone();
65 for p in info_popovers {
66 let keyboard_grace = p.keyboard_grace.borrow();
67 if *keyboard_grace {
68 if let Some(anchor) = p.anchor {
69 show_hover(editor, anchor, false, window, cx);
70 return true;
71 }
72 }
73 }
74
75 let diagnostic_popover = editor.hover_state.diagnostic_popover.clone();
76 if let Some(d) = diagnostic_popover {
77 let keyboard_grace = d.keyboard_grace.borrow();
78 if *keyboard_grace {
79 if let Some(anchor) = d.anchor {
80 show_hover(editor, anchor, false, window, cx);
81 return true;
82 }
83 }
84 }
85
86 false
87}
88
89pub struct InlayHover {
90 pub range: InlayHighlight,
91 pub tooltip: HoverBlock,
92}
93
94pub fn find_hovered_hint_part(
95 label_parts: Vec<InlayHintLabelPart>,
96 hint_start: InlayOffset,
97 hovered_offset: InlayOffset,
98) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
99 if hovered_offset >= hint_start {
100 let mut hovered_character = (hovered_offset - hint_start).0;
101 let mut part_start = hint_start;
102 for part in label_parts {
103 let part_len = part.value.chars().count();
104 if hovered_character > part_len {
105 hovered_character -= part_len;
106 part_start.0 += part_len;
107 } else {
108 let part_end = InlayOffset(part_start.0 + part_len);
109 return Some((part, part_start..part_end));
110 }
111 }
112 }
113 None
114}
115
116pub fn hover_at_inlay(
117 editor: &mut Editor,
118 inlay_hover: InlayHover,
119 window: &mut Window,
120 cx: &mut Context<Editor>,
121) {
122 if EditorSettings::get_global(cx).hover_popover_enabled {
123 if editor.pending_rename.is_some() {
124 return;
125 }
126
127 let Some(project) = editor.project.clone() else {
128 return;
129 };
130
131 if editor
132 .hover_state
133 .info_popovers
134 .iter()
135 .any(|InfoPopover { symbol_range, .. }| {
136 if let RangeInEditor::Inlay(range) = symbol_range {
137 if range == &inlay_hover.range {
138 // Hover triggered from same location as last time. Don't show again.
139 return true;
140 }
141 }
142 false
143 })
144 {
145 hide_hover(editor, cx);
146 }
147
148 let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
149
150 let task = cx.spawn_in(window, |this, mut cx| {
151 async move {
152 cx.background_executor()
153 .timer(Duration::from_millis(hover_popover_delay))
154 .await;
155 this.update(&mut cx, |this, _| {
156 this.hover_state.diagnostic_popover = None;
157 })?;
158
159 let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
160 let blocks = vec![inlay_hover.tooltip];
161 let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
162
163 let scroll_handle = ScrollHandle::new();
164 let hover_popover = InfoPopover {
165 symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
166 parsed_content,
167 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
168 scroll_handle,
169 keyboard_grace: Rc::new(RefCell::new(false)),
170 anchor: None,
171 };
172
173 this.update(&mut cx, |this, cx| {
174 // TODO: no background highlights happen for inlays currently
175 this.hover_state.info_popovers = vec![hover_popover];
176 cx.notify();
177 })?;
178
179 anyhow::Ok(())
180 }
181 .log_err()
182 });
183
184 editor.hover_state.info_task = Some(task);
185 }
186}
187
188/// Hides the type information popup.
189/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
190/// selections changed.
191pub fn hide_hover(editor: &mut Editor, cx: &mut Context<Editor>) -> bool {
192 let info_popovers = editor.hover_state.info_popovers.drain(..);
193 let diagnostics_popover = editor.hover_state.diagnostic_popover.take();
194 let did_hide = info_popovers.count() > 0 || diagnostics_popover.is_some();
195
196 editor.hover_state.info_task = None;
197 editor.hover_state.triggered_from = None;
198
199 editor.clear_background_highlights::<HoverState>(cx);
200
201 if did_hide {
202 cx.notify();
203 }
204
205 did_hide
206}
207
208/// Queries the LSP and shows type info and documentation
209/// about the symbol the mouse is currently hovering over.
210/// Triggered by the `Hover` action when the cursor may be over a symbol.
211fn show_hover(
212 editor: &mut Editor,
213 anchor: Anchor,
214 ignore_timeout: bool,
215 window: &mut Window,
216 cx: &mut Context<Editor>,
217) -> Option<()> {
218 if editor.pending_rename.is_some() {
219 return None;
220 }
221
222 let snapshot = editor.snapshot(window, cx);
223
224 let (buffer, buffer_position) = editor
225 .buffer
226 .read(cx)
227 .text_anchor_for_position(anchor, cx)?;
228
229 let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
230
231 let language_registry = editor.project.as_ref()?.read(cx).languages().clone();
232 let provider = editor.semantics_provider.clone()?;
233
234 if !ignore_timeout {
235 if same_info_hover(editor, &snapshot, anchor)
236 || same_diagnostic_hover(editor, &snapshot, anchor)
237 || editor.hover_state.diagnostic_popover.is_some()
238 {
239 // Hover triggered from same location as last time. Don't show again.
240 return None;
241 } else {
242 hide_hover(editor, cx);
243 }
244 }
245
246 // Don't request again if the location is the same as the previous request
247 if let Some(triggered_from) = &editor.hover_state.triggered_from {
248 if triggered_from
249 .cmp(&anchor, &snapshot.buffer_snapshot)
250 .is_eq()
251 {
252 return None;
253 }
254 }
255
256 let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
257
258 let task = cx.spawn_in(window, |this, mut cx| {
259 async move {
260 // If we need to delay, delay a set amount initially before making the lsp request
261 let delay = if ignore_timeout {
262 None
263 } else {
264 // Construct delay task to wait for later
265 let total_delay = Some(
266 cx.background_executor()
267 .timer(Duration::from_millis(hover_popover_delay)),
268 );
269
270 cx.background_executor()
271 .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
272 .await;
273 total_delay
274 };
275
276 let hover_request = cx.update(|_, cx| provider.hover(&buffer, buffer_position, cx))?;
277
278 if let Some(delay) = delay {
279 delay.await;
280 }
281
282 let local_diagnostic = snapshot
283 .buffer_snapshot
284 .diagnostics_in_range::<_, usize>(anchor..anchor)
285 // Find the entry with the most specific range
286 .min_by_key(|entry| entry.range.len());
287
288 let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
289 let text = match local_diagnostic.diagnostic.source {
290 Some(ref source) => {
291 format!("{source}: {}", local_diagnostic.diagnostic.message)
292 }
293 None => local_diagnostic.diagnostic.message.clone(),
294 };
295 let local_diagnostic = DiagnosticEntry {
296 diagnostic: local_diagnostic.diagnostic,
297 range: snapshot
298 .buffer_snapshot
299 .anchor_before(local_diagnostic.range.start)
300 ..snapshot
301 .buffer_snapshot
302 .anchor_after(local_diagnostic.range.end),
303 };
304
305 let mut border_color: Option<Hsla> = None;
306 let mut background_color: Option<Hsla> = None;
307
308 let parsed_content = cx
309 .new_window_entity(|window, cx| {
310 let status_colors = cx.theme().status();
311
312 match local_diagnostic.diagnostic.severity {
313 DiagnosticSeverity::ERROR => {
314 background_color = Some(status_colors.error_background);
315 border_color = Some(status_colors.error_border);
316 }
317 DiagnosticSeverity::WARNING => {
318 background_color = Some(status_colors.warning_background);
319 border_color = Some(status_colors.warning_border);
320 }
321 DiagnosticSeverity::INFORMATION => {
322 background_color = Some(status_colors.info_background);
323 border_color = Some(status_colors.info_border);
324 }
325 DiagnosticSeverity::HINT => {
326 background_color = Some(status_colors.hint_background);
327 border_color = Some(status_colors.hint_border);
328 }
329 _ => {
330 background_color = Some(status_colors.ignored_background);
331 border_color = Some(status_colors.ignored_border);
332 }
333 };
334 let settings = ThemeSettings::get_global(cx);
335 let mut base_text_style = window.text_style();
336 base_text_style.refine(&TextStyleRefinement {
337 font_family: Some(settings.ui_font.family.clone()),
338 font_fallbacks: settings.ui_font.fallbacks.clone(),
339 font_size: Some(settings.ui_font_size.into()),
340 color: Some(cx.theme().colors().editor_foreground),
341 background_color: Some(gpui::transparent_black()),
342
343 ..Default::default()
344 });
345 let markdown_style = MarkdownStyle {
346 base_text_style,
347 selection_background_color: { cx.theme().players().local().selection },
348 link: TextStyleRefinement {
349 underline: Some(gpui::UnderlineStyle {
350 thickness: px(1.),
351 color: Some(cx.theme().colors().editor_foreground),
352 wavy: false,
353 }),
354 ..Default::default()
355 },
356 ..Default::default()
357 };
358 Markdown::new_text(text, markdown_style.clone(), None, None, window, cx)
359 })
360 .ok();
361
362 Some(DiagnosticPopover {
363 local_diagnostic,
364 parsed_content,
365 border_color,
366 background_color,
367 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
368 anchor: Some(anchor),
369 })
370 } else {
371 None
372 };
373
374 this.update(&mut cx, |this, _| {
375 this.hover_state.diagnostic_popover = diagnostic_popover;
376 })?;
377
378 let invisible_char = if let Some(invisible) = snapshot
379 .buffer_snapshot
380 .chars_at(anchor)
381 .next()
382 .filter(|&c| is_invisible(c))
383 {
384 let after = snapshot.buffer_snapshot.anchor_after(
385 anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
386 );
387 Some((invisible, anchor..after))
388 } else if let Some(invisible) = snapshot
389 .buffer_snapshot
390 .reversed_chars_at(anchor)
391 .next()
392 .filter(|&c| is_invisible(c))
393 {
394 let before = snapshot.buffer_snapshot.anchor_before(
395 anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
396 );
397
398 Some((invisible, before..anchor))
399 } else {
400 None
401 };
402
403 let hovers_response = if let Some(hover_request) = hover_request {
404 hover_request.await
405 } else {
406 Vec::new()
407 };
408 let snapshot = this.update_in(&mut cx, |this, window, cx| this.snapshot(window, cx))?;
409 let mut hover_highlights = Vec::with_capacity(hovers_response.len());
410 let mut info_popovers = Vec::with_capacity(
411 hovers_response.len() + if invisible_char.is_some() { 1 } else { 0 },
412 );
413
414 if let Some((invisible, range)) = invisible_char {
415 let blocks = vec![HoverBlock {
416 text: format!("Unicode character U+{:02X}", invisible as u32),
417 kind: HoverBlockKind::PlainText,
418 }];
419 let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
420 let scroll_handle = ScrollHandle::new();
421 info_popovers.push(InfoPopover {
422 symbol_range: RangeInEditor::Text(range),
423 parsed_content,
424 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
425 scroll_handle,
426 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
427 anchor: Some(anchor),
428 })
429 }
430
431 for hover_result in hovers_response {
432 // Create symbol range of anchors for highlighting and filtering of future requests.
433 let range = hover_result
434 .range
435 .and_then(|range| {
436 let start = snapshot
437 .buffer_snapshot
438 .anchor_in_excerpt(excerpt_id, range.start)?;
439 let end = snapshot
440 .buffer_snapshot
441 .anchor_in_excerpt(excerpt_id, range.end)?;
442 Some(start..end)
443 })
444 .or_else(|| {
445 let snapshot = &snapshot.buffer_snapshot;
446 let offset_range = snapshot.syntax_ancestor(anchor..anchor)?.1;
447 Some(
448 snapshot.anchor_before(offset_range.start)
449 ..snapshot.anchor_after(offset_range.end),
450 )
451 })
452 .unwrap_or_else(|| anchor..anchor);
453
454 let blocks = hover_result.contents;
455 let language = hover_result.language;
456 let parsed_content =
457 parse_blocks(&blocks, &language_registry, language, &mut cx).await;
458 let scroll_handle = ScrollHandle::new();
459 hover_highlights.push(range.clone());
460 info_popovers.push(InfoPopover {
461 symbol_range: RangeInEditor::Text(range),
462 parsed_content,
463 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
464 scroll_handle,
465 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
466 anchor: Some(anchor),
467 });
468 }
469
470 this.update_in(&mut cx, |editor, window, cx| {
471 if hover_highlights.is_empty() {
472 editor.clear_background_highlights::<HoverState>(cx);
473 } else {
474 // Highlight the selected symbol using a background highlight
475 editor.highlight_background::<HoverState>(
476 &hover_highlights,
477 |theme| theme.element_hover, // todo update theme
478 cx,
479 );
480 }
481
482 editor.hover_state.info_popovers = info_popovers;
483 cx.notify();
484 window.refresh();
485 })?;
486
487 anyhow::Ok(())
488 }
489 .log_err()
490 });
491
492 editor.hover_state.info_task = Some(task);
493 None
494}
495
496fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
497 editor
498 .hover_state
499 .info_popovers
500 .iter()
501 .any(|InfoPopover { symbol_range, .. }| {
502 symbol_range
503 .as_text_range()
504 .map(|range| {
505 let hover_range = range.to_offset(&snapshot.buffer_snapshot);
506 let offset = anchor.to_offset(&snapshot.buffer_snapshot);
507 // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
508 // use an inclusive range here to check if we should dismiss the popover
509 (hover_range.start..=hover_range.end).contains(&offset)
510 })
511 .unwrap_or(false)
512 })
513}
514
515fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
516 editor
517 .hover_state
518 .diagnostic_popover
519 .as_ref()
520 .map(|diagnostic| {
521 let hover_range = diagnostic
522 .local_diagnostic
523 .range
524 .to_offset(&snapshot.buffer_snapshot);
525 let offset = anchor.to_offset(&snapshot.buffer_snapshot);
526
527 // Here we do basically the same as in `same_info_hover`, see comment there for an explanation
528 (hover_range.start..=hover_range.end).contains(&offset)
529 })
530 .unwrap_or(false)
531}
532
533async fn parse_blocks(
534 blocks: &[HoverBlock],
535 language_registry: &Arc<LanguageRegistry>,
536 language: Option<Arc<Language>>,
537 cx: &mut AsyncWindowContext,
538) -> Option<Entity<Markdown>> {
539 let fallback_language_name = if let Some(ref l) = language {
540 let l = Arc::clone(l);
541 Some(l.lsp_id().clone())
542 } else {
543 None
544 };
545
546 let combined_text = blocks
547 .iter()
548 .map(|block| match &block.kind {
549 project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
550 Cow::Borrowed(block.text.trim())
551 }
552 project::HoverBlockKind::Code { language } => {
553 Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
554 }
555 })
556 .join("\n\n");
557
558 let rendered_block = cx
559 .new_window_entity(|window, cx| {
560 let settings = ThemeSettings::get_global(cx);
561 let ui_font_family = settings.ui_font.family.clone();
562 let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
563 let buffer_font_family = settings.buffer_font.family.clone();
564 let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
565
566 let mut base_text_style = window.text_style();
567 base_text_style.refine(&TextStyleRefinement {
568 font_family: Some(ui_font_family.clone()),
569 font_fallbacks: ui_font_fallbacks,
570 color: Some(cx.theme().colors().editor_foreground),
571 ..Default::default()
572 });
573
574 let markdown_style = MarkdownStyle {
575 base_text_style,
576 code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
577 inline_code: TextStyleRefinement {
578 background_color: Some(cx.theme().colors().background),
579 font_family: Some(buffer_font_family),
580 font_fallbacks: buffer_font_fallbacks,
581 ..Default::default()
582 },
583 rule_color: cx.theme().colors().border,
584 block_quote_border_color: Color::Muted.color(cx),
585 block_quote: TextStyleRefinement {
586 color: Some(Color::Muted.color(cx)),
587 ..Default::default()
588 },
589 link: TextStyleRefinement {
590 color: Some(cx.theme().colors().editor_foreground),
591 underline: Some(gpui::UnderlineStyle {
592 thickness: px(1.),
593 color: Some(cx.theme().colors().editor_foreground),
594 wavy: false,
595 }),
596 ..Default::default()
597 },
598 syntax: cx.theme().syntax().clone(),
599 selection_background_color: { cx.theme().players().local().selection },
600 break_style: Default::default(),
601 heading: StyleRefinement::default()
602 .font_weight(FontWeight::BOLD)
603 .text_base()
604 .mt(rems(1.))
605 .mb_0(),
606 };
607
608 Markdown::new(
609 combined_text,
610 markdown_style.clone(),
611 Some(language_registry.clone()),
612 fallback_language_name,
613 window,
614 cx,
615 )
616 .copy_code_block_buttons(false)
617 })
618 .ok();
619
620 rendered_block
621}
622
623#[derive(Default, Debug)]
624pub struct HoverState {
625 pub info_popovers: Vec<InfoPopover>,
626 pub diagnostic_popover: Option<DiagnosticPopover>,
627 pub triggered_from: Option<Anchor>,
628 pub info_task: Option<Task<Option<()>>>,
629}
630
631impl HoverState {
632 pub fn visible(&self) -> bool {
633 !self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
634 }
635
636 pub(crate) fn render(
637 &mut self,
638 snapshot: &EditorSnapshot,
639 visible_rows: Range<DisplayRow>,
640 max_size: Size<Pixels>,
641 cx: &mut Context<Editor>,
642 ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
643 // If there is a diagnostic, position the popovers based on that.
644 // Otherwise use the start of the hover range
645 let anchor = self
646 .diagnostic_popover
647 .as_ref()
648 .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
649 .or_else(|| {
650 self.info_popovers.iter().find_map(|info_popover| {
651 match &info_popover.symbol_range {
652 RangeInEditor::Text(range) => Some(&range.start),
653 RangeInEditor::Inlay(_) => None,
654 }
655 })
656 })
657 .or_else(|| {
658 self.info_popovers.iter().find_map(|info_popover| {
659 match &info_popover.symbol_range {
660 RangeInEditor::Text(_) => None,
661 RangeInEditor::Inlay(range) => Some(&range.inlay_position),
662 }
663 })
664 })?;
665 let point = anchor.to_display_point(&snapshot.display_snapshot);
666
667 // Don't render if the relevant point isn't on screen
668 if !self.visible() || !visible_rows.contains(&point.row()) {
669 return None;
670 }
671
672 let mut elements = Vec::new();
673
674 if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
675 elements.push(diagnostic_popover.render(max_size, cx));
676 }
677 for info_popover in &mut self.info_popovers {
678 elements.push(info_popover.render(max_size, cx));
679 }
680
681 Some((point, elements))
682 }
683
684 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
685 let mut hover_popover_is_focused = false;
686 for info_popover in &self.info_popovers {
687 if let Some(markdown_view) = &info_popover.parsed_content {
688 if markdown_view.focus_handle(cx).is_focused(window) {
689 hover_popover_is_focused = true;
690 }
691 }
692 }
693 if let Some(diagnostic_popover) = &self.diagnostic_popover {
694 if let Some(markdown_view) = &diagnostic_popover.parsed_content {
695 if markdown_view.focus_handle(cx).is_focused(window) {
696 hover_popover_is_focused = true;
697 }
698 }
699 }
700 hover_popover_is_focused
701 }
702}
703
704#[derive(Debug, Clone)]
705pub(crate) struct InfoPopover {
706 pub(crate) symbol_range: RangeInEditor,
707 pub(crate) parsed_content: Option<Entity<Markdown>>,
708 pub(crate) scroll_handle: ScrollHandle,
709 pub(crate) scrollbar_state: ScrollbarState,
710 pub(crate) keyboard_grace: Rc<RefCell<bool>>,
711 pub(crate) anchor: Option<Anchor>,
712}
713
714impl InfoPopover {
715 pub(crate) fn render(
716 &mut self,
717 max_size: Size<Pixels>,
718 cx: &mut Context<Editor>,
719 ) -> AnyElement {
720 let keyboard_grace = Rc::clone(&self.keyboard_grace);
721 let mut d = div()
722 .id("info_popover")
723 .elevation_2(cx)
724 // Prevent a mouse down/move on the popover from being propagated to the editor,
725 // because that would dismiss the popover.
726 .on_mouse_move(|_, _, cx| cx.stop_propagation())
727 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
728 let mut keyboard_grace = keyboard_grace.borrow_mut();
729 *keyboard_grace = false;
730 cx.stop_propagation();
731 });
732
733 if let Some(markdown) = &self.parsed_content {
734 d = d
735 .child(
736 div()
737 .id("info-md-container")
738 .overflow_y_scroll()
739 .max_w(max_size.width)
740 .max_h(max_size.height)
741 .p_2()
742 .track_scroll(&self.scroll_handle)
743 .child(markdown.clone()),
744 )
745 .child(self.render_vertical_scrollbar(cx));
746 }
747 d.into_any_element()
748 }
749
750 pub fn scroll(&self, amount: &ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
751 let mut current = self.scroll_handle.offset();
752 current.y -= amount.pixels(
753 window.line_height(),
754 self.scroll_handle.bounds().size.height - px(16.),
755 ) / 2.0;
756 cx.notify();
757 self.scroll_handle.set_offset(current);
758 }
759
760 fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
761 div()
762 .occlude()
763 .id("info-popover-vertical-scroll")
764 .on_mouse_move(cx.listener(|_, _, _, cx| {
765 cx.notify();
766 cx.stop_propagation()
767 }))
768 .on_hover(|_, _, cx| {
769 cx.stop_propagation();
770 })
771 .on_any_mouse_down(|_, _, cx| {
772 cx.stop_propagation();
773 })
774 .on_mouse_up(
775 MouseButton::Left,
776 cx.listener(|_, _, _, cx| {
777 cx.stop_propagation();
778 }),
779 )
780 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
781 cx.notify();
782 }))
783 .h_full()
784 .absolute()
785 .right_1()
786 .top_1()
787 .bottom_0()
788 .w(px(12.))
789 .cursor_default()
790 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
791 }
792}
793
794#[derive(Debug, Clone)]
795pub struct DiagnosticPopover {
796 pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
797 parsed_content: Option<Entity<Markdown>>,
798 border_color: Option<Hsla>,
799 background_color: Option<Hsla>,
800 pub keyboard_grace: Rc<RefCell<bool>>,
801 pub anchor: Option<Anchor>,
802}
803
804impl DiagnosticPopover {
805 pub fn render(&self, max_size: Size<Pixels>, cx: &mut Context<Editor>) -> AnyElement {
806 let keyboard_grace = Rc::clone(&self.keyboard_grace);
807 let mut markdown_div = div().py_1().px_2();
808 if let Some(markdown) = &self.parsed_content {
809 markdown_div = markdown_div.child(markdown.clone());
810 }
811
812 if let Some(background_color) = &self.background_color {
813 markdown_div = markdown_div.bg(*background_color);
814 }
815
816 if let Some(border_color) = &self.border_color {
817 markdown_div = markdown_div
818 .border_1()
819 .border_color(*border_color)
820 .rounded_lg();
821 }
822
823 let diagnostic_div = div()
824 .id("diagnostic")
825 .block()
826 .max_h(max_size.height)
827 .overflow_y_scroll()
828 .max_w(max_size.width)
829 .elevation_2_borderless(cx)
830 // Don't draw the background color if the theme
831 // allows transparent surfaces.
832 .when(theme_is_transparent(cx), |this| {
833 this.bg(gpui::transparent_black())
834 })
835 // Prevent a mouse move on the popover from being propagated to the editor,
836 // because that would dismiss the popover.
837 .on_mouse_move(|_, _, cx| cx.stop_propagation())
838 // Prevent a mouse down on the popover from being propagated to the editor,
839 // because that would move the cursor.
840 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
841 let mut keyboard_grace = keyboard_grace.borrow_mut();
842 *keyboard_grace = false;
843 cx.stop_propagation();
844 })
845 .child(markdown_div);
846
847 diagnostic_div.into_any_element()
848 }
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854 use crate::{
855 actions::ConfirmCompletion,
856 editor_tests::{handle_completion_request, init_test},
857 hover_links::update_inlay_link_and_hover_points,
858 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
859 test::editor_lsp_test_context::EditorLspTestContext,
860 InlayId, PointForPosition,
861 };
862 use collections::BTreeSet;
863 use gpui::App;
864 use indoc::indoc;
865 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
866 use lsp::LanguageServerId;
867 use markdown::parser::MarkdownEvent;
868 use smol::stream::StreamExt;
869 use std::sync::atomic;
870 use std::sync::atomic::AtomicUsize;
871 use text::Bias;
872
873 fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
874 cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay })
875 }
876
877 impl InfoPopover {
878 fn get_rendered_text(&self, cx: &gpui::App) -> String {
879 let mut rendered_text = String::new();
880 if let Some(parsed_content) = self.parsed_content.clone() {
881 let markdown = parsed_content.read(cx);
882 let text = markdown.parsed_markdown().source().to_string();
883 let data = markdown.parsed_markdown().events();
884 let slice = data;
885
886 for (range, event) in slice.iter() {
887 if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
888 rendered_text.push_str(&text[range.clone()])
889 }
890 }
891 }
892 rendered_text
893 }
894 }
895
896 #[gpui::test]
897 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
898 cx: &mut gpui::TestAppContext,
899 ) {
900 init_test(cx, |_| {});
901
902 let mut cx = EditorLspTestContext::new_rust(
903 lsp::ServerCapabilities {
904 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
905 completion_provider: Some(lsp::CompletionOptions {
906 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
907 resolve_provider: Some(true),
908 ..Default::default()
909 }),
910 ..Default::default()
911 },
912 cx,
913 )
914 .await;
915 let counter = Arc::new(AtomicUsize::new(0));
916 // Basic hover delays and then pops without moving the mouse
917 cx.set_state(indoc! {"
918 oneˇ
919 two
920 three
921 fn test() { println!(); }
922 "});
923
924 //prompt autocompletion menu
925 cx.simulate_keystroke(".");
926 handle_completion_request(
927 &mut cx,
928 indoc! {"
929 one.|<>
930 two
931 three
932 "},
933 vec!["first_completion", "second_completion"],
934 counter.clone(),
935 )
936 .await;
937 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
938 .await;
939 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
940
941 let hover_point = cx.display_point(indoc! {"
942 one.
943 two
944 three
945 fn test() { printˇln!(); }
946 "});
947 cx.update_editor(|editor, window, cx| {
948 let snapshot = editor.snapshot(window, cx);
949 let anchor = snapshot
950 .buffer_snapshot
951 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
952 hover_at(editor, Some(anchor), window, cx)
953 });
954 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
955
956 // After delay, hover should be visible.
957 let symbol_range = cx.lsp_range(indoc! {"
958 one.
959 two
960 three
961 fn test() { «println!»(); }
962 "});
963 let mut requests =
964 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
965 Ok(Some(lsp::Hover {
966 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
967 kind: lsp::MarkupKind::Markdown,
968 value: "some basic docs".to_string(),
969 }),
970 range: Some(symbol_range),
971 }))
972 });
973 cx.background_executor
974 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
975 requests.next().await;
976
977 cx.editor(|editor, _window, cx| {
978 assert!(editor.hover_state.visible());
979 assert_eq!(
980 editor.hover_state.info_popovers.len(),
981 1,
982 "Expected exactly one hover but got: {:?}",
983 editor.hover_state.info_popovers
984 );
985 let rendered_text = editor
986 .hover_state
987 .info_popovers
988 .first()
989 .unwrap()
990 .get_rendered_text(cx);
991 assert_eq!(rendered_text, "some basic docs".to_string())
992 });
993
994 // check that the completion menu is still visible and that there still has only been 1 completion request
995 cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
996 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
997
998 //apply a completion and check it was successfully applied
999 let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1000 editor.context_menu_next(&Default::default(), window, cx);
1001 editor
1002 .confirm_completion(&ConfirmCompletion::default(), window, cx)
1003 .unwrap()
1004 });
1005 cx.assert_editor_state(indoc! {"
1006 one.second_completionˇ
1007 two
1008 three
1009 fn test() { println!(); }
1010 "});
1011
1012 // check that the completion menu is no longer visible and that there still has only been 1 completion request
1013 cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1014 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1015
1016 //verify the information popover is still visible and unchanged
1017 cx.editor(|editor, _, cx| {
1018 assert!(editor.hover_state.visible());
1019 assert_eq!(
1020 editor.hover_state.info_popovers.len(),
1021 1,
1022 "Expected exactly one hover but got: {:?}",
1023 editor.hover_state.info_popovers
1024 );
1025 let rendered_text = editor
1026 .hover_state
1027 .info_popovers
1028 .first()
1029 .unwrap()
1030 .get_rendered_text(cx);
1031
1032 assert_eq!(rendered_text, "some basic docs".to_string())
1033 });
1034
1035 // Mouse moved with no hover response dismisses
1036 let hover_point = cx.display_point(indoc! {"
1037 one.second_completionˇ
1038 two
1039 three
1040 fn teˇst() { println!(); }
1041 "});
1042 let mut request = cx
1043 .lsp
1044 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1045 cx.update_editor(|editor, window, cx| {
1046 let snapshot = editor.snapshot(window, cx);
1047 let anchor = snapshot
1048 .buffer_snapshot
1049 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1050 hover_at(editor, Some(anchor), window, cx)
1051 });
1052 cx.background_executor
1053 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1054 request.next().await;
1055
1056 // verify that the information popover is no longer visible
1057 cx.editor(|editor, _, _| {
1058 assert!(!editor.hover_state.visible());
1059 });
1060 }
1061
1062 #[gpui::test]
1063 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1064 init_test(cx, |_| {});
1065
1066 let mut cx = EditorLspTestContext::new_rust(
1067 lsp::ServerCapabilities {
1068 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1069 ..Default::default()
1070 },
1071 cx,
1072 )
1073 .await;
1074
1075 // Basic hover delays and then pops without moving the mouse
1076 cx.set_state(indoc! {"
1077 fn ˇtest() { println!(); }
1078 "});
1079 let hover_point = cx.display_point(indoc! {"
1080 fn test() { printˇln!(); }
1081 "});
1082
1083 cx.update_editor(|editor, window, cx| {
1084 let snapshot = editor.snapshot(window, cx);
1085 let anchor = snapshot
1086 .buffer_snapshot
1087 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1088 hover_at(editor, Some(anchor), window, cx)
1089 });
1090 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1091
1092 // After delay, hover should be visible.
1093 let symbol_range = cx.lsp_range(indoc! {"
1094 fn test() { «println!»(); }
1095 "});
1096 let mut requests =
1097 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1098 Ok(Some(lsp::Hover {
1099 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1100 kind: lsp::MarkupKind::Markdown,
1101 value: "some basic docs".to_string(),
1102 }),
1103 range: Some(symbol_range),
1104 }))
1105 });
1106 cx.background_executor
1107 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1108 requests.next().await;
1109
1110 cx.editor(|editor, _, cx| {
1111 assert!(editor.hover_state.visible());
1112 assert_eq!(
1113 editor.hover_state.info_popovers.len(),
1114 1,
1115 "Expected exactly one hover but got: {:?}",
1116 editor.hover_state.info_popovers
1117 );
1118 let rendered_text = editor
1119 .hover_state
1120 .info_popovers
1121 .first()
1122 .unwrap()
1123 .get_rendered_text(cx);
1124
1125 assert_eq!(rendered_text, "some basic docs".to_string())
1126 });
1127
1128 // Mouse moved with no hover response dismisses
1129 let hover_point = cx.display_point(indoc! {"
1130 fn teˇst() { println!(); }
1131 "});
1132 let mut request = cx
1133 .lsp
1134 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1135 cx.update_editor(|editor, window, cx| {
1136 let snapshot = editor.snapshot(window, cx);
1137 let anchor = snapshot
1138 .buffer_snapshot
1139 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1140 hover_at(editor, Some(anchor), window, cx)
1141 });
1142 cx.background_executor
1143 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1144 request.next().await;
1145 cx.editor(|editor, _, _| {
1146 assert!(!editor.hover_state.visible());
1147 });
1148 }
1149
1150 #[gpui::test]
1151 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1152 init_test(cx, |_| {});
1153
1154 let mut cx = EditorLspTestContext::new_rust(
1155 lsp::ServerCapabilities {
1156 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1157 ..Default::default()
1158 },
1159 cx,
1160 )
1161 .await;
1162
1163 // Hover with keyboard has no delay
1164 cx.set_state(indoc! {"
1165 fˇn test() { println!(); }
1166 "});
1167 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1168 let symbol_range = cx.lsp_range(indoc! {"
1169 «fn» test() { println!(); }
1170 "});
1171
1172 cx.editor(|editor, _window, _cx| {
1173 assert!(!editor.hover_state.visible());
1174
1175 assert_eq!(
1176 editor.hover_state.info_popovers.len(),
1177 0,
1178 "Expected no hovers but got but got: {:?}",
1179 editor.hover_state.info_popovers
1180 );
1181 });
1182
1183 let mut requests =
1184 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1185 Ok(Some(lsp::Hover {
1186 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1187 kind: lsp::MarkupKind::Markdown,
1188 value: "some other basic docs".to_string(),
1189 }),
1190 range: Some(symbol_range),
1191 }))
1192 });
1193
1194 requests.next().await;
1195 cx.dispatch_action(Hover);
1196
1197 cx.condition(|editor, _| editor.hover_state.visible()).await;
1198 cx.editor(|editor, _, cx| {
1199 assert_eq!(
1200 editor.hover_state.info_popovers.len(),
1201 1,
1202 "Expected exactly one hover but got: {:?}",
1203 editor.hover_state.info_popovers
1204 );
1205
1206 let rendered_text = editor
1207 .hover_state
1208 .info_popovers
1209 .first()
1210 .unwrap()
1211 .get_rendered_text(cx);
1212
1213 assert_eq!(rendered_text, "some other basic docs".to_string())
1214 });
1215 }
1216
1217 #[gpui::test]
1218 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1219 init_test(cx, |_| {});
1220
1221 let mut cx = EditorLspTestContext::new_rust(
1222 lsp::ServerCapabilities {
1223 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1224 ..Default::default()
1225 },
1226 cx,
1227 )
1228 .await;
1229
1230 // Hover with keyboard has no delay
1231 cx.set_state(indoc! {"
1232 fˇn test() { println!(); }
1233 "});
1234 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1235 let symbol_range = cx.lsp_range(indoc! {"
1236 «fn» test() { println!(); }
1237 "});
1238 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1239 Ok(Some(lsp::Hover {
1240 contents: lsp::HoverContents::Array(vec![
1241 lsp::MarkedString::String("regular text for hover to show".to_string()),
1242 lsp::MarkedString::String("".to_string()),
1243 lsp::MarkedString::LanguageString(lsp::LanguageString {
1244 language: "Rust".to_string(),
1245 value: "".to_string(),
1246 }),
1247 ]),
1248 range: Some(symbol_range),
1249 }))
1250 })
1251 .next()
1252 .await;
1253 cx.dispatch_action(Hover);
1254
1255 cx.condition(|editor, _| editor.hover_state.visible()).await;
1256 cx.editor(|editor, _, cx| {
1257 assert_eq!(
1258 editor.hover_state.info_popovers.len(),
1259 1,
1260 "Expected exactly one hover but got: {:?}",
1261 editor.hover_state.info_popovers
1262 );
1263 let rendered_text = editor
1264 .hover_state
1265 .info_popovers
1266 .first()
1267 .unwrap()
1268 .get_rendered_text(cx);
1269
1270 assert_eq!(
1271 rendered_text,
1272 "regular text for hover to show".to_string(),
1273 "No empty string hovers should be shown"
1274 );
1275 });
1276 }
1277
1278 #[gpui::test]
1279 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1280 init_test(cx, |_| {});
1281
1282 let mut cx = EditorLspTestContext::new_rust(
1283 lsp::ServerCapabilities {
1284 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1285 ..Default::default()
1286 },
1287 cx,
1288 )
1289 .await;
1290
1291 // Hover with keyboard has no delay
1292 cx.set_state(indoc! {"
1293 fˇn test() { println!(); }
1294 "});
1295 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1296 let symbol_range = cx.lsp_range(indoc! {"
1297 «fn» test() { println!(); }
1298 "});
1299
1300 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1301 let markdown_string = format!("\n```rust\n{code_str}```");
1302
1303 let closure_markdown_string = markdown_string.clone();
1304 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1305 let future_markdown_string = closure_markdown_string.clone();
1306 async move {
1307 Ok(Some(lsp::Hover {
1308 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1309 kind: lsp::MarkupKind::Markdown,
1310 value: future_markdown_string,
1311 }),
1312 range: Some(symbol_range),
1313 }))
1314 }
1315 })
1316 .next()
1317 .await;
1318
1319 cx.dispatch_action(Hover);
1320
1321 cx.condition(|editor, _| editor.hover_state.visible()).await;
1322 cx.editor(|editor, _, cx| {
1323 assert_eq!(
1324 editor.hover_state.info_popovers.len(),
1325 1,
1326 "Expected exactly one hover but got: {:?}",
1327 editor.hover_state.info_popovers
1328 );
1329 let rendered_text = editor
1330 .hover_state
1331 .info_popovers
1332 .first()
1333 .unwrap()
1334 .get_rendered_text(cx);
1335
1336 assert_eq!(
1337 rendered_text, code_str,
1338 "Should not have extra line breaks at end of rendered hover"
1339 );
1340 });
1341 }
1342
1343 #[gpui::test]
1344 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1345 init_test(cx, |_| {});
1346
1347 let mut cx = EditorLspTestContext::new_rust(
1348 lsp::ServerCapabilities {
1349 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1350 ..Default::default()
1351 },
1352 cx,
1353 )
1354 .await;
1355
1356 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1357 // info popover once request completes
1358 cx.set_state(indoc! {"
1359 fn teˇst() { println!(); }
1360 "});
1361
1362 // Send diagnostic to client
1363 let range = cx.text_anchor_range(indoc! {"
1364 fn «test»() { println!(); }
1365 "});
1366 cx.update_buffer(|buffer, cx| {
1367 let snapshot = buffer.text_snapshot();
1368 let set = DiagnosticSet::from_sorted_entries(
1369 vec![DiagnosticEntry {
1370 range,
1371 diagnostic: Diagnostic {
1372 message: "A test diagnostic message.".to_string(),
1373 ..Default::default()
1374 },
1375 }],
1376 &snapshot,
1377 );
1378 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1379 });
1380
1381 // Hover pops diagnostic immediately
1382 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1383 cx.background_executor.run_until_parked();
1384
1385 cx.editor(|Editor { hover_state, .. }, _, _| {
1386 assert!(
1387 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1388 )
1389 });
1390
1391 // Info Popover shows after request responded to
1392 let range = cx.lsp_range(indoc! {"
1393 fn «test»() { println!(); }
1394 "});
1395 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1396 Ok(Some(lsp::Hover {
1397 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1398 kind: lsp::MarkupKind::Markdown,
1399 value: "some new docs".to_string(),
1400 }),
1401 range: Some(range),
1402 }))
1403 });
1404 cx.background_executor
1405 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1406
1407 cx.background_executor.run_until_parked();
1408 cx.editor(|Editor { hover_state, .. }, _, _| {
1409 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1410 });
1411 }
1412
1413 #[gpui::test]
1414 // https://github.com/zed-industries/zed/issues/15498
1415 async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1416 init_test(cx, |_| {});
1417
1418 let mut cx = EditorLspTestContext::new_rust(
1419 lsp::ServerCapabilities {
1420 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1421 ..Default::default()
1422 },
1423 cx,
1424 )
1425 .await;
1426
1427 cx.set_state(indoc! {"
1428 fn fuˇnc(abc def: i32) -> u32 {
1429 }
1430 "});
1431
1432 cx.lsp.handle_request::<lsp::request::HoverRequest, _, _>({
1433 |_, _| async move {
1434 Ok(Some(lsp::Hover {
1435 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1436 kind: lsp::MarkupKind::Markdown,
1437 value: indoc!(
1438 r#"
1439 ### function `errands_data_read`
1440
1441 ---
1442 → `char *`
1443 Function to read a file into a string
1444
1445 ---
1446 ```cpp
1447 static char *errands_data_read()
1448 ```
1449 "#
1450 )
1451 .to_string(),
1452 }),
1453 range: None,
1454 }))
1455 }
1456 });
1457 cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1458 cx.run_until_parked();
1459
1460 cx.update_editor(|editor, _, cx| {
1461 let popover = editor.hover_state.info_popovers.first().unwrap();
1462 let content = popover.get_rendered_text(cx);
1463
1464 assert!(content.contains("Function to read a file"));
1465 });
1466 }
1467
1468 #[gpui::test]
1469 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1470 init_test(cx, |settings| {
1471 settings.defaults.inlay_hints = Some(InlayHintSettings {
1472 enabled: true,
1473 edit_debounce_ms: 0,
1474 scroll_debounce_ms: 0,
1475 show_type_hints: true,
1476 show_parameter_hints: true,
1477 show_other_hints: true,
1478 show_background: false,
1479 })
1480 });
1481
1482 let mut cx = EditorLspTestContext::new_rust(
1483 lsp::ServerCapabilities {
1484 inlay_hint_provider: Some(lsp::OneOf::Right(
1485 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1486 resolve_provider: Some(true),
1487 ..Default::default()
1488 }),
1489 )),
1490 ..Default::default()
1491 },
1492 cx,
1493 )
1494 .await;
1495
1496 cx.set_state(indoc! {"
1497 struct TestStruct;
1498
1499 // ==================
1500
1501 struct TestNewType<T>(T);
1502
1503 fn main() {
1504 let variableˇ = TestNewType(TestStruct);
1505 }
1506 "});
1507
1508 let hint_start_offset = cx.ranges(indoc! {"
1509 struct TestStruct;
1510
1511 // ==================
1512
1513 struct TestNewType<T>(T);
1514
1515 fn main() {
1516 let variableˇ = TestNewType(TestStruct);
1517 }
1518 "})[0]
1519 .start;
1520 let hint_position = cx.to_lsp(hint_start_offset);
1521 let new_type_target_range = cx.lsp_range(indoc! {"
1522 struct TestStruct;
1523
1524 // ==================
1525
1526 struct «TestNewType»<T>(T);
1527
1528 fn main() {
1529 let variable = TestNewType(TestStruct);
1530 }
1531 "});
1532 let struct_target_range = cx.lsp_range(indoc! {"
1533 struct «TestStruct»;
1534
1535 // ==================
1536
1537 struct TestNewType<T>(T);
1538
1539 fn main() {
1540 let variable = TestNewType(TestStruct);
1541 }
1542 "});
1543
1544 let uri = cx.buffer_lsp_url.clone();
1545 let new_type_label = "TestNewType";
1546 let struct_label = "TestStruct";
1547 let entire_hint_label = ": TestNewType<TestStruct>";
1548 let closure_uri = uri.clone();
1549 cx.lsp
1550 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1551 let task_uri = closure_uri.clone();
1552 async move {
1553 assert_eq!(params.text_document.uri, task_uri);
1554 Ok(Some(vec![lsp::InlayHint {
1555 position: hint_position,
1556 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1557 value: entire_hint_label.to_string(),
1558 ..Default::default()
1559 }]),
1560 kind: Some(lsp::InlayHintKind::TYPE),
1561 text_edits: None,
1562 tooltip: None,
1563 padding_left: Some(false),
1564 padding_right: Some(false),
1565 data: None,
1566 }]))
1567 }
1568 })
1569 .next()
1570 .await;
1571 cx.background_executor.run_until_parked();
1572 cx.update_editor(|editor, _, cx| {
1573 let expected_layers = vec![entire_hint_label.to_string()];
1574 assert_eq!(expected_layers, cached_hint_labels(editor));
1575 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1576 });
1577
1578 let inlay_range = cx
1579 .ranges(indoc! {"
1580 struct TestStruct;
1581
1582 // ==================
1583
1584 struct TestNewType<T>(T);
1585
1586 fn main() {
1587 let variable« »= TestNewType(TestStruct);
1588 }
1589 "})
1590 .first()
1591 .cloned()
1592 .unwrap();
1593 let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1594 let snapshot = editor.snapshot(window, cx);
1595 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1596 let next_valid = inlay_range.end.to_display_point(&snapshot);
1597 assert_eq!(previous_valid.row(), next_valid.row());
1598 assert!(previous_valid.column() < next_valid.column());
1599 let exact_unclipped = DisplayPoint::new(
1600 previous_valid.row(),
1601 previous_valid.column()
1602 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1603 as u32,
1604 );
1605 PointForPosition {
1606 previous_valid,
1607 next_valid,
1608 exact_unclipped,
1609 column_overshoot_after_line_end: 0,
1610 }
1611 });
1612 cx.update_editor(|editor, window, cx| {
1613 update_inlay_link_and_hover_points(
1614 &editor.snapshot(window, cx),
1615 new_type_hint_part_hover_position,
1616 editor,
1617 true,
1618 false,
1619 window,
1620 cx,
1621 );
1622 });
1623
1624 let resolve_closure_uri = uri.clone();
1625 cx.lsp
1626 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1627 move |mut hint_to_resolve, _| {
1628 let mut resolved_hint_positions = BTreeSet::new();
1629 let task_uri = resolve_closure_uri.clone();
1630 async move {
1631 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1632 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1633
1634 // `: TestNewType<TestStruct>`
1635 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1636 lsp::InlayHintLabelPart {
1637 value: ": ".to_string(),
1638 ..Default::default()
1639 },
1640 lsp::InlayHintLabelPart {
1641 value: new_type_label.to_string(),
1642 location: Some(lsp::Location {
1643 uri: task_uri.clone(),
1644 range: new_type_target_range,
1645 }),
1646 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1647 "A tooltip for `{new_type_label}`"
1648 ))),
1649 ..Default::default()
1650 },
1651 lsp::InlayHintLabelPart {
1652 value: "<".to_string(),
1653 ..Default::default()
1654 },
1655 lsp::InlayHintLabelPart {
1656 value: struct_label.to_string(),
1657 location: Some(lsp::Location {
1658 uri: task_uri,
1659 range: struct_target_range,
1660 }),
1661 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1662 lsp::MarkupContent {
1663 kind: lsp::MarkupKind::Markdown,
1664 value: format!("A tooltip for `{struct_label}`"),
1665 },
1666 )),
1667 ..Default::default()
1668 },
1669 lsp::InlayHintLabelPart {
1670 value: ">".to_string(),
1671 ..Default::default()
1672 },
1673 ]);
1674
1675 Ok(hint_to_resolve)
1676 }
1677 },
1678 )
1679 .next()
1680 .await;
1681 cx.background_executor.run_until_parked();
1682
1683 cx.update_editor(|editor, window, cx| {
1684 update_inlay_link_and_hover_points(
1685 &editor.snapshot(window, cx),
1686 new_type_hint_part_hover_position,
1687 editor,
1688 true,
1689 false,
1690 window,
1691 cx,
1692 );
1693 });
1694 cx.background_executor
1695 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1696 cx.background_executor.run_until_parked();
1697 cx.update_editor(|editor, _, cx| {
1698 let hover_state = &editor.hover_state;
1699 assert!(
1700 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1701 );
1702 let popover = hover_state.info_popovers.first().cloned().unwrap();
1703 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1704 assert_eq!(
1705 popover.symbol_range,
1706 RangeInEditor::Inlay(InlayHighlight {
1707 inlay: InlayId::Hint(0),
1708 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1709 range: ": ".len()..": ".len() + new_type_label.len(),
1710 }),
1711 "Popover range should match the new type label part"
1712 );
1713 assert_eq!(
1714 popover.get_rendered_text(cx),
1715 format!("A tooltip for {new_type_label}"),
1716 );
1717 });
1718
1719 let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1720 let snapshot = editor.snapshot(window, cx);
1721 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1722 let next_valid = inlay_range.end.to_display_point(&snapshot);
1723 assert_eq!(previous_valid.row(), next_valid.row());
1724 assert!(previous_valid.column() < next_valid.column());
1725 let exact_unclipped = DisplayPoint::new(
1726 previous_valid.row(),
1727 previous_valid.column()
1728 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1729 as u32,
1730 );
1731 PointForPosition {
1732 previous_valid,
1733 next_valid,
1734 exact_unclipped,
1735 column_overshoot_after_line_end: 0,
1736 }
1737 });
1738 cx.update_editor(|editor, window, cx| {
1739 update_inlay_link_and_hover_points(
1740 &editor.snapshot(window, cx),
1741 struct_hint_part_hover_position,
1742 editor,
1743 true,
1744 false,
1745 window,
1746 cx,
1747 );
1748 });
1749 cx.background_executor
1750 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1751 cx.background_executor.run_until_parked();
1752 cx.update_editor(|editor, _, cx| {
1753 let hover_state = &editor.hover_state;
1754 assert!(
1755 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1756 );
1757 let popover = hover_state.info_popovers.first().cloned().unwrap();
1758 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1759 assert_eq!(
1760 popover.symbol_range,
1761 RangeInEditor::Inlay(InlayHighlight {
1762 inlay: InlayId::Hint(0),
1763 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1764 range: ": ".len() + new_type_label.len() + "<".len()
1765 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1766 }),
1767 "Popover range should match the struct label part"
1768 );
1769 assert_eq!(
1770 popover.get_rendered_text(cx),
1771 format!("A tooltip for {struct_label}"),
1772 "Rendered markdown element should remove backticks from text"
1773 );
1774 });
1775 }
1776}