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