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