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