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
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 match event {
889 MarkdownEvent::Text(parsed) => rendered_text.push_str(parsed),
890 MarkdownEvent::Code => rendered_text.push_str(&text[range.clone()]),
891 _ => {}
892 }
893 }
894 }
895 rendered_text
896 }
897 }
898
899 #[gpui::test]
900 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
901 cx: &mut gpui::TestAppContext,
902 ) {
903 init_test(cx, |_| {});
904
905 let mut cx = EditorLspTestContext::new_rust(
906 lsp::ServerCapabilities {
907 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
908 completion_provider: Some(lsp::CompletionOptions {
909 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
910 resolve_provider: Some(true),
911 ..Default::default()
912 }),
913 ..Default::default()
914 },
915 cx,
916 )
917 .await;
918 let counter = Arc::new(AtomicUsize::new(0));
919 // Basic hover delays and then pops without moving the mouse
920 cx.set_state(indoc! {"
921 oneˇ
922 two
923 three
924 fn test() { println!(); }
925 "});
926
927 //prompt autocompletion menu
928 cx.simulate_keystroke(".");
929 handle_completion_request(
930 &mut cx,
931 indoc! {"
932 one.|<>
933 two
934 three
935 "},
936 vec!["first_completion", "second_completion"],
937 counter.clone(),
938 )
939 .await;
940 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
941 .await;
942 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
943
944 let hover_point = cx.display_point(indoc! {"
945 one.
946 two
947 three
948 fn test() { printˇln!(); }
949 "});
950 cx.update_editor(|editor, window, cx| {
951 let snapshot = editor.snapshot(window, cx);
952 let anchor = snapshot
953 .buffer_snapshot
954 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
955 hover_at(editor, Some(anchor), window, cx)
956 });
957 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
958
959 // After delay, hover should be visible.
960 let symbol_range = cx.lsp_range(indoc! {"
961 one.
962 two
963 three
964 fn test() { «println!»(); }
965 "});
966 let mut requests =
967 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
968 Ok(Some(lsp::Hover {
969 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
970 kind: lsp::MarkupKind::Markdown,
971 value: "some basic docs".to_string(),
972 }),
973 range: Some(symbol_range),
974 }))
975 });
976 cx.background_executor
977 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
978 requests.next().await;
979
980 cx.editor(|editor, _window, cx| {
981 assert!(editor.hover_state.visible());
982 assert_eq!(
983 editor.hover_state.info_popovers.len(),
984 1,
985 "Expected exactly one hover but got: {:?}",
986 editor.hover_state.info_popovers
987 );
988 let rendered_text = editor
989 .hover_state
990 .info_popovers
991 .first()
992 .unwrap()
993 .get_rendered_text(cx);
994 assert_eq!(rendered_text, "some basic docs".to_string())
995 });
996
997 // check that the completion menu is still visible and that there still has only been 1 completion request
998 cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
999 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1000
1001 //apply a completion and check it was successfully applied
1002 let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1003 editor.context_menu_next(&Default::default(), window, cx);
1004 editor
1005 .confirm_completion(&ConfirmCompletion::default(), window, cx)
1006 .unwrap()
1007 });
1008 cx.assert_editor_state(indoc! {"
1009 one.second_completionˇ
1010 two
1011 three
1012 fn test() { println!(); }
1013 "});
1014
1015 // check that the completion menu is no longer visible and that there still has only been 1 completion request
1016 cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1017 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1018
1019 //verify the information popover is still visible and unchanged
1020 cx.editor(|editor, _, cx| {
1021 assert!(editor.hover_state.visible());
1022 assert_eq!(
1023 editor.hover_state.info_popovers.len(),
1024 1,
1025 "Expected exactly one hover but got: {:?}",
1026 editor.hover_state.info_popovers
1027 );
1028 let rendered_text = editor
1029 .hover_state
1030 .info_popovers
1031 .first()
1032 .unwrap()
1033 .get_rendered_text(cx);
1034
1035 assert_eq!(rendered_text, "some basic docs".to_string())
1036 });
1037
1038 // Mouse moved with no hover response dismisses
1039 let hover_point = cx.display_point(indoc! {"
1040 one.second_completionˇ
1041 two
1042 three
1043 fn teˇst() { println!(); }
1044 "});
1045 let mut request = cx
1046 .lsp
1047 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1048 cx.update_editor(|editor, window, cx| {
1049 let snapshot = editor.snapshot(window, cx);
1050 let anchor = snapshot
1051 .buffer_snapshot
1052 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1053 hover_at(editor, Some(anchor), window, cx)
1054 });
1055 cx.background_executor
1056 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1057 request.next().await;
1058
1059 // verify that the information popover is no longer visible
1060 cx.editor(|editor, _, _| {
1061 assert!(!editor.hover_state.visible());
1062 });
1063 }
1064
1065 #[gpui::test]
1066 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1067 init_test(cx, |_| {});
1068
1069 let mut cx = EditorLspTestContext::new_rust(
1070 lsp::ServerCapabilities {
1071 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1072 ..Default::default()
1073 },
1074 cx,
1075 )
1076 .await;
1077
1078 // Basic hover delays and then pops without moving the mouse
1079 cx.set_state(indoc! {"
1080 fn ˇtest() { println!(); }
1081 "});
1082 let hover_point = cx.display_point(indoc! {"
1083 fn test() { printˇln!(); }
1084 "});
1085
1086 cx.update_editor(|editor, window, cx| {
1087 let snapshot = editor.snapshot(window, cx);
1088 let anchor = snapshot
1089 .buffer_snapshot
1090 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1091 hover_at(editor, Some(anchor), window, cx)
1092 });
1093 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1094
1095 // After delay, hover should be visible.
1096 let symbol_range = cx.lsp_range(indoc! {"
1097 fn test() { «println!»(); }
1098 "});
1099 let mut requests =
1100 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1101 Ok(Some(lsp::Hover {
1102 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1103 kind: lsp::MarkupKind::Markdown,
1104 value: "some basic docs".to_string(),
1105 }),
1106 range: Some(symbol_range),
1107 }))
1108 });
1109 cx.background_executor
1110 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1111 requests.next().await;
1112
1113 cx.editor(|editor, _, cx| {
1114 assert!(editor.hover_state.visible());
1115 assert_eq!(
1116 editor.hover_state.info_popovers.len(),
1117 1,
1118 "Expected exactly one hover but got: {:?}",
1119 editor.hover_state.info_popovers
1120 );
1121 let rendered_text = editor
1122 .hover_state
1123 .info_popovers
1124 .first()
1125 .unwrap()
1126 .get_rendered_text(cx);
1127
1128 assert_eq!(rendered_text, "some basic docs".to_string())
1129 });
1130
1131 // Mouse moved with no hover response dismisses
1132 let hover_point = cx.display_point(indoc! {"
1133 fn teˇst() { println!(); }
1134 "});
1135 let mut request = cx
1136 .lsp
1137 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1138 cx.update_editor(|editor, window, cx| {
1139 let snapshot = editor.snapshot(window, cx);
1140 let anchor = snapshot
1141 .buffer_snapshot
1142 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1143 hover_at(editor, Some(anchor), window, cx)
1144 });
1145 cx.background_executor
1146 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1147 request.next().await;
1148 cx.editor(|editor, _, _| {
1149 assert!(!editor.hover_state.visible());
1150 });
1151 }
1152
1153 #[gpui::test]
1154 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1155 init_test(cx, |_| {});
1156
1157 let mut cx = EditorLspTestContext::new_rust(
1158 lsp::ServerCapabilities {
1159 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1160 ..Default::default()
1161 },
1162 cx,
1163 )
1164 .await;
1165
1166 // Hover with keyboard has no delay
1167 cx.set_state(indoc! {"
1168 fˇn test() { println!(); }
1169 "});
1170 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1171 let symbol_range = cx.lsp_range(indoc! {"
1172 «fn» test() { println!(); }
1173 "});
1174
1175 cx.editor(|editor, _window, _cx| {
1176 assert!(!editor.hover_state.visible());
1177
1178 assert_eq!(
1179 editor.hover_state.info_popovers.len(),
1180 0,
1181 "Expected no hovers but got but got: {:?}",
1182 editor.hover_state.info_popovers
1183 );
1184 });
1185
1186 let mut requests =
1187 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1188 Ok(Some(lsp::Hover {
1189 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1190 kind: lsp::MarkupKind::Markdown,
1191 value: "some other basic docs".to_string(),
1192 }),
1193 range: Some(symbol_range),
1194 }))
1195 });
1196
1197 requests.next().await;
1198 cx.dispatch_action(Hover);
1199
1200 cx.condition(|editor, _| editor.hover_state.visible()).await;
1201 cx.editor(|editor, _, cx| {
1202 assert_eq!(
1203 editor.hover_state.info_popovers.len(),
1204 1,
1205 "Expected exactly one hover but got: {:?}",
1206 editor.hover_state.info_popovers
1207 );
1208
1209 let rendered_text = editor
1210 .hover_state
1211 .info_popovers
1212 .first()
1213 .unwrap()
1214 .get_rendered_text(cx);
1215
1216 assert_eq!(rendered_text, "some other basic docs".to_string())
1217 });
1218 }
1219
1220 #[gpui::test]
1221 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1222 init_test(cx, |_| {});
1223
1224 let mut cx = EditorLspTestContext::new_rust(
1225 lsp::ServerCapabilities {
1226 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1227 ..Default::default()
1228 },
1229 cx,
1230 )
1231 .await;
1232
1233 // Hover with keyboard has no delay
1234 cx.set_state(indoc! {"
1235 fˇn test() { println!(); }
1236 "});
1237 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1238 let symbol_range = cx.lsp_range(indoc! {"
1239 «fn» test() { println!(); }
1240 "});
1241 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1242 Ok(Some(lsp::Hover {
1243 contents: lsp::HoverContents::Array(vec![
1244 lsp::MarkedString::String("regular text for hover to show".to_string()),
1245 lsp::MarkedString::String("".to_string()),
1246 lsp::MarkedString::LanguageString(lsp::LanguageString {
1247 language: "Rust".to_string(),
1248 value: "".to_string(),
1249 }),
1250 ]),
1251 range: Some(symbol_range),
1252 }))
1253 })
1254 .next()
1255 .await;
1256 cx.dispatch_action(Hover);
1257
1258 cx.condition(|editor, _| editor.hover_state.visible()).await;
1259 cx.editor(|editor, _, cx| {
1260 assert_eq!(
1261 editor.hover_state.info_popovers.len(),
1262 1,
1263 "Expected exactly one hover but got: {:?}",
1264 editor.hover_state.info_popovers
1265 );
1266 let rendered_text = editor
1267 .hover_state
1268 .info_popovers
1269 .first()
1270 .unwrap()
1271 .get_rendered_text(cx);
1272
1273 assert_eq!(
1274 rendered_text,
1275 "regular text for hover to show".to_string(),
1276 "No empty string hovers should be shown"
1277 );
1278 });
1279 }
1280
1281 #[gpui::test]
1282 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1283 init_test(cx, |_| {});
1284
1285 let mut cx = EditorLspTestContext::new_rust(
1286 lsp::ServerCapabilities {
1287 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1288 ..Default::default()
1289 },
1290 cx,
1291 )
1292 .await;
1293
1294 // Hover with keyboard has no delay
1295 cx.set_state(indoc! {"
1296 fˇn test() { println!(); }
1297 "});
1298 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1299 let symbol_range = cx.lsp_range(indoc! {"
1300 «fn» test() { println!(); }
1301 "});
1302
1303 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1304 let markdown_string = format!("\n```rust\n{code_str}```");
1305
1306 let closure_markdown_string = markdown_string.clone();
1307 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1308 let future_markdown_string = closure_markdown_string.clone();
1309 async move {
1310 Ok(Some(lsp::Hover {
1311 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1312 kind: lsp::MarkupKind::Markdown,
1313 value: future_markdown_string,
1314 }),
1315 range: Some(symbol_range),
1316 }))
1317 }
1318 })
1319 .next()
1320 .await;
1321
1322 cx.dispatch_action(Hover);
1323
1324 cx.condition(|editor, _| editor.hover_state.visible()).await;
1325 cx.editor(|editor, _, cx| {
1326 assert_eq!(
1327 editor.hover_state.info_popovers.len(),
1328 1,
1329 "Expected exactly one hover but got: {:?}",
1330 editor.hover_state.info_popovers
1331 );
1332 let rendered_text = editor
1333 .hover_state
1334 .info_popovers
1335 .first()
1336 .unwrap()
1337 .get_rendered_text(cx);
1338
1339 assert_eq!(
1340 rendered_text, code_str,
1341 "Should not have extra line breaks at end of rendered hover"
1342 );
1343 });
1344 }
1345
1346 #[gpui::test]
1347 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1348 init_test(cx, |_| {});
1349
1350 let mut cx = EditorLspTestContext::new_rust(
1351 lsp::ServerCapabilities {
1352 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1353 ..Default::default()
1354 },
1355 cx,
1356 )
1357 .await;
1358
1359 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1360 // info popover once request completes
1361 cx.set_state(indoc! {"
1362 fn teˇst() { println!(); }
1363 "});
1364
1365 // Send diagnostic to client
1366 let range = cx.text_anchor_range(indoc! {"
1367 fn «test»() { println!(); }
1368 "});
1369 cx.update_buffer(|buffer, cx| {
1370 let snapshot = buffer.text_snapshot();
1371 let set = DiagnosticSet::from_sorted_entries(
1372 vec![DiagnosticEntry {
1373 range,
1374 diagnostic: Diagnostic {
1375 message: "A test diagnostic message.".to_string(),
1376 ..Default::default()
1377 },
1378 }],
1379 &snapshot,
1380 );
1381 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1382 });
1383
1384 // Hover pops diagnostic immediately
1385 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1386 cx.background_executor.run_until_parked();
1387
1388 cx.editor(|Editor { hover_state, .. }, _, _| {
1389 assert!(
1390 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1391 )
1392 });
1393
1394 // Info Popover shows after request responded to
1395 let range = cx.lsp_range(indoc! {"
1396 fn «test»() { println!(); }
1397 "});
1398 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1399 Ok(Some(lsp::Hover {
1400 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1401 kind: lsp::MarkupKind::Markdown,
1402 value: "some new docs".to_string(),
1403 }),
1404 range: Some(range),
1405 }))
1406 });
1407 cx.background_executor
1408 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1409
1410 cx.background_executor.run_until_parked();
1411 cx.editor(|Editor { hover_state, .. }, _, _| {
1412 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1413 });
1414 }
1415
1416 #[gpui::test]
1417 // https://github.com/zed-industries/zed/issues/15498
1418 async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1419 init_test(cx, |_| {});
1420
1421 let mut cx = EditorLspTestContext::new_rust(
1422 lsp::ServerCapabilities {
1423 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1424 ..Default::default()
1425 },
1426 cx,
1427 )
1428 .await;
1429
1430 cx.set_state(indoc! {"
1431 fn fuˇnc(abc def: i32) -> u32 {
1432 }
1433 "});
1434
1435 cx.lsp.handle_request::<lsp::request::HoverRequest, _, _>({
1436 |_, _| async move {
1437 Ok(Some(lsp::Hover {
1438 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1439 kind: lsp::MarkupKind::Markdown,
1440 value: indoc!(
1441 r#"
1442 ### function `errands_data_read`
1443
1444 ---
1445 → `char *`
1446 Function to read a file into a string
1447
1448 ---
1449 ```cpp
1450 static char *errands_data_read()
1451 ```
1452 "#
1453 )
1454 .to_string(),
1455 }),
1456 range: None,
1457 }))
1458 }
1459 });
1460 cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1461 cx.run_until_parked();
1462
1463 cx.update_editor(|editor, _, cx| {
1464 let popover = editor.hover_state.info_popovers.first().unwrap();
1465 let content = popover.get_rendered_text(cx);
1466
1467 assert!(content.contains("Function to read a file"));
1468 });
1469 }
1470
1471 #[gpui::test]
1472 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1473 init_test(cx, |settings| {
1474 settings.defaults.inlay_hints = Some(InlayHintSettings {
1475 enabled: true,
1476 edit_debounce_ms: 0,
1477 scroll_debounce_ms: 0,
1478 show_type_hints: true,
1479 show_parameter_hints: true,
1480 show_other_hints: true,
1481 show_background: false,
1482 })
1483 });
1484
1485 let mut cx = EditorLspTestContext::new_rust(
1486 lsp::ServerCapabilities {
1487 inlay_hint_provider: Some(lsp::OneOf::Right(
1488 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1489 resolve_provider: Some(true),
1490 ..Default::default()
1491 }),
1492 )),
1493 ..Default::default()
1494 },
1495 cx,
1496 )
1497 .await;
1498
1499 cx.set_state(indoc! {"
1500 struct TestStruct;
1501
1502 // ==================
1503
1504 struct TestNewType<T>(T);
1505
1506 fn main() {
1507 let variableˇ = TestNewType(TestStruct);
1508 }
1509 "});
1510
1511 let hint_start_offset = cx.ranges(indoc! {"
1512 struct TestStruct;
1513
1514 // ==================
1515
1516 struct TestNewType<T>(T);
1517
1518 fn main() {
1519 let variableˇ = TestNewType(TestStruct);
1520 }
1521 "})[0]
1522 .start;
1523 let hint_position = cx.to_lsp(hint_start_offset);
1524 let new_type_target_range = cx.lsp_range(indoc! {"
1525 struct TestStruct;
1526
1527 // ==================
1528
1529 struct «TestNewType»<T>(T);
1530
1531 fn main() {
1532 let variable = TestNewType(TestStruct);
1533 }
1534 "});
1535 let struct_target_range = cx.lsp_range(indoc! {"
1536 struct «TestStruct»;
1537
1538 // ==================
1539
1540 struct TestNewType<T>(T);
1541
1542 fn main() {
1543 let variable = TestNewType(TestStruct);
1544 }
1545 "});
1546
1547 let uri = cx.buffer_lsp_url.clone();
1548 let new_type_label = "TestNewType";
1549 let struct_label = "TestStruct";
1550 let entire_hint_label = ": TestNewType<TestStruct>";
1551 let closure_uri = uri.clone();
1552 cx.lsp
1553 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1554 let task_uri = closure_uri.clone();
1555 async move {
1556 assert_eq!(params.text_document.uri, task_uri);
1557 Ok(Some(vec![lsp::InlayHint {
1558 position: hint_position,
1559 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1560 value: entire_hint_label.to_string(),
1561 ..Default::default()
1562 }]),
1563 kind: Some(lsp::InlayHintKind::TYPE),
1564 text_edits: None,
1565 tooltip: None,
1566 padding_left: Some(false),
1567 padding_right: Some(false),
1568 data: None,
1569 }]))
1570 }
1571 })
1572 .next()
1573 .await;
1574 cx.background_executor.run_until_parked();
1575 cx.update_editor(|editor, _, cx| {
1576 let expected_layers = vec![entire_hint_label.to_string()];
1577 assert_eq!(expected_layers, cached_hint_labels(editor));
1578 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1579 });
1580
1581 let inlay_range = cx
1582 .ranges(indoc! {"
1583 struct TestStruct;
1584
1585 // ==================
1586
1587 struct TestNewType<T>(T);
1588
1589 fn main() {
1590 let variable« »= TestNewType(TestStruct);
1591 }
1592 "})
1593 .first()
1594 .cloned()
1595 .unwrap();
1596 let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1597 let snapshot = editor.snapshot(window, cx);
1598 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1599 let next_valid = inlay_range.end.to_display_point(&snapshot);
1600 assert_eq!(previous_valid.row(), next_valid.row());
1601 assert!(previous_valid.column() < next_valid.column());
1602 let exact_unclipped = DisplayPoint::new(
1603 previous_valid.row(),
1604 previous_valid.column()
1605 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1606 as u32,
1607 );
1608 PointForPosition {
1609 previous_valid,
1610 next_valid,
1611 exact_unclipped,
1612 column_overshoot_after_line_end: 0,
1613 }
1614 });
1615 cx.update_editor(|editor, window, cx| {
1616 update_inlay_link_and_hover_points(
1617 &editor.snapshot(window, cx),
1618 new_type_hint_part_hover_position,
1619 editor,
1620 true,
1621 false,
1622 window,
1623 cx,
1624 );
1625 });
1626
1627 let resolve_closure_uri = uri.clone();
1628 cx.lsp
1629 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1630 move |mut hint_to_resolve, _| {
1631 let mut resolved_hint_positions = BTreeSet::new();
1632 let task_uri = resolve_closure_uri.clone();
1633 async move {
1634 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1635 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1636
1637 // `: TestNewType<TestStruct>`
1638 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1639 lsp::InlayHintLabelPart {
1640 value: ": ".to_string(),
1641 ..Default::default()
1642 },
1643 lsp::InlayHintLabelPart {
1644 value: new_type_label.to_string(),
1645 location: Some(lsp::Location {
1646 uri: task_uri.clone(),
1647 range: new_type_target_range,
1648 }),
1649 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1650 "A tooltip for `{new_type_label}`"
1651 ))),
1652 ..Default::default()
1653 },
1654 lsp::InlayHintLabelPart {
1655 value: "<".to_string(),
1656 ..Default::default()
1657 },
1658 lsp::InlayHintLabelPart {
1659 value: struct_label.to_string(),
1660 location: Some(lsp::Location {
1661 uri: task_uri,
1662 range: struct_target_range,
1663 }),
1664 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1665 lsp::MarkupContent {
1666 kind: lsp::MarkupKind::Markdown,
1667 value: format!("A tooltip for `{struct_label}`"),
1668 },
1669 )),
1670 ..Default::default()
1671 },
1672 lsp::InlayHintLabelPart {
1673 value: ">".to_string(),
1674 ..Default::default()
1675 },
1676 ]);
1677
1678 Ok(hint_to_resolve)
1679 }
1680 },
1681 )
1682 .next()
1683 .await;
1684 cx.background_executor.run_until_parked();
1685
1686 cx.update_editor(|editor, window, cx| {
1687 update_inlay_link_and_hover_points(
1688 &editor.snapshot(window, cx),
1689 new_type_hint_part_hover_position,
1690 editor,
1691 true,
1692 false,
1693 window,
1694 cx,
1695 );
1696 });
1697 cx.background_executor
1698 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1699 cx.background_executor.run_until_parked();
1700 cx.update_editor(|editor, _, cx| {
1701 let hover_state = &editor.hover_state;
1702 assert!(
1703 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1704 );
1705 let popover = hover_state.info_popovers.first().cloned().unwrap();
1706 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1707 assert_eq!(
1708 popover.symbol_range,
1709 RangeInEditor::Inlay(InlayHighlight {
1710 inlay: InlayId::Hint(0),
1711 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1712 range: ": ".len()..": ".len() + new_type_label.len(),
1713 }),
1714 "Popover range should match the new type label part"
1715 );
1716 assert_eq!(
1717 popover.get_rendered_text(cx),
1718 format!("A tooltip for {new_type_label}"),
1719 );
1720 });
1721
1722 let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1723 let snapshot = editor.snapshot(window, cx);
1724 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1725 let next_valid = inlay_range.end.to_display_point(&snapshot);
1726 assert_eq!(previous_valid.row(), next_valid.row());
1727 assert!(previous_valid.column() < next_valid.column());
1728 let exact_unclipped = DisplayPoint::new(
1729 previous_valid.row(),
1730 previous_valid.column()
1731 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1732 as u32,
1733 );
1734 PointForPosition {
1735 previous_valid,
1736 next_valid,
1737 exact_unclipped,
1738 column_overshoot_after_line_end: 0,
1739 }
1740 });
1741 cx.update_editor(|editor, window, cx| {
1742 update_inlay_link_and_hover_points(
1743 &editor.snapshot(window, cx),
1744 struct_hint_part_hover_position,
1745 editor,
1746 true,
1747 false,
1748 window,
1749 cx,
1750 );
1751 });
1752 cx.background_executor
1753 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1754 cx.background_executor.run_until_parked();
1755 cx.update_editor(|editor, _, cx| {
1756 let hover_state = &editor.hover_state;
1757 assert!(
1758 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1759 );
1760 let popover = hover_state.info_popovers.first().cloned().unwrap();
1761 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1762 assert_eq!(
1763 popover.symbol_range,
1764 RangeInEditor::Inlay(InlayHighlight {
1765 inlay: InlayId::Hint(0),
1766 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1767 range: ": ".len() + new_type_label.len() + "<".len()
1768 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1769 }),
1770 "Popover range should match the struct label part"
1771 );
1772 assert_eq!(
1773 popover.get_rendered_text(cx),
1774 format!("A tooltip for {struct_label}"),
1775 "Rendered markdown element should remove backticks from text"
1776 );
1777 });
1778 }
1779}