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