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