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