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