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