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