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