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