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