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