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