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