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