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