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::*, 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(cx)
591 .overflow_y_scroll()
592 .px_2()
593 .py_1()
594 .bg(diagnostic_colors.background)
595 .text_color(style.text.color)
596 .border_1()
597 .border_color(diagnostic_colors.border)
598 .rounded_md()
599 .max_w(max_size.width)
600 .max_h(max_size.height)
601 .cursor(CursorStyle::PointingHand)
602 .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
603 // Prevent a mouse move on the popover from being propagated to the editor,
604 // because that would dismiss the popover.
605 .on_mouse_move(|_, cx| cx.stop_propagation())
606 // Prevent a mouse down on the popover from being propagated to the editor,
607 // because that would move the cursor.
608 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
609 .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
610 .child(SharedString::from(text))
611 .into_any_element()
612 }
613
614 pub fn activation_info(&self) -> (usize, Anchor) {
615 let entry = self
616 .primary_diagnostic
617 .as_ref()
618 .unwrap_or(&self.local_diagnostic);
619
620 (entry.diagnostic.group_id, entry.range.start)
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::{
628 actions::ConfirmCompletion,
629 editor_tests::{handle_completion_request, init_test},
630 hover_links::update_inlay_link_and_hover_points,
631 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
632 test::editor_lsp_test_context::EditorLspTestContext,
633 InlayId, PointForPosition,
634 };
635 use collections::BTreeSet;
636 use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
637 use indoc::indoc;
638 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
639 use lsp::LanguageServerId;
640 use project::{HoverBlock, HoverBlockKind};
641 use smol::stream::StreamExt;
642 use std::sync::atomic;
643 use std::sync::atomic::AtomicUsize;
644 use text::Bias;
645 use unindent::Unindent;
646 use util::test::marked_text_ranges;
647
648 #[gpui::test]
649 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
650 cx: &mut gpui::TestAppContext,
651 ) {
652 init_test(cx, |_| {});
653 const HOVER_DELAY_MILLIS: u64 = 350;
654
655 let mut cx = EditorLspTestContext::new_rust(
656 lsp::ServerCapabilities {
657 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
658 completion_provider: Some(lsp::CompletionOptions {
659 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
660 resolve_provider: Some(true),
661 ..Default::default()
662 }),
663 ..Default::default()
664 },
665 cx,
666 )
667 .await;
668 let counter = Arc::new(AtomicUsize::new(0));
669 // Basic hover delays and then pops without moving the mouse
670 cx.set_state(indoc! {"
671 oneˇ
672 two
673 three
674 fn test() { println!(); }
675 "});
676
677 //prompt autocompletion menu
678 cx.simulate_keystroke(".");
679 handle_completion_request(
680 &mut cx,
681 indoc! {"
682 one.|<>
683 two
684 three
685 "},
686 vec!["first_completion", "second_completion"],
687 counter.clone(),
688 )
689 .await;
690 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
691 .await;
692 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
693
694 let hover_point = cx.display_point(indoc! {"
695 one.
696 two
697 three
698 fn test() { printˇln!(); }
699 "});
700 cx.update_editor(|editor, cx| {
701 let snapshot = editor.snapshot(cx);
702 let anchor = snapshot
703 .buffer_snapshot
704 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
705 hover_at(editor, Some(anchor), cx)
706 });
707 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
708
709 // After delay, hover should be visible.
710 let symbol_range = cx.lsp_range(indoc! {"
711 one.
712 two
713 three
714 fn test() { «println!»(); }
715 "});
716 let mut requests =
717 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
718 Ok(Some(lsp::Hover {
719 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
720 kind: lsp::MarkupKind::Markdown,
721 value: "some basic docs".to_string(),
722 }),
723 range: Some(symbol_range),
724 }))
725 });
726 cx.background_executor
727 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
728 requests.next().await;
729
730 cx.editor(|editor, _| {
731 assert!(editor.hover_state.visible());
732 assert_eq!(
733 editor.hover_state.info_popovers.len(),
734 1,
735 "Expected exactly one hover but got: {:?}",
736 editor.hover_state.info_popovers
737 );
738 let rendered = editor
739 .hover_state
740 .info_popovers
741 .first()
742 .cloned()
743 .unwrap()
744 .parsed_content;
745 assert_eq!(rendered.text, "some basic docs".to_string())
746 });
747
748 // check that the completion menu is still visible and that there still has only been 1 completion request
749 cx.editor(|editor, _| assert!(editor.context_menu_visible()));
750 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
751
752 //apply a completion and check it was successfully applied
753 let _apply_additional_edits = cx.update_editor(|editor, cx| {
754 editor.context_menu_next(&Default::default(), cx);
755 editor
756 .confirm_completion(&ConfirmCompletion::default(), cx)
757 .unwrap()
758 });
759 cx.assert_editor_state(indoc! {"
760 one.second_completionˇ
761 two
762 three
763 fn test() { println!(); }
764 "});
765
766 // check that the completion menu is no longer visible and that there still has only been 1 completion request
767 cx.editor(|editor, _| assert!(!editor.context_menu_visible()));
768 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
769
770 //verify the information popover is still visible and unchanged
771 cx.editor(|editor, _| {
772 assert!(editor.hover_state.visible());
773 assert_eq!(
774 editor.hover_state.info_popovers.len(),
775 1,
776 "Expected exactly one hover but got: {:?}",
777 editor.hover_state.info_popovers
778 );
779 let rendered = editor
780 .hover_state
781 .info_popovers
782 .first()
783 .cloned()
784 .unwrap()
785 .parsed_content;
786 assert_eq!(rendered.text, "some basic docs".to_string())
787 });
788
789 // Mouse moved with no hover response dismisses
790 let hover_point = cx.display_point(indoc! {"
791 one.second_completionˇ
792 two
793 three
794 fn teˇst() { println!(); }
795 "});
796 let mut request = cx
797 .lsp
798 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
799 cx.update_editor(|editor, cx| {
800 let snapshot = editor.snapshot(cx);
801 let anchor = snapshot
802 .buffer_snapshot
803 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
804 hover_at(editor, Some(anchor), cx)
805 });
806 cx.background_executor
807 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
808 request.next().await;
809
810 // verify that the information popover is no longer visible
811 cx.editor(|editor, _| {
812 assert!(!editor.hover_state.visible());
813 });
814 }
815
816 #[gpui::test]
817 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
818 init_test(cx, |_| {});
819
820 let mut cx = EditorLspTestContext::new_rust(
821 lsp::ServerCapabilities {
822 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
823 ..Default::default()
824 },
825 cx,
826 )
827 .await;
828
829 // Basic hover delays and then pops without moving the mouse
830 cx.set_state(indoc! {"
831 fn ˇtest() { println!(); }
832 "});
833 let hover_point = cx.display_point(indoc! {"
834 fn test() { printˇln!(); }
835 "});
836
837 cx.update_editor(|editor, cx| {
838 let snapshot = editor.snapshot(cx);
839 let anchor = snapshot
840 .buffer_snapshot
841 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
842 hover_at(editor, Some(anchor), cx)
843 });
844 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
845
846 // After delay, hover should be visible.
847 let symbol_range = cx.lsp_range(indoc! {"
848 fn test() { «println!»(); }
849 "});
850 let mut requests =
851 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
852 Ok(Some(lsp::Hover {
853 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
854 kind: lsp::MarkupKind::Markdown,
855 value: "some basic docs".to_string(),
856 }),
857 range: Some(symbol_range),
858 }))
859 });
860 cx.background_executor
861 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
862 requests.next().await;
863
864 cx.editor(|editor, _| {
865 assert!(editor.hover_state.visible());
866 assert_eq!(
867 editor.hover_state.info_popovers.len(),
868 1,
869 "Expected exactly one hover but got: {:?}",
870 editor.hover_state.info_popovers
871 );
872 let rendered = editor
873 .hover_state
874 .info_popovers
875 .first()
876 .cloned()
877 .unwrap()
878 .parsed_content;
879 assert_eq!(rendered.text, "some basic docs".to_string())
880 });
881
882 // Mouse moved with no hover response dismisses
883 let hover_point = cx.display_point(indoc! {"
884 fn teˇst() { println!(); }
885 "});
886 let mut request = cx
887 .lsp
888 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
889 cx.update_editor(|editor, cx| {
890 let snapshot = editor.snapshot(cx);
891 let anchor = snapshot
892 .buffer_snapshot
893 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
894 hover_at(editor, Some(anchor), cx)
895 });
896 cx.background_executor
897 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
898 request.next().await;
899 cx.editor(|editor, _| {
900 assert!(!editor.hover_state.visible());
901 });
902 }
903
904 #[gpui::test]
905 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
906 init_test(cx, |_| {});
907
908 let mut cx = EditorLspTestContext::new_rust(
909 lsp::ServerCapabilities {
910 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
911 ..Default::default()
912 },
913 cx,
914 )
915 .await;
916
917 // Hover with keyboard has no delay
918 cx.set_state(indoc! {"
919 fˇn test() { println!(); }
920 "});
921 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
922 let symbol_range = cx.lsp_range(indoc! {"
923 «fn» test() { println!(); }
924 "});
925 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
926 Ok(Some(lsp::Hover {
927 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
928 kind: lsp::MarkupKind::Markdown,
929 value: "some other basic docs".to_string(),
930 }),
931 range: Some(symbol_range),
932 }))
933 })
934 .next()
935 .await;
936
937 cx.condition(|editor, _| editor.hover_state.visible()).await;
938 cx.editor(|editor, _| {
939 assert_eq!(
940 editor.hover_state.info_popovers.len(),
941 1,
942 "Expected exactly one hover but got: {:?}",
943 editor.hover_state.info_popovers
944 );
945 let rendered = editor
946 .hover_state
947 .info_popovers
948 .first()
949 .cloned()
950 .unwrap()
951 .parsed_content;
952 assert_eq!(rendered.text, "some other basic docs".to_string())
953 });
954 }
955
956 #[gpui::test]
957 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
958 init_test(cx, |_| {});
959
960 let mut cx = EditorLspTestContext::new_rust(
961 lsp::ServerCapabilities {
962 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
963 ..Default::default()
964 },
965 cx,
966 )
967 .await;
968
969 // Hover with keyboard has no delay
970 cx.set_state(indoc! {"
971 fˇn test() { println!(); }
972 "});
973 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
974 let symbol_range = cx.lsp_range(indoc! {"
975 «fn» test() { println!(); }
976 "});
977 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
978 Ok(Some(lsp::Hover {
979 contents: lsp::HoverContents::Array(vec![
980 lsp::MarkedString::String("regular text for hover to show".to_string()),
981 lsp::MarkedString::String("".to_string()),
982 lsp::MarkedString::LanguageString(lsp::LanguageString {
983 language: "Rust".to_string(),
984 value: "".to_string(),
985 }),
986 ]),
987 range: Some(symbol_range),
988 }))
989 })
990 .next()
991 .await;
992
993 cx.condition(|editor, _| editor.hover_state.visible()).await;
994 cx.editor(|editor, _| {
995 assert_eq!(
996 editor.hover_state.info_popovers.len(),
997 1,
998 "Expected exactly one hover but got: {:?}",
999 editor.hover_state.info_popovers
1000 );
1001 let rendered = editor
1002 .hover_state
1003 .info_popovers
1004 .first()
1005 .cloned()
1006 .unwrap()
1007 .parsed_content;
1008 assert_eq!(
1009 rendered.text,
1010 "regular text for hover to show".to_string(),
1011 "No empty string hovers should be shown"
1012 );
1013 });
1014 }
1015
1016 #[gpui::test]
1017 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1018 init_test(cx, |_| {});
1019
1020 let mut cx = EditorLspTestContext::new_rust(
1021 lsp::ServerCapabilities {
1022 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1023 ..Default::default()
1024 },
1025 cx,
1026 )
1027 .await;
1028
1029 // Hover with keyboard has no delay
1030 cx.set_state(indoc! {"
1031 fˇn test() { println!(); }
1032 "});
1033 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1034 let symbol_range = cx.lsp_range(indoc! {"
1035 «fn» test() { println!(); }
1036 "});
1037
1038 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1039 let markdown_string = format!("\n```rust\n{code_str}```");
1040
1041 let closure_markdown_string = markdown_string.clone();
1042 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1043 let future_markdown_string = closure_markdown_string.clone();
1044 async move {
1045 Ok(Some(lsp::Hover {
1046 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1047 kind: lsp::MarkupKind::Markdown,
1048 value: future_markdown_string,
1049 }),
1050 range: Some(symbol_range),
1051 }))
1052 }
1053 })
1054 .next()
1055 .await;
1056
1057 cx.condition(|editor, _| editor.hover_state.visible()).await;
1058 cx.editor(|editor, _| {
1059 assert_eq!(
1060 editor.hover_state.info_popovers.len(),
1061 1,
1062 "Expected exactly one hover but got: {:?}",
1063 editor.hover_state.info_popovers
1064 );
1065 let rendered = editor
1066 .hover_state
1067 .info_popovers
1068 .first()
1069 .cloned()
1070 .unwrap()
1071 .parsed_content;
1072 assert_eq!(
1073 rendered.text,
1074 code_str.trim(),
1075 "Should not have extra line breaks at end of rendered hover"
1076 );
1077 });
1078 }
1079
1080 #[gpui::test]
1081 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1082 init_test(cx, |_| {});
1083
1084 let mut cx = EditorLspTestContext::new_rust(
1085 lsp::ServerCapabilities {
1086 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1087 ..Default::default()
1088 },
1089 cx,
1090 )
1091 .await;
1092
1093 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1094 // info popover once request completes
1095 cx.set_state(indoc! {"
1096 fn teˇst() { println!(); }
1097 "});
1098
1099 // Send diagnostic to client
1100 let range = cx.text_anchor_range(indoc! {"
1101 fn «test»() { println!(); }
1102 "});
1103 cx.update_buffer(|buffer, cx| {
1104 let snapshot = buffer.text_snapshot();
1105 let set = DiagnosticSet::from_sorted_entries(
1106 vec![DiagnosticEntry {
1107 range,
1108 diagnostic: Diagnostic {
1109 message: "A test diagnostic message.".to_string(),
1110 ..Default::default()
1111 },
1112 }],
1113 &snapshot,
1114 );
1115 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1116 });
1117
1118 // Hover pops diagnostic immediately
1119 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
1120 cx.background_executor.run_until_parked();
1121
1122 cx.editor(|Editor { hover_state, .. }, _| {
1123 assert!(
1124 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1125 )
1126 });
1127
1128 // Info Popover shows after request responded to
1129 let range = cx.lsp_range(indoc! {"
1130 fn «test»() { println!(); }
1131 "});
1132 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1133 Ok(Some(lsp::Hover {
1134 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1135 kind: lsp::MarkupKind::Markdown,
1136 value: "some new docs".to_string(),
1137 }),
1138 range: Some(range),
1139 }))
1140 });
1141 cx.background_executor
1142 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1143
1144 cx.background_executor.run_until_parked();
1145 cx.editor(|Editor { hover_state, .. }, _| {
1146 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1147 });
1148 }
1149
1150 #[gpui::test]
1151 fn test_render_blocks(cx: &mut gpui::TestAppContext) {
1152 init_test(cx, |_| {});
1153
1154 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1155 let editor = cx.add_window(|cx| Editor::single_line(cx));
1156 editor
1157 .update(cx, |editor, _cx| {
1158 let style = editor.style.clone().unwrap();
1159
1160 struct Row {
1161 blocks: Vec<HoverBlock>,
1162 expected_marked_text: String,
1163 expected_styles: Vec<HighlightStyle>,
1164 }
1165
1166 let rows = &[
1167 // Strong emphasis
1168 Row {
1169 blocks: vec![HoverBlock {
1170 text: "one **two** three".to_string(),
1171 kind: HoverBlockKind::Markdown,
1172 }],
1173 expected_marked_text: "one «two» three".to_string(),
1174 expected_styles: vec![HighlightStyle {
1175 font_weight: Some(FontWeight::BOLD),
1176 ..Default::default()
1177 }],
1178 },
1179 // Links
1180 Row {
1181 blocks: vec three".to_string(),
1183 kind: HoverBlockKind::Markdown,
1184 }],
1185 expected_marked_text: "one «two» three".to_string(),
1186 expected_styles: vec![HighlightStyle {
1187 underline: Some(UnderlineStyle {
1188 thickness: 1.0.into(),
1189 ..Default::default()
1190 }),
1191 ..Default::default()
1192 }],
1193 },
1194 // Lists
1195 Row {
1196 blocks: vec
1204 - d"
1205 .unindent(),
1206 kind: HoverBlockKind::Markdown,
1207 }],
1208 expected_marked_text: "
1209 lists:
1210 - one
1211 - a
1212 - b
1213 - two
1214 - «c»
1215 - d"
1216 .unindent(),
1217 expected_styles: vec![HighlightStyle {
1218 underline: Some(UnderlineStyle {
1219 thickness: 1.0.into(),
1220 ..Default::default()
1221 }),
1222 ..Default::default()
1223 }],
1224 },
1225 // Multi-paragraph list items
1226 Row {
1227 blocks: vec![HoverBlock {
1228 text: "
1229 * one two
1230 three
1231
1232 * four five
1233 * six seven
1234 eight
1235
1236 nine
1237 * ten
1238 * six"
1239 .unindent(),
1240 kind: HoverBlockKind::Markdown,
1241 }],
1242 expected_marked_text: "
1243 - one two three
1244 - four five
1245 - six seven eight
1246
1247 nine
1248 - ten
1249 - six"
1250 .unindent(),
1251 expected_styles: vec![HighlightStyle {
1252 underline: Some(UnderlineStyle {
1253 thickness: 1.0.into(),
1254 ..Default::default()
1255 }),
1256 ..Default::default()
1257 }],
1258 },
1259 ];
1260
1261 for Row {
1262 blocks,
1263 expected_marked_text,
1264 expected_styles,
1265 } in &rows[0..]
1266 {
1267 let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
1268
1269 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
1270 let expected_highlights = ranges
1271 .into_iter()
1272 .zip(expected_styles.iter().cloned())
1273 .collect::<Vec<_>>();
1274 assert_eq!(
1275 rendered.text, expected_text,
1276 "wrong text for input {blocks:?}"
1277 );
1278
1279 let rendered_highlights: Vec<_> = rendered
1280 .highlights
1281 .iter()
1282 .filter_map(|(range, highlight)| {
1283 let highlight = highlight.to_highlight_style(&style.syntax)?;
1284 Some((range.clone(), highlight))
1285 })
1286 .collect();
1287
1288 assert_eq!(
1289 rendered_highlights, expected_highlights,
1290 "wrong highlights for input {blocks:?}"
1291 );
1292 }
1293 })
1294 .unwrap();
1295 }
1296
1297 #[gpui::test]
1298 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1299 init_test(cx, |settings| {
1300 settings.defaults.inlay_hints = Some(InlayHintSettings {
1301 enabled: true,
1302 edit_debounce_ms: 0,
1303 scroll_debounce_ms: 0,
1304 show_type_hints: true,
1305 show_parameter_hints: true,
1306 show_other_hints: true,
1307 })
1308 });
1309
1310 let mut cx = EditorLspTestContext::new_rust(
1311 lsp::ServerCapabilities {
1312 inlay_hint_provider: Some(lsp::OneOf::Right(
1313 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1314 resolve_provider: Some(true),
1315 ..Default::default()
1316 }),
1317 )),
1318 ..Default::default()
1319 },
1320 cx,
1321 )
1322 .await;
1323
1324 cx.set_state(indoc! {"
1325 struct TestStruct;
1326
1327 // ==================
1328
1329 struct TestNewType<T>(T);
1330
1331 fn main() {
1332 let variableˇ = TestNewType(TestStruct);
1333 }
1334 "});
1335
1336 let hint_start_offset = cx.ranges(indoc! {"
1337 struct TestStruct;
1338
1339 // ==================
1340
1341 struct TestNewType<T>(T);
1342
1343 fn main() {
1344 let variableˇ = TestNewType(TestStruct);
1345 }
1346 "})[0]
1347 .start;
1348 let hint_position = cx.to_lsp(hint_start_offset);
1349 let new_type_target_range = cx.lsp_range(indoc! {"
1350 struct TestStruct;
1351
1352 // ==================
1353
1354 struct «TestNewType»<T>(T);
1355
1356 fn main() {
1357 let variable = TestNewType(TestStruct);
1358 }
1359 "});
1360 let struct_target_range = cx.lsp_range(indoc! {"
1361 struct «TestStruct»;
1362
1363 // ==================
1364
1365 struct TestNewType<T>(T);
1366
1367 fn main() {
1368 let variable = TestNewType(TestStruct);
1369 }
1370 "});
1371
1372 let uri = cx.buffer_lsp_url.clone();
1373 let new_type_label = "TestNewType";
1374 let struct_label = "TestStruct";
1375 let entire_hint_label = ": TestNewType<TestStruct>";
1376 let closure_uri = uri.clone();
1377 cx.lsp
1378 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1379 let task_uri = closure_uri.clone();
1380 async move {
1381 assert_eq!(params.text_document.uri, task_uri);
1382 Ok(Some(vec![lsp::InlayHint {
1383 position: hint_position,
1384 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1385 value: entire_hint_label.to_string(),
1386 ..Default::default()
1387 }]),
1388 kind: Some(lsp::InlayHintKind::TYPE),
1389 text_edits: None,
1390 tooltip: None,
1391 padding_left: Some(false),
1392 padding_right: Some(false),
1393 data: None,
1394 }]))
1395 }
1396 })
1397 .next()
1398 .await;
1399 cx.background_executor.run_until_parked();
1400 cx.update_editor(|editor, cx| {
1401 let expected_layers = vec![entire_hint_label.to_string()];
1402 assert_eq!(expected_layers, cached_hint_labels(editor));
1403 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1404 });
1405
1406 let inlay_range = cx
1407 .ranges(indoc! {"
1408 struct TestStruct;
1409
1410 // ==================
1411
1412 struct TestNewType<T>(T);
1413
1414 fn main() {
1415 let variable« »= TestNewType(TestStruct);
1416 }
1417 "})
1418 .get(0)
1419 .cloned()
1420 .unwrap();
1421 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1422 let snapshot = editor.snapshot(cx);
1423 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1424 let next_valid = inlay_range.end.to_display_point(&snapshot);
1425 assert_eq!(previous_valid.row(), next_valid.row());
1426 assert!(previous_valid.column() < next_valid.column());
1427 let exact_unclipped = DisplayPoint::new(
1428 previous_valid.row(),
1429 previous_valid.column()
1430 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1431 as u32,
1432 );
1433 PointForPosition {
1434 previous_valid,
1435 next_valid,
1436 exact_unclipped,
1437 column_overshoot_after_line_end: 0,
1438 }
1439 });
1440 cx.update_editor(|editor, cx| {
1441 update_inlay_link_and_hover_points(
1442 &editor.snapshot(cx),
1443 new_type_hint_part_hover_position,
1444 editor,
1445 true,
1446 false,
1447 cx,
1448 );
1449 });
1450
1451 let resolve_closure_uri = uri.clone();
1452 cx.lsp
1453 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1454 move |mut hint_to_resolve, _| {
1455 let mut resolved_hint_positions = BTreeSet::new();
1456 let task_uri = resolve_closure_uri.clone();
1457 async move {
1458 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1459 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1460
1461 // `: TestNewType<TestStruct>`
1462 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1463 lsp::InlayHintLabelPart {
1464 value: ": ".to_string(),
1465 ..Default::default()
1466 },
1467 lsp::InlayHintLabelPart {
1468 value: new_type_label.to_string(),
1469 location: Some(lsp::Location {
1470 uri: task_uri.clone(),
1471 range: new_type_target_range,
1472 }),
1473 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1474 "A tooltip for `{new_type_label}`"
1475 ))),
1476 ..Default::default()
1477 },
1478 lsp::InlayHintLabelPart {
1479 value: "<".to_string(),
1480 ..Default::default()
1481 },
1482 lsp::InlayHintLabelPart {
1483 value: struct_label.to_string(),
1484 location: Some(lsp::Location {
1485 uri: task_uri,
1486 range: struct_target_range,
1487 }),
1488 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1489 lsp::MarkupContent {
1490 kind: lsp::MarkupKind::Markdown,
1491 value: format!("A tooltip for `{struct_label}`"),
1492 },
1493 )),
1494 ..Default::default()
1495 },
1496 lsp::InlayHintLabelPart {
1497 value: ">".to_string(),
1498 ..Default::default()
1499 },
1500 ]);
1501
1502 Ok(hint_to_resolve)
1503 }
1504 },
1505 )
1506 .next()
1507 .await;
1508 cx.background_executor.run_until_parked();
1509
1510 cx.update_editor(|editor, cx| {
1511 update_inlay_link_and_hover_points(
1512 &editor.snapshot(cx),
1513 new_type_hint_part_hover_position,
1514 editor,
1515 true,
1516 false,
1517 cx,
1518 );
1519 });
1520 cx.background_executor
1521 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1522 cx.background_executor.run_until_parked();
1523 cx.update_editor(|editor, cx| {
1524 let hover_state = &editor.hover_state;
1525 assert!(
1526 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1527 );
1528 let popover = hover_state.info_popovers.first().cloned().unwrap();
1529 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1530 assert_eq!(
1531 popover.symbol_range,
1532 RangeInEditor::Inlay(InlayHighlight {
1533 inlay: InlayId::Hint(0),
1534 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1535 range: ": ".len()..": ".len() + new_type_label.len(),
1536 }),
1537 "Popover range should match the new type label part"
1538 );
1539 assert_eq!(
1540 popover.parsed_content.text,
1541 format!("A tooltip for `{new_type_label}`"),
1542 "Rendered text should not anyhow alter backticks"
1543 );
1544 });
1545
1546 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1547 let snapshot = editor.snapshot(cx);
1548 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1549 let next_valid = inlay_range.end.to_display_point(&snapshot);
1550 assert_eq!(previous_valid.row(), next_valid.row());
1551 assert!(previous_valid.column() < next_valid.column());
1552 let exact_unclipped = DisplayPoint::new(
1553 previous_valid.row(),
1554 previous_valid.column()
1555 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1556 as u32,
1557 );
1558 PointForPosition {
1559 previous_valid,
1560 next_valid,
1561 exact_unclipped,
1562 column_overshoot_after_line_end: 0,
1563 }
1564 });
1565 cx.update_editor(|editor, cx| {
1566 update_inlay_link_and_hover_points(
1567 &editor.snapshot(cx),
1568 struct_hint_part_hover_position,
1569 editor,
1570 true,
1571 false,
1572 cx,
1573 );
1574 });
1575 cx.background_executor
1576 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1577 cx.background_executor.run_until_parked();
1578 cx.update_editor(|editor, cx| {
1579 let hover_state = &editor.hover_state;
1580 assert!(
1581 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1582 );
1583 let popover = hover_state.info_popovers.first().cloned().unwrap();
1584 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1585 assert_eq!(
1586 popover.symbol_range,
1587 RangeInEditor::Inlay(InlayHighlight {
1588 inlay: InlayId::Hint(0),
1589 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1590 range: ": ".len() + new_type_label.len() + "<".len()
1591 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1592 }),
1593 "Popover range should match the struct label part"
1594 );
1595 assert_eq!(
1596 popover.parsed_content.text,
1597 format!("A tooltip for {struct_label}"),
1598 "Rendered markdown element should remove backticks from text"
1599 );
1600 });
1601 }
1602}