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