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 .elevation_2_borderless(cx)
737 // Don't draw the background color if the theme
738 // allows transparent surfaces.
739 .when(window_is_transparent(cx), |this| {
740 this.bg(gpui::transparent_black())
741 })
742 // Prevent a mouse move on the popover from being propagated to the editor,
743 // because that would dismiss the popover.
744 .on_mouse_move(|_, cx| cx.stop_propagation())
745 // Prevent a mouse down on the popover from being propagated to the editor,
746 // because that would move the cursor.
747 .on_mouse_down(MouseButton::Left, move |_, cx| {
748 let mut keyboard_grace = keyboard_grace.borrow_mut();
749 *keyboard_grace = false;
750 cx.stop_propagation();
751 })
752 .child(markdown_div);
753
754 diagnostic_div.into_any_element()
755 }
756
757 pub fn activation_info(&self) -> (usize, Anchor) {
758 let entry = self
759 .primary_diagnostic
760 .as_ref()
761 .unwrap_or(&self.local_diagnostic);
762
763 (entry.diagnostic.group_id, entry.range.start)
764 }
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770 use crate::{
771 actions::ConfirmCompletion,
772 editor_tests::{handle_completion_request, init_test},
773 hover_links::update_inlay_link_and_hover_points,
774 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
775 test::editor_lsp_test_context::EditorLspTestContext,
776 InlayId, PointForPosition,
777 };
778 use collections::BTreeSet;
779 use indoc::indoc;
780 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
781 use lsp::LanguageServerId;
782 use markdown::parser::MarkdownEvent;
783 use smol::stream::StreamExt;
784 use std::sync::atomic;
785 use std::sync::atomic::AtomicUsize;
786 use text::Bias;
787
788 impl InfoPopover {
789 fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
790 let mut rendered_text = String::new();
791 if let Some(parsed_content) = self.parsed_content.clone() {
792 let markdown = parsed_content.read(cx);
793 let text = markdown.parsed_markdown().source().to_string();
794 let data = markdown.parsed_markdown().events();
795 let slice = data;
796
797 for (range, event) in slice.iter() {
798 if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
799 rendered_text.push_str(&text[range.clone()])
800 }
801 }
802 }
803 rendered_text
804 }
805 }
806
807 #[gpui::test]
808 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
809 cx: &mut gpui::TestAppContext,
810 ) {
811 init_test(cx, |_| {});
812 const HOVER_DELAY_MILLIS: u64 = 350;
813
814 let mut cx = EditorLspTestContext::new_rust(
815 lsp::ServerCapabilities {
816 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
817 completion_provider: Some(lsp::CompletionOptions {
818 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
819 resolve_provider: Some(true),
820 ..Default::default()
821 }),
822 ..Default::default()
823 },
824 cx,
825 )
826 .await;
827 let counter = Arc::new(AtomicUsize::new(0));
828 // Basic hover delays and then pops without moving the mouse
829 cx.set_state(indoc! {"
830 oneˇ
831 two
832 three
833 fn test() { println!(); }
834 "});
835
836 //prompt autocompletion menu
837 cx.simulate_keystroke(".");
838 handle_completion_request(
839 &mut cx,
840 indoc! {"
841 one.|<>
842 two
843 three
844 "},
845 vec!["first_completion", "second_completion"],
846 counter.clone(),
847 )
848 .await;
849 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
850 .await;
851 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
852
853 let hover_point = cx.display_point(indoc! {"
854 one.
855 two
856 three
857 fn test() { printˇln!(); }
858 "});
859 cx.update_editor(|editor, cx| {
860 let snapshot = editor.snapshot(cx);
861 let anchor = snapshot
862 .buffer_snapshot
863 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
864 hover_at(editor, Some(anchor), cx)
865 });
866 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
867
868 // After delay, hover should be visible.
869 let symbol_range = cx.lsp_range(indoc! {"
870 one.
871 two
872 three
873 fn test() { «println!»(); }
874 "});
875 let mut requests =
876 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
877 Ok(Some(lsp::Hover {
878 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
879 kind: lsp::MarkupKind::Markdown,
880 value: "some basic docs".to_string(),
881 }),
882 range: Some(symbol_range),
883 }))
884 });
885 cx.background_executor
886 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
887 requests.next().await;
888
889 cx.editor(|editor, cx| {
890 assert!(editor.hover_state.visible());
891 assert_eq!(
892 editor.hover_state.info_popovers.len(),
893 1,
894 "Expected exactly one hover but got: {:?}",
895 editor.hover_state.info_popovers
896 );
897 let rendered_text = editor
898 .hover_state
899 .info_popovers
900 .first()
901 .unwrap()
902 .get_rendered_text(cx);
903 assert_eq!(rendered_text, "some basic docs".to_string())
904 });
905
906 // check that the completion menu is still visible and that there still has only been 1 completion request
907 cx.editor(|editor, _| assert!(editor.context_menu_visible()));
908 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
909
910 //apply a completion and check it was successfully applied
911 let _apply_additional_edits = cx.update_editor(|editor, cx| {
912 editor.context_menu_next(&Default::default(), cx);
913 editor
914 .confirm_completion(&ConfirmCompletion::default(), cx)
915 .unwrap()
916 });
917 cx.assert_editor_state(indoc! {"
918 one.second_completionˇ
919 two
920 three
921 fn test() { println!(); }
922 "});
923
924 // check that the completion menu is no longer visible and that there still has only been 1 completion request
925 cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
926 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
927
928 //verify the information popover is still visible and unchanged
929 cx.editor(|editor, cx| {
930 assert!(editor.hover_state.visible());
931 assert_eq!(
932 editor.hover_state.info_popovers.len(),
933 1,
934 "Expected exactly one hover but got: {:?}",
935 editor.hover_state.info_popovers
936 );
937 let rendered_text = editor
938 .hover_state
939 .info_popovers
940 .first()
941 .unwrap()
942 .get_rendered_text(cx);
943
944 assert_eq!(rendered_text, "some basic docs".to_string())
945 });
946
947 // Mouse moved with no hover response dismisses
948 let hover_point = cx.display_point(indoc! {"
949 one.second_completionˇ
950 two
951 three
952 fn teˇst() { println!(); }
953 "});
954 let mut request = cx
955 .lsp
956 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
957 cx.update_editor(|editor, cx| {
958 let snapshot = editor.snapshot(cx);
959 let anchor = snapshot
960 .buffer_snapshot
961 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
962 hover_at(editor, Some(anchor), cx)
963 });
964 cx.background_executor
965 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
966 request.next().await;
967
968 // verify that the information popover is no longer visible
969 cx.editor(|editor, _| {
970 assert!(!editor.hover_state.visible());
971 });
972 }
973
974 #[gpui::test]
975 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
976 init_test(cx, |_| {});
977
978 let mut cx = EditorLspTestContext::new_rust(
979 lsp::ServerCapabilities {
980 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
981 ..Default::default()
982 },
983 cx,
984 )
985 .await;
986
987 // Basic hover delays and then pops without moving the mouse
988 cx.set_state(indoc! {"
989 fn ˇtest() { println!(); }
990 "});
991 let hover_point = cx.display_point(indoc! {"
992 fn test() { printˇln!(); }
993 "});
994
995 cx.update_editor(|editor, cx| {
996 let snapshot = editor.snapshot(cx);
997 let anchor = snapshot
998 .buffer_snapshot
999 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1000 hover_at(editor, Some(anchor), cx)
1001 });
1002 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
1003
1004 // After delay, hover should be visible.
1005 let symbol_range = cx.lsp_range(indoc! {"
1006 fn test() { «println!»(); }
1007 "});
1008 let mut requests =
1009 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1010 Ok(Some(lsp::Hover {
1011 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1012 kind: lsp::MarkupKind::Markdown,
1013 value: "some basic docs".to_string(),
1014 }),
1015 range: Some(symbol_range),
1016 }))
1017 });
1018 cx.background_executor
1019 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1020 requests.next().await;
1021
1022 cx.editor(|editor, cx| {
1023 assert!(editor.hover_state.visible());
1024 assert_eq!(
1025 editor.hover_state.info_popovers.len(),
1026 1,
1027 "Expected exactly one hover but got: {:?}",
1028 editor.hover_state.info_popovers
1029 );
1030 let rendered_text = editor
1031 .hover_state
1032 .info_popovers
1033 .first()
1034 .unwrap()
1035 .get_rendered_text(cx);
1036
1037 assert_eq!(rendered_text, "some basic docs".to_string())
1038 });
1039
1040 // Mouse moved with no hover response dismisses
1041 let hover_point = cx.display_point(indoc! {"
1042 fn teˇst() { println!(); }
1043 "});
1044 let mut request = cx
1045 .lsp
1046 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
1047 cx.update_editor(|editor, cx| {
1048 let snapshot = editor.snapshot(cx);
1049 let anchor = snapshot
1050 .buffer_snapshot
1051 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1052 hover_at(editor, Some(anchor), cx)
1053 });
1054 cx.background_executor
1055 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1056 request.next().await;
1057 cx.editor(|editor, _| {
1058 assert!(!editor.hover_state.visible());
1059 });
1060 }
1061
1062 #[gpui::test]
1063 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1064 init_test(cx, |_| {});
1065
1066 let mut cx = EditorLspTestContext::new_rust(
1067 lsp::ServerCapabilities {
1068 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1069 ..Default::default()
1070 },
1071 cx,
1072 )
1073 .await;
1074
1075 // Hover with keyboard has no delay
1076 cx.set_state(indoc! {"
1077 fˇn test() { println!(); }
1078 "});
1079 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1080 let symbol_range = cx.lsp_range(indoc! {"
1081 «fn» test() { println!(); }
1082 "});
1083
1084 cx.editor(|editor, _cx| {
1085 assert!(!editor.hover_state.visible());
1086
1087 assert_eq!(
1088 editor.hover_state.info_popovers.len(),
1089 0,
1090 "Expected no hovers but got but got: {:?}",
1091 editor.hover_state.info_popovers
1092 );
1093 });
1094
1095 let mut requests =
1096 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1097 Ok(Some(lsp::Hover {
1098 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1099 kind: lsp::MarkupKind::Markdown,
1100 value: "some other basic docs".to_string(),
1101 }),
1102 range: Some(symbol_range),
1103 }))
1104 });
1105
1106 requests.next().await;
1107 cx.dispatch_action(Hover);
1108
1109 cx.condition(|editor, _| editor.hover_state.visible()).await;
1110 cx.editor(|editor, cx| {
1111 assert_eq!(
1112 editor.hover_state.info_popovers.len(),
1113 1,
1114 "Expected exactly one hover but got: {:?}",
1115 editor.hover_state.info_popovers
1116 );
1117
1118 let rendered_text = editor
1119 .hover_state
1120 .info_popovers
1121 .first()
1122 .unwrap()
1123 .get_rendered_text(cx);
1124
1125 assert_eq!(rendered_text, "some other basic docs".to_string())
1126 });
1127 }
1128
1129 #[gpui::test]
1130 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1131 init_test(cx, |_| {});
1132
1133 let mut cx = EditorLspTestContext::new_rust(
1134 lsp::ServerCapabilities {
1135 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1136 ..Default::default()
1137 },
1138 cx,
1139 )
1140 .await;
1141
1142 // Hover with keyboard has no delay
1143 cx.set_state(indoc! {"
1144 fˇn test() { println!(); }
1145 "});
1146 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1147 let symbol_range = cx.lsp_range(indoc! {"
1148 «fn» test() { println!(); }
1149 "});
1150 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1151 Ok(Some(lsp::Hover {
1152 contents: lsp::HoverContents::Array(vec![
1153 lsp::MarkedString::String("regular text for hover to show".to_string()),
1154 lsp::MarkedString::String("".to_string()),
1155 lsp::MarkedString::LanguageString(lsp::LanguageString {
1156 language: "Rust".to_string(),
1157 value: "".to_string(),
1158 }),
1159 ]),
1160 range: Some(symbol_range),
1161 }))
1162 })
1163 .next()
1164 .await;
1165 cx.dispatch_action(Hover);
1166
1167 cx.condition(|editor, _| editor.hover_state.visible()).await;
1168 cx.editor(|editor, cx| {
1169 assert_eq!(
1170 editor.hover_state.info_popovers.len(),
1171 1,
1172 "Expected exactly one hover but got: {:?}",
1173 editor.hover_state.info_popovers
1174 );
1175 let rendered_text = editor
1176 .hover_state
1177 .info_popovers
1178 .first()
1179 .unwrap()
1180 .get_rendered_text(cx);
1181
1182 assert_eq!(
1183 rendered_text,
1184 "regular text for hover to show".to_string(),
1185 "No empty string hovers should be shown"
1186 );
1187 });
1188 }
1189
1190 #[gpui::test]
1191 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1192 init_test(cx, |_| {});
1193
1194 let mut cx = EditorLspTestContext::new_rust(
1195 lsp::ServerCapabilities {
1196 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1197 ..Default::default()
1198 },
1199 cx,
1200 )
1201 .await;
1202
1203 // Hover with keyboard has no delay
1204 cx.set_state(indoc! {"
1205 fˇn test() { println!(); }
1206 "});
1207 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1208 let symbol_range = cx.lsp_range(indoc! {"
1209 «fn» test() { println!(); }
1210 "});
1211
1212 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1213 let markdown_string = format!("\n```rust\n{code_str}```");
1214
1215 let closure_markdown_string = markdown_string.clone();
1216 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1217 let future_markdown_string = closure_markdown_string.clone();
1218 async move {
1219 Ok(Some(lsp::Hover {
1220 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1221 kind: lsp::MarkupKind::Markdown,
1222 value: future_markdown_string,
1223 }),
1224 range: Some(symbol_range),
1225 }))
1226 }
1227 })
1228 .next()
1229 .await;
1230
1231 cx.dispatch_action(Hover);
1232
1233 cx.condition(|editor, _| editor.hover_state.visible()).await;
1234 cx.editor(|editor, cx| {
1235 assert_eq!(
1236 editor.hover_state.info_popovers.len(),
1237 1,
1238 "Expected exactly one hover but got: {:?}",
1239 editor.hover_state.info_popovers
1240 );
1241 let rendered_text = editor
1242 .hover_state
1243 .info_popovers
1244 .first()
1245 .unwrap()
1246 .get_rendered_text(cx);
1247
1248 assert_eq!(
1249 rendered_text, code_str,
1250 "Should not have extra line breaks at end of rendered hover"
1251 );
1252 });
1253 }
1254
1255 #[gpui::test]
1256 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1257 init_test(cx, |_| {});
1258
1259 let mut cx = EditorLspTestContext::new_rust(
1260 lsp::ServerCapabilities {
1261 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1262 ..Default::default()
1263 },
1264 cx,
1265 )
1266 .await;
1267
1268 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1269 // info popover once request completes
1270 cx.set_state(indoc! {"
1271 fn teˇst() { println!(); }
1272 "});
1273
1274 // Send diagnostic to client
1275 let range = cx.text_anchor_range(indoc! {"
1276 fn «test»() { println!(); }
1277 "});
1278 cx.update_buffer(|buffer, cx| {
1279 let snapshot = buffer.text_snapshot();
1280 let set = DiagnosticSet::from_sorted_entries(
1281 vec![DiagnosticEntry {
1282 range,
1283 diagnostic: Diagnostic {
1284 message: "A test diagnostic message.".to_string(),
1285 ..Default::default()
1286 },
1287 }],
1288 &snapshot,
1289 );
1290 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1291 });
1292
1293 // Hover pops diagnostic immediately
1294 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1295 cx.background_executor.run_until_parked();
1296
1297 cx.editor(|Editor { hover_state, .. }, _| {
1298 assert!(
1299 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1300 )
1301 });
1302
1303 // Info Popover shows after request responded to
1304 let range = cx.lsp_range(indoc! {"
1305 fn «test»() { println!(); }
1306 "});
1307 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1308 Ok(Some(lsp::Hover {
1309 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1310 kind: lsp::MarkupKind::Markdown,
1311 value: "some new docs".to_string(),
1312 }),
1313 range: Some(range),
1314 }))
1315 });
1316 cx.background_executor
1317 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1318
1319 cx.background_executor.run_until_parked();
1320 cx.editor(|Editor { hover_state, .. }, _| {
1321 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1322 });
1323 }
1324
1325 #[gpui::test]
1326 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1327 init_test(cx, |settings| {
1328 settings.defaults.inlay_hints = Some(InlayHintSettings {
1329 enabled: true,
1330 edit_debounce_ms: 0,
1331 scroll_debounce_ms: 0,
1332 show_type_hints: true,
1333 show_parameter_hints: true,
1334 show_other_hints: true,
1335 })
1336 });
1337
1338 let mut cx = EditorLspTestContext::new_rust(
1339 lsp::ServerCapabilities {
1340 inlay_hint_provider: Some(lsp::OneOf::Right(
1341 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1342 resolve_provider: Some(true),
1343 ..Default::default()
1344 }),
1345 )),
1346 ..Default::default()
1347 },
1348 cx,
1349 )
1350 .await;
1351
1352 cx.set_state(indoc! {"
1353 struct TestStruct;
1354
1355 // ==================
1356
1357 struct TestNewType<T>(T);
1358
1359 fn main() {
1360 let variableˇ = TestNewType(TestStruct);
1361 }
1362 "});
1363
1364 let hint_start_offset = cx.ranges(indoc! {"
1365 struct TestStruct;
1366
1367 // ==================
1368
1369 struct TestNewType<T>(T);
1370
1371 fn main() {
1372 let variableˇ = TestNewType(TestStruct);
1373 }
1374 "})[0]
1375 .start;
1376 let hint_position = cx.to_lsp(hint_start_offset);
1377 let new_type_target_range = cx.lsp_range(indoc! {"
1378 struct TestStruct;
1379
1380 // ==================
1381
1382 struct «TestNewType»<T>(T);
1383
1384 fn main() {
1385 let variable = TestNewType(TestStruct);
1386 }
1387 "});
1388 let struct_target_range = cx.lsp_range(indoc! {"
1389 struct «TestStruct»;
1390
1391 // ==================
1392
1393 struct TestNewType<T>(T);
1394
1395 fn main() {
1396 let variable = TestNewType(TestStruct);
1397 }
1398 "});
1399
1400 let uri = cx.buffer_lsp_url.clone();
1401 let new_type_label = "TestNewType";
1402 let struct_label = "TestStruct";
1403 let entire_hint_label = ": TestNewType<TestStruct>";
1404 let closure_uri = uri.clone();
1405 cx.lsp
1406 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1407 let task_uri = closure_uri.clone();
1408 async move {
1409 assert_eq!(params.text_document.uri, task_uri);
1410 Ok(Some(vec![lsp::InlayHint {
1411 position: hint_position,
1412 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1413 value: entire_hint_label.to_string(),
1414 ..Default::default()
1415 }]),
1416 kind: Some(lsp::InlayHintKind::TYPE),
1417 text_edits: None,
1418 tooltip: None,
1419 padding_left: Some(false),
1420 padding_right: Some(false),
1421 data: None,
1422 }]))
1423 }
1424 })
1425 .next()
1426 .await;
1427 cx.background_executor.run_until_parked();
1428 cx.update_editor(|editor, cx| {
1429 let expected_layers = vec![entire_hint_label.to_string()];
1430 assert_eq!(expected_layers, cached_hint_labels(editor));
1431 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1432 });
1433
1434 let inlay_range = cx
1435 .ranges(indoc! {"
1436 struct TestStruct;
1437
1438 // ==================
1439
1440 struct TestNewType<T>(T);
1441
1442 fn main() {
1443 let variable« »= TestNewType(TestStruct);
1444 }
1445 "})
1446 .get(0)
1447 .cloned()
1448 .unwrap();
1449 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1450 let snapshot = editor.snapshot(cx);
1451 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1452 let next_valid = inlay_range.end.to_display_point(&snapshot);
1453 assert_eq!(previous_valid.row(), next_valid.row());
1454 assert!(previous_valid.column() < next_valid.column());
1455 let exact_unclipped = DisplayPoint::new(
1456 previous_valid.row(),
1457 previous_valid.column()
1458 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1459 as u32,
1460 );
1461 PointForPosition {
1462 previous_valid,
1463 next_valid,
1464 exact_unclipped,
1465 column_overshoot_after_line_end: 0,
1466 }
1467 });
1468 cx.update_editor(|editor, cx| {
1469 update_inlay_link_and_hover_points(
1470 &editor.snapshot(cx),
1471 new_type_hint_part_hover_position,
1472 editor,
1473 true,
1474 false,
1475 cx,
1476 );
1477 });
1478
1479 let resolve_closure_uri = uri.clone();
1480 cx.lsp
1481 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1482 move |mut hint_to_resolve, _| {
1483 let mut resolved_hint_positions = BTreeSet::new();
1484 let task_uri = resolve_closure_uri.clone();
1485 async move {
1486 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1487 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1488
1489 // `: TestNewType<TestStruct>`
1490 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1491 lsp::InlayHintLabelPart {
1492 value: ": ".to_string(),
1493 ..Default::default()
1494 },
1495 lsp::InlayHintLabelPart {
1496 value: new_type_label.to_string(),
1497 location: Some(lsp::Location {
1498 uri: task_uri.clone(),
1499 range: new_type_target_range,
1500 }),
1501 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1502 "A tooltip for `{new_type_label}`"
1503 ))),
1504 ..Default::default()
1505 },
1506 lsp::InlayHintLabelPart {
1507 value: "<".to_string(),
1508 ..Default::default()
1509 },
1510 lsp::InlayHintLabelPart {
1511 value: struct_label.to_string(),
1512 location: Some(lsp::Location {
1513 uri: task_uri,
1514 range: struct_target_range,
1515 }),
1516 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1517 lsp::MarkupContent {
1518 kind: lsp::MarkupKind::Markdown,
1519 value: format!("A tooltip for `{struct_label}`"),
1520 },
1521 )),
1522 ..Default::default()
1523 },
1524 lsp::InlayHintLabelPart {
1525 value: ">".to_string(),
1526 ..Default::default()
1527 },
1528 ]);
1529
1530 Ok(hint_to_resolve)
1531 }
1532 },
1533 )
1534 .next()
1535 .await;
1536 cx.background_executor.run_until_parked();
1537
1538 cx.update_editor(|editor, cx| {
1539 update_inlay_link_and_hover_points(
1540 &editor.snapshot(cx),
1541 new_type_hint_part_hover_position,
1542 editor,
1543 true,
1544 false,
1545 cx,
1546 );
1547 });
1548 cx.background_executor
1549 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1550 cx.background_executor.run_until_parked();
1551 cx.update_editor(|editor, cx| {
1552 let hover_state = &editor.hover_state;
1553 assert!(
1554 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1555 );
1556 let popover = hover_state.info_popovers.first().cloned().unwrap();
1557 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1558 assert_eq!(
1559 popover.symbol_range,
1560 RangeInEditor::Inlay(InlayHighlight {
1561 inlay: InlayId::Hint(0),
1562 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1563 range: ": ".len()..": ".len() + new_type_label.len(),
1564 }),
1565 "Popover range should match the new type label part"
1566 );
1567 assert_eq!(
1568 popover.get_rendered_text(cx),
1569 format!("A tooltip for {new_type_label}"),
1570 );
1571 });
1572
1573 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1574 let snapshot = editor.snapshot(cx);
1575 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1576 let next_valid = inlay_range.end.to_display_point(&snapshot);
1577 assert_eq!(previous_valid.row(), next_valid.row());
1578 assert!(previous_valid.column() < next_valid.column());
1579 let exact_unclipped = DisplayPoint::new(
1580 previous_valid.row(),
1581 previous_valid.column()
1582 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1583 as u32,
1584 );
1585 PointForPosition {
1586 previous_valid,
1587 next_valid,
1588 exact_unclipped,
1589 column_overshoot_after_line_end: 0,
1590 }
1591 });
1592 cx.update_editor(|editor, cx| {
1593 update_inlay_link_and_hover_points(
1594 &editor.snapshot(cx),
1595 struct_hint_part_hover_position,
1596 editor,
1597 true,
1598 false,
1599 cx,
1600 );
1601 });
1602 cx.background_executor
1603 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1604 cx.background_executor.run_until_parked();
1605 cx.update_editor(|editor, cx| {
1606 let hover_state = &editor.hover_state;
1607 assert!(
1608 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1609 );
1610 let popover = hover_state.info_popovers.first().cloned().unwrap();
1611 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1612 assert_eq!(
1613 popover.symbol_range,
1614 RangeInEditor::Inlay(InlayHighlight {
1615 inlay: InlayId::Hint(0),
1616 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1617 range: ": ".len() + new_type_label.len() + "<".len()
1618 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1619 }),
1620 "Popover range should match the struct label part"
1621 );
1622 assert_eq!(
1623 popover.get_rendered_text(cx),
1624 format!("A tooltip for {struct_label}"),
1625 "Rendered markdown element should remove backticks from text"
1626 );
1627 });
1628 }
1629}