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