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