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