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: 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 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(false),
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 () = cx
906 .update_editor(|editor, cx| {
907 editor.context_menu_next(&Default::default(), cx);
908 editor
909 .confirm_completion(&ConfirmCompletion::default(), cx)
910 .unwrap()
911 })
912 .await
913 .unwrap();
914 cx.assert_editor_state(indoc! {"
915 one.second_completionˇ
916 two
917 three
918 fn test() { println!(); }
919 "});
920
921 // check that the completion menu is no longer visible and that there still has only been 1 completion request
922 cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
923 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
924
925 //verify the information popover is still visible and unchanged
926 cx.editor(|editor, cx| {
927 assert!(editor.hover_state.visible());
928 assert_eq!(
929 editor.hover_state.info_popovers.len(),
930 1,
931 "Expected exactly one hover but got: {:?}",
932 editor.hover_state.info_popovers
933 );
934 let rendered_text = editor
935 .hover_state
936 .info_popovers
937 .first()
938 .unwrap()
939 .get_rendered_text(cx);
940
941 assert_eq!(rendered_text, "some basic docs".to_string())
942 });
943
944 // Mouse moved with no hover response dismisses
945 let hover_point = cx.display_point(indoc! {"
946 one.second_completionˇ
947 two
948 three
949 fn teˇst() { println!(); }
950 "});
951 let mut request = cx
952 .lsp
953 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
954 cx.update_editor(|editor, cx| {
955 let snapshot = editor.snapshot(cx);
956 let anchor = snapshot
957 .buffer_snapshot
958 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
959 hover_at(editor, Some(anchor), cx)
960 });
961 cx.background_executor
962 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
963 request.next().await;
964
965 // verify that the information popover is no longer visible
966 cx.editor(|editor, _| {
967 assert!(!editor.hover_state.visible());
968 });
969 }
970
971 #[gpui::test]
972 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
973 init_test(cx, |_| {});
974
975 let mut cx = EditorLspTestContext::new_rust(
976 lsp::ServerCapabilities {
977 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
978 ..Default::default()
979 },
980 cx,
981 )
982 .await;
983
984 // Basic hover delays and then pops without moving the mouse
985 cx.set_state(indoc! {"
986 fn ˇtest() { println!(); }
987 "});
988 let hover_point = cx.display_point(indoc! {"
989 fn test() { printˇln!(); }
990 "});
991
992 cx.update_editor(|editor, cx| {
993 let snapshot = editor.snapshot(cx);
994 let anchor = snapshot
995 .buffer_snapshot
996 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
997 hover_at(editor, Some(anchor), cx)
998 });
999 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
1000
1001 // After delay, hover should be visible.
1002 let symbol_range = cx.lsp_range(indoc! {"
1003 fn test() { «println!»(); }
1004 "});
1005 let mut requests =
1006 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1007 Ok(Some(lsp::Hover {
1008 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1009 kind: lsp::MarkupKind::Markdown,
1010 value: "some basic docs".to_string(),
1011 }),
1012 range: Some(symbol_range),
1013 }))
1014 });
1015 cx.background_executor
1016 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1017 requests.next().await;
1018
1019 cx.editor(|editor, cx| {
1020 assert!(editor.hover_state.visible());
1021 assert_eq!(
1022 editor.hover_state.info_popovers.len(),
1023 1,
1024 "Expected exactly one hover but got: {:?}",
1025 editor.hover_state.info_popovers
1026 );
1027 let rendered_text = editor
1028 .hover_state
1029 .info_popovers
1030 .first()
1031 .unwrap()
1032 .get_rendered_text(cx);
1033
1034 assert_eq!(rendered_text, "some basic docs".to_string())
1035 });
1036
1037 // Mouse moved with no hover response dismisses
1038 let hover_point = cx.display_point(indoc! {"
1039 fn teˇst() { println!(); }
1040 "});
1041 let mut request = cx
1042 .lsp
1043 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1044 cx.update_editor(|editor, cx| {
1045 let snapshot = editor.snapshot(cx);
1046 let anchor = snapshot
1047 .buffer_snapshot
1048 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1049 hover_at(editor, Some(anchor), cx)
1050 });
1051 cx.background_executor
1052 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1053 request.next().await;
1054 cx.editor(|editor, _| {
1055 assert!(!editor.hover_state.visible());
1056 });
1057 }
1058
1059 #[gpui::test]
1060 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1061 init_test(cx, |_| {});
1062
1063 let mut cx = EditorLspTestContext::new_rust(
1064 lsp::ServerCapabilities {
1065 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1066 ..Default::default()
1067 },
1068 cx,
1069 )
1070 .await;
1071
1072 // Hover with keyboard has no delay
1073 cx.set_state(indoc! {"
1074 fˇn test() { println!(); }
1075 "});
1076 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1077 let symbol_range = cx.lsp_range(indoc! {"
1078 «fn» test() { println!(); }
1079 "});
1080
1081 cx.editor(|editor, _cx| {
1082 assert!(!editor.hover_state.visible());
1083
1084 assert_eq!(
1085 editor.hover_state.info_popovers.len(),
1086 0,
1087 "Expected no hovers but got but got: {:?}",
1088 editor.hover_state.info_popovers
1089 );
1090 });
1091
1092 let mut requests =
1093 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1094 Ok(Some(lsp::Hover {
1095 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1096 kind: lsp::MarkupKind::Markdown,
1097 value: "some other basic docs".to_string(),
1098 }),
1099 range: Some(symbol_range),
1100 }))
1101 });
1102
1103 requests.next().await;
1104 cx.dispatch_action(Hover);
1105
1106 cx.condition(|editor, _| editor.hover_state.visible()).await;
1107 cx.editor(|editor, cx| {
1108 assert_eq!(
1109 editor.hover_state.info_popovers.len(),
1110 1,
1111 "Expected exactly one hover but got: {:?}",
1112 editor.hover_state.info_popovers
1113 );
1114
1115 let rendered_text = editor
1116 .hover_state
1117 .info_popovers
1118 .first()
1119 .unwrap()
1120 .get_rendered_text(cx);
1121
1122 assert_eq!(rendered_text, "some other basic docs".to_string())
1123 });
1124 }
1125
1126 #[gpui::test]
1127 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1128 init_test(cx, |_| {});
1129
1130 let mut cx = EditorLspTestContext::new_rust(
1131 lsp::ServerCapabilities {
1132 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1133 ..Default::default()
1134 },
1135 cx,
1136 )
1137 .await;
1138
1139 // Hover with keyboard has no delay
1140 cx.set_state(indoc! {"
1141 fˇn test() { println!(); }
1142 "});
1143 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1144 let symbol_range = cx.lsp_range(indoc! {"
1145 «fn» test() { println!(); }
1146 "});
1147 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1148 Ok(Some(lsp::Hover {
1149 contents: lsp::HoverContents::Array(vec![
1150 lsp::MarkedString::String("regular text for hover to show".to_string()),
1151 lsp::MarkedString::String("".to_string()),
1152 lsp::MarkedString::LanguageString(lsp::LanguageString {
1153 language: "Rust".to_string(),
1154 value: "".to_string(),
1155 }),
1156 ]),
1157 range: Some(symbol_range),
1158 }))
1159 })
1160 .next()
1161 .await;
1162 cx.dispatch_action(Hover);
1163
1164 cx.condition(|editor, _| editor.hover_state.visible()).await;
1165 cx.editor(|editor, cx| {
1166 assert_eq!(
1167 editor.hover_state.info_popovers.len(),
1168 1,
1169 "Expected exactly one hover but got: {:?}",
1170 editor.hover_state.info_popovers
1171 );
1172 let rendered_text = editor
1173 .hover_state
1174 .info_popovers
1175 .first()
1176 .unwrap()
1177 .get_rendered_text(cx);
1178
1179 assert_eq!(
1180 rendered_text,
1181 "regular text for hover to show".to_string(),
1182 "No empty string hovers should be shown"
1183 );
1184 });
1185 }
1186
1187 #[gpui::test]
1188 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1189 init_test(cx, |_| {});
1190
1191 let mut cx = EditorLspTestContext::new_rust(
1192 lsp::ServerCapabilities {
1193 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1194 ..Default::default()
1195 },
1196 cx,
1197 )
1198 .await;
1199
1200 // Hover with keyboard has no delay
1201 cx.set_state(indoc! {"
1202 fˇn test() { println!(); }
1203 "});
1204 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1205 let symbol_range = cx.lsp_range(indoc! {"
1206 «fn» test() { println!(); }
1207 "});
1208
1209 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1210 let markdown_string = format!("\n```rust\n{code_str}```");
1211
1212 let closure_markdown_string = markdown_string.clone();
1213 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1214 let future_markdown_string = closure_markdown_string.clone();
1215 async move {
1216 Ok(Some(lsp::Hover {
1217 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1218 kind: lsp::MarkupKind::Markdown,
1219 value: future_markdown_string,
1220 }),
1221 range: Some(symbol_range),
1222 }))
1223 }
1224 })
1225 .next()
1226 .await;
1227
1228 cx.dispatch_action(Hover);
1229
1230 cx.condition(|editor, _| editor.hover_state.visible()).await;
1231 cx.editor(|editor, cx| {
1232 assert_eq!(
1233 editor.hover_state.info_popovers.len(),
1234 1,
1235 "Expected exactly one hover but got: {:?}",
1236 editor.hover_state.info_popovers
1237 );
1238 let rendered_text = editor
1239 .hover_state
1240 .info_popovers
1241 .first()
1242 .unwrap()
1243 .get_rendered_text(cx);
1244
1245 assert_eq!(
1246 rendered_text, code_str,
1247 "Should not have extra line breaks at end of rendered hover"
1248 );
1249 });
1250 }
1251
1252 #[gpui::test]
1253 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1254 init_test(cx, |_| {});
1255
1256 let mut cx = EditorLspTestContext::new_rust(
1257 lsp::ServerCapabilities {
1258 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1259 ..Default::default()
1260 },
1261 cx,
1262 )
1263 .await;
1264
1265 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1266 // info popover once request completes
1267 cx.set_state(indoc! {"
1268 fn teˇst() { println!(); }
1269 "});
1270
1271 // Send diagnostic to client
1272 let range = cx.text_anchor_range(indoc! {"
1273 fn «test»() { println!(); }
1274 "});
1275 cx.update_buffer(|buffer, cx| {
1276 let snapshot = buffer.text_snapshot();
1277 let set = DiagnosticSet::from_sorted_entries(
1278 vec![DiagnosticEntry {
1279 range,
1280 diagnostic: Diagnostic {
1281 message: "A test diagnostic message.".to_string(),
1282 ..Default::default()
1283 },
1284 }],
1285 &snapshot,
1286 );
1287 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1288 });
1289
1290 // Hover pops diagnostic immediately
1291 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1292 cx.background_executor.run_until_parked();
1293
1294 cx.editor(|Editor { hover_state, .. }, _| {
1295 assert!(
1296 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1297 )
1298 });
1299
1300 // Info Popover shows after request responded to
1301 let range = cx.lsp_range(indoc! {"
1302 fn «test»() { println!(); }
1303 "});
1304 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1305 Ok(Some(lsp::Hover {
1306 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1307 kind: lsp::MarkupKind::Markdown,
1308 value: "some new docs".to_string(),
1309 }),
1310 range: Some(range),
1311 }))
1312 });
1313 cx.background_executor
1314 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1315
1316 cx.background_executor.run_until_parked();
1317 cx.editor(|Editor { hover_state, .. }, _| {
1318 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1319 });
1320 }
1321
1322 #[gpui::test]
1323 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1324 init_test(cx, |settings| {
1325 settings.defaults.inlay_hints = Some(InlayHintSettings {
1326 enabled: true,
1327 edit_debounce_ms: 0,
1328 scroll_debounce_ms: 0,
1329 show_type_hints: true,
1330 show_parameter_hints: true,
1331 show_other_hints: true,
1332 show_background: false,
1333 })
1334 });
1335
1336 let mut cx = EditorLspTestContext::new_rust(
1337 lsp::ServerCapabilities {
1338 inlay_hint_provider: Some(lsp::OneOf::Right(
1339 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1340 resolve_provider: Some(true),
1341 ..Default::default()
1342 }),
1343 )),
1344 ..Default::default()
1345 },
1346 cx,
1347 )
1348 .await;
1349
1350 cx.set_state(indoc! {"
1351 struct TestStruct;
1352
1353 // ==================
1354
1355 struct TestNewType<T>(T);
1356
1357 fn main() {
1358 let variableˇ = TestNewType(TestStruct);
1359 }
1360 "});
1361
1362 let hint_start_offset = cx.ranges(indoc! {"
1363 struct TestStruct;
1364
1365 // ==================
1366
1367 struct TestNewType<T>(T);
1368
1369 fn main() {
1370 let variableˇ = TestNewType(TestStruct);
1371 }
1372 "})[0]
1373 .start;
1374 let hint_position = cx.to_lsp(hint_start_offset);
1375 let new_type_target_range = cx.lsp_range(indoc! {"
1376 struct TestStruct;
1377
1378 // ==================
1379
1380 struct «TestNewType»<T>(T);
1381
1382 fn main() {
1383 let variable = TestNewType(TestStruct);
1384 }
1385 "});
1386 let struct_target_range = cx.lsp_range(indoc! {"
1387 struct «TestStruct»;
1388
1389 // ==================
1390
1391 struct TestNewType<T>(T);
1392
1393 fn main() {
1394 let variable = TestNewType(TestStruct);
1395 }
1396 "});
1397
1398 let uri = cx.buffer_lsp_url.clone();
1399 let new_type_label = "TestNewType";
1400 let struct_label = "TestStruct";
1401 let entire_hint_label = ": TestNewType<TestStruct>";
1402 let closure_uri = uri.clone();
1403 cx.lsp
1404 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1405 let task_uri = closure_uri.clone();
1406 async move {
1407 assert_eq!(params.text_document.uri, task_uri);
1408 Ok(Some(vec![lsp::InlayHint {
1409 position: hint_position,
1410 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1411 value: entire_hint_label.to_string(),
1412 ..Default::default()
1413 }]),
1414 kind: Some(lsp::InlayHintKind::TYPE),
1415 text_edits: None,
1416 tooltip: None,
1417 padding_left: Some(false),
1418 padding_right: Some(false),
1419 data: None,
1420 }]))
1421 }
1422 })
1423 .next()
1424 .await;
1425 cx.background_executor.run_until_parked();
1426 cx.update_editor(|editor, cx| {
1427 let expected_layers = vec![entire_hint_label.to_string()];
1428 assert_eq!(expected_layers, cached_hint_labels(editor));
1429 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1430 });
1431
1432 let inlay_range = cx
1433 .ranges(indoc! {"
1434 struct TestStruct;
1435
1436 // ==================
1437
1438 struct TestNewType<T>(T);
1439
1440 fn main() {
1441 let variable« »= TestNewType(TestStruct);
1442 }
1443 "})
1444 .first()
1445 .cloned()
1446 .unwrap();
1447 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1448 let snapshot = editor.snapshot(cx);
1449 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1450 let next_valid = inlay_range.end.to_display_point(&snapshot);
1451 assert_eq!(previous_valid.row(), next_valid.row());
1452 assert!(previous_valid.column() < next_valid.column());
1453 let exact_unclipped = DisplayPoint::new(
1454 previous_valid.row(),
1455 previous_valid.column()
1456 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1457 as u32,
1458 );
1459 PointForPosition {
1460 previous_valid,
1461 next_valid,
1462 exact_unclipped,
1463 column_overshoot_after_line_end: 0,
1464 }
1465 });
1466 cx.update_editor(|editor, cx| {
1467 update_inlay_link_and_hover_points(
1468 &editor.snapshot(cx),
1469 new_type_hint_part_hover_position,
1470 editor,
1471 true,
1472 false,
1473 cx,
1474 );
1475 });
1476
1477 let resolve_closure_uri = uri.clone();
1478 cx.lsp
1479 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1480 move |mut hint_to_resolve, _| {
1481 let mut resolved_hint_positions = BTreeSet::new();
1482 let task_uri = resolve_closure_uri.clone();
1483 async move {
1484 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1485 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1486
1487 // `: TestNewType<TestStruct>`
1488 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1489 lsp::InlayHintLabelPart {
1490 value: ": ".to_string(),
1491 ..Default::default()
1492 },
1493 lsp::InlayHintLabelPart {
1494 value: new_type_label.to_string(),
1495 location: Some(lsp::Location {
1496 uri: task_uri.clone(),
1497 range: new_type_target_range,
1498 }),
1499 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1500 "A tooltip for `{new_type_label}`"
1501 ))),
1502 ..Default::default()
1503 },
1504 lsp::InlayHintLabelPart {
1505 value: "<".to_string(),
1506 ..Default::default()
1507 },
1508 lsp::InlayHintLabelPart {
1509 value: struct_label.to_string(),
1510 location: Some(lsp::Location {
1511 uri: task_uri,
1512 range: struct_target_range,
1513 }),
1514 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1515 lsp::MarkupContent {
1516 kind: lsp::MarkupKind::Markdown,
1517 value: format!("A tooltip for `{struct_label}`"),
1518 },
1519 )),
1520 ..Default::default()
1521 },
1522 lsp::InlayHintLabelPart {
1523 value: ">".to_string(),
1524 ..Default::default()
1525 },
1526 ]);
1527
1528 Ok(hint_to_resolve)
1529 }
1530 },
1531 )
1532 .next()
1533 .await;
1534 cx.background_executor.run_until_parked();
1535
1536 cx.update_editor(|editor, cx| {
1537 update_inlay_link_and_hover_points(
1538 &editor.snapshot(cx),
1539 new_type_hint_part_hover_position,
1540 editor,
1541 true,
1542 false,
1543 cx,
1544 );
1545 });
1546 cx.background_executor
1547 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1548 cx.background_executor.run_until_parked();
1549 cx.update_editor(|editor, cx| {
1550 let hover_state = &editor.hover_state;
1551 assert!(
1552 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1553 );
1554 let popover = hover_state.info_popovers.first().cloned().unwrap();
1555 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1556 assert_eq!(
1557 popover.symbol_range,
1558 RangeInEditor::Inlay(InlayHighlight {
1559 inlay: InlayId::Hint(0),
1560 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1561 range: ": ".len()..": ".len() + new_type_label.len(),
1562 }),
1563 "Popover range should match the new type label part"
1564 );
1565 assert_eq!(
1566 popover.get_rendered_text(cx),
1567 format!("A tooltip for {new_type_label}"),
1568 );
1569 });
1570
1571 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1572 let snapshot = editor.snapshot(cx);
1573 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1574 let next_valid = inlay_range.end.to_display_point(&snapshot);
1575 assert_eq!(previous_valid.row(), next_valid.row());
1576 assert!(previous_valid.column() < next_valid.column());
1577 let exact_unclipped = DisplayPoint::new(
1578 previous_valid.row(),
1579 previous_valid.column()
1580 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1581 as u32,
1582 );
1583 PointForPosition {
1584 previous_valid,
1585 next_valid,
1586 exact_unclipped,
1587 column_overshoot_after_line_end: 0,
1588 }
1589 });
1590 cx.update_editor(|editor, cx| {
1591 update_inlay_link_and_hover_points(
1592 &editor.snapshot(cx),
1593 struct_hint_part_hover_position,
1594 editor,
1595 true,
1596 false,
1597 cx,
1598 );
1599 });
1600 cx.background_executor
1601 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1602 cx.background_executor.run_until_parked();
1603 cx.update_editor(|editor, cx| {
1604 let hover_state = &editor.hover_state;
1605 assert!(
1606 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1607 );
1608 let popover = hover_state.info_popovers.first().cloned().unwrap();
1609 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1610 assert_eq!(
1611 popover.symbol_range,
1612 RangeInEditor::Inlay(InlayHighlight {
1613 inlay: InlayId::Hint(0),
1614 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1615 range: ": ".len() + new_type_label.len() + "<".len()
1616 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1617 }),
1618 "Popover range should match the struct label part"
1619 );
1620 assert_eq!(
1621 popover.get_rendered_text(cx),
1622 format!("A tooltip for {struct_label}"),
1623 "Rendered markdown element should remove backticks from text"
1624 );
1625 });
1626 }
1627}