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