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