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 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) -> Option<()> {
199 if editor.pending_rename.is_some() {
200 return None;
201 }
202
203 let snapshot = editor.snapshot(cx);
204
205 let (buffer, buffer_position) = editor
206 .buffer
207 .read(cx)
208 .text_anchor_for_position(anchor, cx)?;
209
210 let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
211
212 let language_registry = editor.project.as_ref()?.read(cx).languages().clone();
213 let provider = editor.semantics_provider.clone()?;
214
215 if !ignore_timeout {
216 if same_info_hover(editor, &snapshot, anchor)
217 || same_diagnostic_hover(editor, &snapshot, anchor)
218 || editor.hover_state.diagnostic_popover.is_some()
219 {
220 // Hover triggered from same location as last time. Don't show again.
221 return None;
222 } else {
223 hide_hover(editor, cx);
224 }
225 }
226
227 // Don't request again if the location is the same as the previous request
228 if let Some(triggered_from) = &editor.hover_state.triggered_from {
229 if triggered_from
230 .cmp(&anchor, &snapshot.buffer_snapshot)
231 .is_eq()
232 {
233 return None;
234 }
235 }
236
237 let task = cx.spawn(|this, mut cx| {
238 async move {
239 // If we need to delay, delay a set amount initially before making the lsp request
240 let delay = if ignore_timeout {
241 None
242 } else {
243 // Construct delay task to wait for later
244 let total_delay = Some(
245 cx.background_executor()
246 .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
247 );
248
249 cx.background_executor()
250 .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
251 .await;
252 total_delay
253 };
254
255 let hover_request = cx.update(|cx| provider.hover(&buffer, buffer_position, cx))?;
256
257 if let Some(delay) = delay {
258 delay.await;
259 }
260
261 // If there's a diagnostic, assign it on the hover state and notify
262 let local_diagnostic = snapshot
263 .buffer_snapshot
264 .diagnostics_in_range::<_, usize>(anchor..anchor, false)
265 // Find the entry with the most specific range
266 .min_by_key(|entry| entry.range.end - entry.range.start)
267 .map(|entry| DiagnosticEntry {
268 diagnostic: entry.diagnostic,
269 range: entry.range.to_anchors(&snapshot.buffer_snapshot),
270 });
271
272 // Pull the primary diagnostic out so we can jump to it if the popover is clicked
273 let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
274 snapshot
275 .buffer_snapshot
276 .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
277 .find(|diagnostic| diagnostic.diagnostic.is_primary)
278 .map(|entry| DiagnosticEntry {
279 diagnostic: entry.diagnostic,
280 range: entry.range.to_anchors(&snapshot.buffer_snapshot),
281 })
282 });
283
284 let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
285 let text = match local_diagnostic.diagnostic.source {
286 Some(ref source) => {
287 format!("{source}: {}", local_diagnostic.diagnostic.message)
288 }
289 None => local_diagnostic.diagnostic.message.clone(),
290 };
291
292 let mut border_color: Option<Hsla> = None;
293 let mut background_color: Option<Hsla> = None;
294
295 let parsed_content = cx
296 .new_view(|cx| {
297 let status_colors = cx.theme().status();
298
299 match local_diagnostic.diagnostic.severity {
300 DiagnosticSeverity::ERROR => {
301 background_color = Some(status_colors.error_background);
302 border_color = Some(status_colors.error_border);
303 }
304 DiagnosticSeverity::WARNING => {
305 background_color = Some(status_colors.warning_background);
306 border_color = Some(status_colors.warning_border);
307 }
308 DiagnosticSeverity::INFORMATION => {
309 background_color = Some(status_colors.info_background);
310 border_color = Some(status_colors.info_border);
311 }
312 DiagnosticSeverity::HINT => {
313 background_color = Some(status_colors.hint_background);
314 border_color = Some(status_colors.hint_border);
315 }
316 _ => {
317 background_color = Some(status_colors.ignored_background);
318 border_color = Some(status_colors.ignored_border);
319 }
320 };
321 let settings = ThemeSettings::get_global(cx);
322 let mut base_text_style = cx.text_style();
323 base_text_style.refine(&TextStyleRefinement {
324 font_family: Some(settings.ui_font.family.clone()),
325 font_size: Some(settings.ui_font_size.into()),
326 color: Some(cx.theme().colors().editor_foreground),
327 background_color: Some(gpui::transparent_black()),
328
329 ..Default::default()
330 });
331 let markdown_style = MarkdownStyle {
332 base_text_style,
333 selection_background_color: { cx.theme().players().local().selection },
334 link: TextStyleRefinement {
335 underline: Some(gpui::UnderlineStyle {
336 thickness: px(1.),
337 color: Some(cx.theme().colors().editor_foreground),
338 wavy: false,
339 }),
340 ..Default::default()
341 },
342 ..Default::default()
343 };
344 Markdown::new_text(text, markdown_style.clone(), None, cx, None)
345 })
346 .ok();
347
348 Some(DiagnosticPopover {
349 local_diagnostic,
350 primary_diagnostic,
351 parsed_content,
352 border_color,
353 background_color,
354 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
355 anchor: Some(anchor),
356 })
357 } else {
358 None
359 };
360
361 this.update(&mut cx, |this, _| {
362 this.hover_state.diagnostic_popover = diagnostic_popover;
363 })?;
364
365 let hovers_response = if let Some(hover_request) = hover_request {
366 hover_request.await
367 } else {
368 Vec::new()
369 };
370 let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
371 let mut hover_highlights = Vec::with_capacity(hovers_response.len());
372 let mut info_popovers = Vec::with_capacity(hovers_response.len());
373 let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
374
375 for hover_result in hovers_response {
376 // Create symbol range of anchors for highlighting and filtering of future requests.
377 let range = hover_result
378 .range
379 .and_then(|range| {
380 let start = snapshot
381 .buffer_snapshot
382 .anchor_in_excerpt(excerpt_id, range.start)?;
383 let end = snapshot
384 .buffer_snapshot
385 .anchor_in_excerpt(excerpt_id, range.end)?;
386
387 Some(start..end)
388 })
389 .or_else(|| {
390 let snapshot = &snapshot.buffer_snapshot;
391 let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?;
392 Some(
393 snapshot.anchor_before(offset_range.start)
394 ..snapshot.anchor_after(offset_range.end),
395 )
396 })
397 .unwrap_or_else(|| anchor..anchor);
398
399 let blocks = hover_result.contents;
400 let language = hover_result.language;
401 let parsed_content =
402 parse_blocks(&blocks, &language_registry, language, &mut cx).await;
403 info_popover_tasks.push((
404 range.clone(),
405 InfoPopover {
406 symbol_range: RangeInEditor::Text(range),
407 parsed_content,
408 scroll_handle: ScrollHandle::new(),
409 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
410 anchor: Some(anchor),
411 },
412 ));
413 }
414 for (highlight_range, info_popover) in info_popover_tasks {
415 hover_highlights.push(highlight_range);
416 info_popovers.push(info_popover);
417 }
418
419 this.update(&mut cx, |editor, cx| {
420 if hover_highlights.is_empty() {
421 editor.clear_background_highlights::<HoverState>(cx);
422 } else {
423 // Highlight the selected symbol using a background highlight
424 editor.highlight_background::<HoverState>(
425 &hover_highlights,
426 |theme| theme.element_hover, // todo update theme
427 cx,
428 );
429 }
430
431 editor.hover_state.info_popovers = info_popovers;
432 cx.notify();
433 cx.refresh();
434 })?;
435
436 anyhow::Ok(())
437 }
438 .log_err()
439 });
440
441 editor.hover_state.info_task = Some(task);
442 None
443}
444
445fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
446 editor
447 .hover_state
448 .info_popovers
449 .iter()
450 .any(|InfoPopover { symbol_range, .. }| {
451 symbol_range
452 .as_text_range()
453 .map(|range| {
454 let hover_range = range.to_offset(&snapshot.buffer_snapshot);
455 let offset = anchor.to_offset(&snapshot.buffer_snapshot);
456 // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
457 // use an inclusive range here to check if we should dismiss the popover
458 (hover_range.start..=hover_range.end).contains(&offset)
459 })
460 .unwrap_or(false)
461 })
462}
463
464fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
465 editor
466 .hover_state
467 .diagnostic_popover
468 .as_ref()
469 .map(|diagnostic| {
470 let hover_range = diagnostic
471 .local_diagnostic
472 .range
473 .to_offset(&snapshot.buffer_snapshot);
474 let offset = anchor.to_offset(&snapshot.buffer_snapshot);
475
476 // Here we do basically the same as in `same_info_hover`, see comment there for an explanation
477 (hover_range.start..=hover_range.end).contains(&offset)
478 })
479 .unwrap_or(false)
480}
481
482async fn parse_blocks(
483 blocks: &[HoverBlock],
484 language_registry: &Arc<LanguageRegistry>,
485 language: Option<Arc<Language>>,
486 cx: &mut AsyncWindowContext,
487) -> Option<View<Markdown>> {
488 let fallback_language_name = if let Some(ref l) = language {
489 let l = Arc::clone(l);
490 Some(l.lsp_id().clone())
491 } else {
492 None
493 };
494
495 let combined_text = blocks
496 .iter()
497 .map(|block| match &block.kind {
498 project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
499 Cow::Borrowed(block.text.trim())
500 }
501 project::HoverBlockKind::Code { language } => {
502 Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
503 }
504 })
505 .join("\n\n");
506
507 let rendered_block = cx
508 .new_view(|cx| {
509 let settings = ThemeSettings::get_global(cx);
510 let ui_font_family = settings.ui_font.family.clone();
511 let buffer_font_family = settings.buffer_font.family.clone();
512
513 let mut base_text_style = cx.text_style();
514 base_text_style.refine(&TextStyleRefinement {
515 font_family: Some(ui_font_family.clone()),
516 color: Some(cx.theme().colors().editor_foreground),
517 ..Default::default()
518 });
519
520 let markdown_style = MarkdownStyle {
521 base_text_style,
522 code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
523 inline_code: TextStyleRefinement {
524 background_color: Some(cx.theme().colors().background),
525 font_family: Some(buffer_font_family),
526 ..Default::default()
527 },
528 rule_color: cx.theme().colors().border,
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 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 .overflow_y_scroll()
729 .max_w(max_size.width)
730 .elevation_2_borderless(cx)
731 // Don't draw the background color if the theme
732 // allows transparent surfaces.
733 .when(window_is_transparent(cx), |this| {
734 this.bg(gpui::transparent_black())
735 })
736 // Prevent a mouse move on the popover from being propagated to the editor,
737 // because that would dismiss the popover.
738 .on_mouse_move(|_, cx| cx.stop_propagation())
739 // Prevent a mouse down on the popover from being propagated to the editor,
740 // because that would move the cursor.
741 .on_mouse_down(MouseButton::Left, move |_, cx| {
742 let mut keyboard_grace = keyboard_grace.borrow_mut();
743 *keyboard_grace = false;
744 cx.stop_propagation();
745 })
746 .child(markdown_div);
747
748 diagnostic_div.into_any_element()
749 }
750
751 pub fn activation_info(&self) -> (usize, Anchor) {
752 let entry = self
753 .primary_diagnostic
754 .as_ref()
755 .unwrap_or(&self.local_diagnostic);
756
757 (entry.diagnostic.group_id, entry.range.start)
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use crate::{
765 actions::ConfirmCompletion,
766 editor_tests::{handle_completion_request, init_test},
767 hover_links::update_inlay_link_and_hover_points,
768 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
769 test::editor_lsp_test_context::EditorLspTestContext,
770 InlayId, PointForPosition,
771 };
772 use collections::BTreeSet;
773 use indoc::indoc;
774 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
775 use lsp::LanguageServerId;
776 use markdown::parser::MarkdownEvent;
777 use smol::stream::StreamExt;
778 use std::sync::atomic;
779 use std::sync::atomic::AtomicUsize;
780 use text::Bias;
781
782 impl InfoPopover {
783 fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
784 let mut rendered_text = String::new();
785 if let Some(parsed_content) = self.parsed_content.clone() {
786 let markdown = parsed_content.read(cx);
787 let text = markdown.parsed_markdown().source().to_string();
788 let data = markdown.parsed_markdown().events();
789 let slice = data;
790
791 for (range, event) in slice.iter() {
792 if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
793 rendered_text.push_str(&text[range.clone()])
794 }
795 }
796 }
797 rendered_text
798 }
799 }
800
801 #[gpui::test]
802 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
803 cx: &mut gpui::TestAppContext,
804 ) {
805 init_test(cx, |_| {});
806 const HOVER_DELAY_MILLIS: u64 = 350;
807
808 let mut cx = EditorLspTestContext::new_rust(
809 lsp::ServerCapabilities {
810 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
811 completion_provider: Some(lsp::CompletionOptions {
812 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
813 resolve_provider: Some(true),
814 ..Default::default()
815 }),
816 ..Default::default()
817 },
818 cx,
819 )
820 .await;
821 let counter = Arc::new(AtomicUsize::new(0));
822 // Basic hover delays and then pops without moving the mouse
823 cx.set_state(indoc! {"
824 oneˇ
825 two
826 three
827 fn test() { println!(); }
828 "});
829
830 //prompt autocompletion menu
831 cx.simulate_keystroke(".");
832 handle_completion_request(
833 &mut cx,
834 indoc! {"
835 one.|<>
836 two
837 three
838 "},
839 vec!["first_completion", "second_completion"],
840 counter.clone(),
841 )
842 .await;
843 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
844 .await;
845 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
846
847 let hover_point = cx.display_point(indoc! {"
848 one.
849 two
850 three
851 fn test() { printˇln!(); }
852 "});
853 cx.update_editor(|editor, cx| {
854 let snapshot = editor.snapshot(cx);
855 let anchor = snapshot
856 .buffer_snapshot
857 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
858 hover_at(editor, Some(anchor), cx)
859 });
860 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
861
862 // After delay, hover should be visible.
863 let symbol_range = cx.lsp_range(indoc! {"
864 one.
865 two
866 three
867 fn test() { «println!»(); }
868 "});
869 let mut requests =
870 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
871 Ok(Some(lsp::Hover {
872 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
873 kind: lsp::MarkupKind::Markdown,
874 value: "some basic docs".to_string(),
875 }),
876 range: Some(symbol_range),
877 }))
878 });
879 cx.background_executor
880 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
881 requests.next().await;
882
883 cx.editor(|editor, cx| {
884 assert!(editor.hover_state.visible());
885 assert_eq!(
886 editor.hover_state.info_popovers.len(),
887 1,
888 "Expected exactly one hover but got: {:?}",
889 editor.hover_state.info_popovers
890 );
891 let rendered_text = editor
892 .hover_state
893 .info_popovers
894 .first()
895 .unwrap()
896 .get_rendered_text(cx);
897 assert_eq!(rendered_text, "some basic docs".to_string())
898 });
899
900 // check that the completion menu is still visible and that there still has only been 1 completion request
901 cx.editor(|editor, _| assert!(editor.context_menu_visible()));
902 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
903
904 //apply a completion and check it was successfully applied
905 let _apply_additional_edits = cx.update_editor(|editor, cx| {
906 editor.context_menu_next(&Default::default(), cx);
907 editor
908 .confirm_completion(&ConfirmCompletion::default(), cx)
909 .unwrap()
910 });
911 cx.assert_editor_state(indoc! {"
912 one.second_completionˇ
913 two
914 three
915 fn test() { println!(); }
916 "});
917
918 // check that the completion menu is no longer visible and that there still has only been 1 completion request
919 cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
920 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
921
922 //verify the information popover is still visible and unchanged
923 cx.editor(|editor, cx| {
924 assert!(editor.hover_state.visible());
925 assert_eq!(
926 editor.hover_state.info_popovers.len(),
927 1,
928 "Expected exactly one hover but got: {:?}",
929 editor.hover_state.info_popovers
930 );
931 let rendered_text = editor
932 .hover_state
933 .info_popovers
934 .first()
935 .unwrap()
936 .get_rendered_text(cx);
937
938 assert_eq!(rendered_text, "some basic docs".to_string())
939 });
940
941 // Mouse moved with no hover response dismisses
942 let hover_point = cx.display_point(indoc! {"
943 one.second_completionˇ
944 two
945 three
946 fn teˇst() { println!(); }
947 "});
948 let mut request = cx
949 .lsp
950 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
951 cx.update_editor(|editor, cx| {
952 let snapshot = editor.snapshot(cx);
953 let anchor = snapshot
954 .buffer_snapshot
955 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
956 hover_at(editor, Some(anchor), cx)
957 });
958 cx.background_executor
959 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
960 request.next().await;
961
962 // verify that the information popover is no longer visible
963 cx.editor(|editor, _| {
964 assert!(!editor.hover_state.visible());
965 });
966 }
967
968 #[gpui::test]
969 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
970 init_test(cx, |_| {});
971
972 let mut cx = EditorLspTestContext::new_rust(
973 lsp::ServerCapabilities {
974 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
975 ..Default::default()
976 },
977 cx,
978 )
979 .await;
980
981 // Basic hover delays and then pops without moving the mouse
982 cx.set_state(indoc! {"
983 fn ˇtest() { println!(); }
984 "});
985 let hover_point = cx.display_point(indoc! {"
986 fn test() { printˇln!(); }
987 "});
988
989 cx.update_editor(|editor, cx| {
990 let snapshot = editor.snapshot(cx);
991 let anchor = snapshot
992 .buffer_snapshot
993 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
994 hover_at(editor, Some(anchor), cx)
995 });
996 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
997
998 // After delay, hover should be visible.
999 let symbol_range = cx.lsp_range(indoc! {"
1000 fn test() { «println!»(); }
1001 "});
1002 let mut requests =
1003 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1004 Ok(Some(lsp::Hover {
1005 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1006 kind: lsp::MarkupKind::Markdown,
1007 value: "some basic docs".to_string(),
1008 }),
1009 range: Some(symbol_range),
1010 }))
1011 });
1012 cx.background_executor
1013 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1014 requests.next().await;
1015
1016 cx.editor(|editor, cx| {
1017 assert!(editor.hover_state.visible());
1018 assert_eq!(
1019 editor.hover_state.info_popovers.len(),
1020 1,
1021 "Expected exactly one hover but got: {:?}",
1022 editor.hover_state.info_popovers
1023 );
1024 let rendered_text = editor
1025 .hover_state
1026 .info_popovers
1027 .first()
1028 .unwrap()
1029 .get_rendered_text(cx);
1030
1031 assert_eq!(rendered_text, "some basic docs".to_string())
1032 });
1033
1034 // Mouse moved with no hover response dismisses
1035 let hover_point = cx.display_point(indoc! {"
1036 fn teˇst() { println!(); }
1037 "});
1038 let mut request = cx
1039 .lsp
1040 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1041 cx.update_editor(|editor, cx| {
1042 let snapshot = editor.snapshot(cx);
1043 let anchor = snapshot
1044 .buffer_snapshot
1045 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1046 hover_at(editor, Some(anchor), cx)
1047 });
1048 cx.background_executor
1049 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1050 request.next().await;
1051 cx.editor(|editor, _| {
1052 assert!(!editor.hover_state.visible());
1053 });
1054 }
1055
1056 #[gpui::test]
1057 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1058 init_test(cx, |_| {});
1059
1060 let mut cx = EditorLspTestContext::new_rust(
1061 lsp::ServerCapabilities {
1062 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1063 ..Default::default()
1064 },
1065 cx,
1066 )
1067 .await;
1068
1069 // Hover with keyboard has no delay
1070 cx.set_state(indoc! {"
1071 fˇn test() { println!(); }
1072 "});
1073 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1074 let symbol_range = cx.lsp_range(indoc! {"
1075 «fn» test() { println!(); }
1076 "});
1077
1078 cx.editor(|editor, _cx| {
1079 assert!(!editor.hover_state.visible());
1080
1081 assert_eq!(
1082 editor.hover_state.info_popovers.len(),
1083 0,
1084 "Expected no hovers but got but got: {:?}",
1085 editor.hover_state.info_popovers
1086 );
1087 });
1088
1089 let mut requests =
1090 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1091 Ok(Some(lsp::Hover {
1092 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1093 kind: lsp::MarkupKind::Markdown,
1094 value: "some other basic docs".to_string(),
1095 }),
1096 range: Some(symbol_range),
1097 }))
1098 });
1099
1100 requests.next().await;
1101 cx.dispatch_action(Hover);
1102
1103 cx.condition(|editor, _| editor.hover_state.visible()).await;
1104 cx.editor(|editor, cx| {
1105 assert_eq!(
1106 editor.hover_state.info_popovers.len(),
1107 1,
1108 "Expected exactly one hover but got: {:?}",
1109 editor.hover_state.info_popovers
1110 );
1111
1112 let rendered_text = editor
1113 .hover_state
1114 .info_popovers
1115 .first()
1116 .unwrap()
1117 .get_rendered_text(cx);
1118
1119 assert_eq!(rendered_text, "some other basic docs".to_string())
1120 });
1121 }
1122
1123 #[gpui::test]
1124 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1125 init_test(cx, |_| {});
1126
1127 let mut cx = EditorLspTestContext::new_rust(
1128 lsp::ServerCapabilities {
1129 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1130 ..Default::default()
1131 },
1132 cx,
1133 )
1134 .await;
1135
1136 // Hover with keyboard has no delay
1137 cx.set_state(indoc! {"
1138 fˇn test() { println!(); }
1139 "});
1140 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1141 let symbol_range = cx.lsp_range(indoc! {"
1142 «fn» test() { println!(); }
1143 "});
1144 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1145 Ok(Some(lsp::Hover {
1146 contents: lsp::HoverContents::Array(vec![
1147 lsp::MarkedString::String("regular text for hover to show".to_string()),
1148 lsp::MarkedString::String("".to_string()),
1149 lsp::MarkedString::LanguageString(lsp::LanguageString {
1150 language: "Rust".to_string(),
1151 value: "".to_string(),
1152 }),
1153 ]),
1154 range: Some(symbol_range),
1155 }))
1156 })
1157 .next()
1158 .await;
1159 cx.dispatch_action(Hover);
1160
1161 cx.condition(|editor, _| editor.hover_state.visible()).await;
1162 cx.editor(|editor, cx| {
1163 assert_eq!(
1164 editor.hover_state.info_popovers.len(),
1165 1,
1166 "Expected exactly one hover but got: {:?}",
1167 editor.hover_state.info_popovers
1168 );
1169 let rendered_text = editor
1170 .hover_state
1171 .info_popovers
1172 .first()
1173 .unwrap()
1174 .get_rendered_text(cx);
1175
1176 assert_eq!(
1177 rendered_text,
1178 "regular text for hover to show".to_string(),
1179 "No empty string hovers should be shown"
1180 );
1181 });
1182 }
1183
1184 #[gpui::test]
1185 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1186 init_test(cx, |_| {});
1187
1188 let mut cx = EditorLspTestContext::new_rust(
1189 lsp::ServerCapabilities {
1190 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1191 ..Default::default()
1192 },
1193 cx,
1194 )
1195 .await;
1196
1197 // Hover with keyboard has no delay
1198 cx.set_state(indoc! {"
1199 fˇn test() { println!(); }
1200 "});
1201 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1202 let symbol_range = cx.lsp_range(indoc! {"
1203 «fn» test() { println!(); }
1204 "});
1205
1206 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1207 let markdown_string = format!("\n```rust\n{code_str}```");
1208
1209 let closure_markdown_string = markdown_string.clone();
1210 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1211 let future_markdown_string = closure_markdown_string.clone();
1212 async move {
1213 Ok(Some(lsp::Hover {
1214 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1215 kind: lsp::MarkupKind::Markdown,
1216 value: future_markdown_string,
1217 }),
1218 range: Some(symbol_range),
1219 }))
1220 }
1221 })
1222 .next()
1223 .await;
1224
1225 cx.dispatch_action(Hover);
1226
1227 cx.condition(|editor, _| editor.hover_state.visible()).await;
1228 cx.editor(|editor, cx| {
1229 assert_eq!(
1230 editor.hover_state.info_popovers.len(),
1231 1,
1232 "Expected exactly one hover but got: {:?}",
1233 editor.hover_state.info_popovers
1234 );
1235 let rendered_text = editor
1236 .hover_state
1237 .info_popovers
1238 .first()
1239 .unwrap()
1240 .get_rendered_text(cx);
1241
1242 assert_eq!(
1243 rendered_text, code_str,
1244 "Should not have extra line breaks at end of rendered hover"
1245 );
1246 });
1247 }
1248
1249 #[gpui::test]
1250 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1251 init_test(cx, |_| {});
1252
1253 let mut cx = EditorLspTestContext::new_rust(
1254 lsp::ServerCapabilities {
1255 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1256 ..Default::default()
1257 },
1258 cx,
1259 )
1260 .await;
1261
1262 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1263 // info popover once request completes
1264 cx.set_state(indoc! {"
1265 fn teˇst() { println!(); }
1266 "});
1267
1268 // Send diagnostic to client
1269 let range = cx.text_anchor_range(indoc! {"
1270 fn «test»() { println!(); }
1271 "});
1272 cx.update_buffer(|buffer, cx| {
1273 let snapshot = buffer.text_snapshot();
1274 let set = DiagnosticSet::from_sorted_entries(
1275 vec![DiagnosticEntry {
1276 range,
1277 diagnostic: Diagnostic {
1278 message: "A test diagnostic message.".to_string(),
1279 ..Default::default()
1280 },
1281 }],
1282 &snapshot,
1283 );
1284 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1285 });
1286
1287 // Hover pops diagnostic immediately
1288 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1289 cx.background_executor.run_until_parked();
1290
1291 cx.editor(|Editor { hover_state, .. }, _| {
1292 assert!(
1293 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1294 )
1295 });
1296
1297 // Info Popover shows after request responded to
1298 let range = cx.lsp_range(indoc! {"
1299 fn «test»() { println!(); }
1300 "});
1301 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1302 Ok(Some(lsp::Hover {
1303 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1304 kind: lsp::MarkupKind::Markdown,
1305 value: "some new docs".to_string(),
1306 }),
1307 range: Some(range),
1308 }))
1309 });
1310 cx.background_executor
1311 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1312
1313 cx.background_executor.run_until_parked();
1314 cx.editor(|Editor { hover_state, .. }, _| {
1315 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1316 });
1317 }
1318
1319 #[gpui::test]
1320 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1321 init_test(cx, |settings| {
1322 settings.defaults.inlay_hints = Some(InlayHintSettings {
1323 enabled: true,
1324 edit_debounce_ms: 0,
1325 scroll_debounce_ms: 0,
1326 show_type_hints: true,
1327 show_parameter_hints: true,
1328 show_other_hints: true,
1329 show_background: false,
1330 })
1331 });
1332
1333 let mut cx = EditorLspTestContext::new_rust(
1334 lsp::ServerCapabilities {
1335 inlay_hint_provider: Some(lsp::OneOf::Right(
1336 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1337 resolve_provider: Some(true),
1338 ..Default::default()
1339 }),
1340 )),
1341 ..Default::default()
1342 },
1343 cx,
1344 )
1345 .await;
1346
1347 cx.set_state(indoc! {"
1348 struct TestStruct;
1349
1350 // ==================
1351
1352 struct TestNewType<T>(T);
1353
1354 fn main() {
1355 let variableˇ = TestNewType(TestStruct);
1356 }
1357 "});
1358
1359 let hint_start_offset = cx.ranges(indoc! {"
1360 struct TestStruct;
1361
1362 // ==================
1363
1364 struct TestNewType<T>(T);
1365
1366 fn main() {
1367 let variableˇ = TestNewType(TestStruct);
1368 }
1369 "})[0]
1370 .start;
1371 let hint_position = cx.to_lsp(hint_start_offset);
1372 let new_type_target_range = cx.lsp_range(indoc! {"
1373 struct TestStruct;
1374
1375 // ==================
1376
1377 struct «TestNewType»<T>(T);
1378
1379 fn main() {
1380 let variable = TestNewType(TestStruct);
1381 }
1382 "});
1383 let struct_target_range = cx.lsp_range(indoc! {"
1384 struct «TestStruct»;
1385
1386 // ==================
1387
1388 struct TestNewType<T>(T);
1389
1390 fn main() {
1391 let variable = TestNewType(TestStruct);
1392 }
1393 "});
1394
1395 let uri = cx.buffer_lsp_url.clone();
1396 let new_type_label = "TestNewType";
1397 let struct_label = "TestStruct";
1398 let entire_hint_label = ": TestNewType<TestStruct>";
1399 let closure_uri = uri.clone();
1400 cx.lsp
1401 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1402 let task_uri = closure_uri.clone();
1403 async move {
1404 assert_eq!(params.text_document.uri, task_uri);
1405 Ok(Some(vec![lsp::InlayHint {
1406 position: hint_position,
1407 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1408 value: entire_hint_label.to_string(),
1409 ..Default::default()
1410 }]),
1411 kind: Some(lsp::InlayHintKind::TYPE),
1412 text_edits: None,
1413 tooltip: None,
1414 padding_left: Some(false),
1415 padding_right: Some(false),
1416 data: None,
1417 }]))
1418 }
1419 })
1420 .next()
1421 .await;
1422 cx.background_executor.run_until_parked();
1423 cx.update_editor(|editor, cx| {
1424 let expected_layers = vec![entire_hint_label.to_string()];
1425 assert_eq!(expected_layers, cached_hint_labels(editor));
1426 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1427 });
1428
1429 let inlay_range = cx
1430 .ranges(indoc! {"
1431 struct TestStruct;
1432
1433 // ==================
1434
1435 struct TestNewType<T>(T);
1436
1437 fn main() {
1438 let variable« »= TestNewType(TestStruct);
1439 }
1440 "})
1441 .first()
1442 .cloned()
1443 .unwrap();
1444 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1445 let snapshot = editor.snapshot(cx);
1446 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1447 let next_valid = inlay_range.end.to_display_point(&snapshot);
1448 assert_eq!(previous_valid.row(), next_valid.row());
1449 assert!(previous_valid.column() < next_valid.column());
1450 let exact_unclipped = DisplayPoint::new(
1451 previous_valid.row(),
1452 previous_valid.column()
1453 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1454 as u32,
1455 );
1456 PointForPosition {
1457 previous_valid,
1458 next_valid,
1459 exact_unclipped,
1460 column_overshoot_after_line_end: 0,
1461 }
1462 });
1463 cx.update_editor(|editor, cx| {
1464 update_inlay_link_and_hover_points(
1465 &editor.snapshot(cx),
1466 new_type_hint_part_hover_position,
1467 editor,
1468 true,
1469 false,
1470 cx,
1471 );
1472 });
1473
1474 let resolve_closure_uri = uri.clone();
1475 cx.lsp
1476 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1477 move |mut hint_to_resolve, _| {
1478 let mut resolved_hint_positions = BTreeSet::new();
1479 let task_uri = resolve_closure_uri.clone();
1480 async move {
1481 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1482 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1483
1484 // `: TestNewType<TestStruct>`
1485 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1486 lsp::InlayHintLabelPart {
1487 value: ": ".to_string(),
1488 ..Default::default()
1489 },
1490 lsp::InlayHintLabelPart {
1491 value: new_type_label.to_string(),
1492 location: Some(lsp::Location {
1493 uri: task_uri.clone(),
1494 range: new_type_target_range,
1495 }),
1496 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1497 "A tooltip for `{new_type_label}`"
1498 ))),
1499 ..Default::default()
1500 },
1501 lsp::InlayHintLabelPart {
1502 value: "<".to_string(),
1503 ..Default::default()
1504 },
1505 lsp::InlayHintLabelPart {
1506 value: struct_label.to_string(),
1507 location: Some(lsp::Location {
1508 uri: task_uri,
1509 range: struct_target_range,
1510 }),
1511 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1512 lsp::MarkupContent {
1513 kind: lsp::MarkupKind::Markdown,
1514 value: format!("A tooltip for `{struct_label}`"),
1515 },
1516 )),
1517 ..Default::default()
1518 },
1519 lsp::InlayHintLabelPart {
1520 value: ">".to_string(),
1521 ..Default::default()
1522 },
1523 ]);
1524
1525 Ok(hint_to_resolve)
1526 }
1527 },
1528 )
1529 .next()
1530 .await;
1531 cx.background_executor.run_until_parked();
1532
1533 cx.update_editor(|editor, cx| {
1534 update_inlay_link_and_hover_points(
1535 &editor.snapshot(cx),
1536 new_type_hint_part_hover_position,
1537 editor,
1538 true,
1539 false,
1540 cx,
1541 );
1542 });
1543 cx.background_executor
1544 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1545 cx.background_executor.run_until_parked();
1546 cx.update_editor(|editor, cx| {
1547 let hover_state = &editor.hover_state;
1548 assert!(
1549 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1550 );
1551 let popover = hover_state.info_popovers.first().cloned().unwrap();
1552 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1553 assert_eq!(
1554 popover.symbol_range,
1555 RangeInEditor::Inlay(InlayHighlight {
1556 inlay: InlayId::Hint(0),
1557 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1558 range: ": ".len()..": ".len() + new_type_label.len(),
1559 }),
1560 "Popover range should match the new type label part"
1561 );
1562 assert_eq!(
1563 popover.get_rendered_text(cx),
1564 format!("A tooltip for {new_type_label}"),
1565 );
1566 });
1567
1568 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1569 let snapshot = editor.snapshot(cx);
1570 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1571 let next_valid = inlay_range.end.to_display_point(&snapshot);
1572 assert_eq!(previous_valid.row(), next_valid.row());
1573 assert!(previous_valid.column() < next_valid.column());
1574 let exact_unclipped = DisplayPoint::new(
1575 previous_valid.row(),
1576 previous_valid.column()
1577 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1578 as u32,
1579 );
1580 PointForPosition {
1581 previous_valid,
1582 next_valid,
1583 exact_unclipped,
1584 column_overshoot_after_line_end: 0,
1585 }
1586 });
1587 cx.update_editor(|editor, cx| {
1588 update_inlay_link_and_hover_points(
1589 &editor.snapshot(cx),
1590 struct_hint_part_hover_position,
1591 editor,
1592 true,
1593 false,
1594 cx,
1595 );
1596 });
1597 cx.background_executor
1598 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1599 cx.background_executor.run_until_parked();
1600 cx.update_editor(|editor, cx| {
1601 let hover_state = &editor.hover_state;
1602 assert!(
1603 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1604 );
1605 let popover = hover_state.info_popovers.first().cloned().unwrap();
1606 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1607 assert_eq!(
1608 popover.symbol_range,
1609 RangeInEditor::Inlay(InlayHighlight {
1610 inlay: InlayId::Hint(0),
1611 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1612 range: ": ".len() + new_type_label.len() + "<".len()
1613 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1614 }),
1615 "Popover range should match the struct label part"
1616 );
1617 assert_eq!(
1618 popover.get_rendered_text(cx),
1619 format!("A tooltip for {struct_label}"),
1620 "Rendered markdown element should remove backticks from text"
1621 );
1622 });
1623 }
1624}