1use crate::{
2 ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
3 EditorSnapshot, GlobalDiagnosticRenderer, HighlightKey, Hover,
4 display_map::{InlayOffset, ToDisplayPoint, is_invisible},
5 editor_settings::EditorSettingsScrollbarProxy,
6 hover_links::{InlayHighlight, RangeInEditor},
7 movement::TextLayoutDetails,
8 scroll::ScrollAmount,
9};
10use anyhow::Context as _;
11use gpui::{
12 AnyElement, App, AsyncWindowContext, Bounds, Context, Entity, Focusable as _, FontWeight, Hsla,
13 InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
14 StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
15 Window, canvas, div, px,
16};
17use itertools::Itertools;
18use language::{DiagnosticEntry, Language, LanguageRegistry};
19use lsp::DiagnosticSeverity;
20use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
21use multi_buffer::{MultiBufferOffset, ToOffset, ToPoint};
22use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
23use settings::Settings;
24use std::{
25 borrow::Cow,
26 cell::{Cell, RefCell},
27};
28use std::{ops::Range, sync::Arc, time::Duration};
29use std::{path::PathBuf, rc::Rc};
30use theme_settings::ThemeSettings;
31use ui::{CopyButton, Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
32use url::Url;
33use util::TryFutureExt;
34use workspace::{OpenOptions, OpenVisible, Workspace};
35
36pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
37pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
38pub const POPOVER_RIGHT_OFFSET: Pixels = px(8.0);
39pub const HOVER_POPOVER_GAP: Pixels = px(10.);
40
41/// Bindable action which uses the most recent selection head to trigger a hover
42pub fn hover(editor: &mut Editor, _: &Hover, window: &mut Window, cx: &mut Context<Editor>) {
43 let head = editor.selections.newest_anchor().head();
44 show_hover(editor, head, true, window, cx);
45}
46
47/// The internal hover action dispatches between `show_hover` or `hide_hover`
48/// depending on whether a point to hover over is provided.
49pub fn hover_at(
50 editor: &mut Editor,
51 anchor: Option<Anchor>,
52 mouse_position: Option<gpui::Point<Pixels>>,
53 window: &mut Window,
54 cx: &mut Context<Editor>,
55) {
56 if EditorSettings::get_global(cx).hover_popover_enabled {
57 if show_keyboard_hover(editor, window, cx) {
58 return;
59 }
60
61 if let Some(anchor) = anchor {
62 editor.hover_state.hiding_delay_task = None;
63 editor.hover_state.closest_mouse_distance = None;
64 show_hover(editor, anchor, false, window, cx);
65 } else if !editor.hover_state.visible() {
66 editor.hover_state.info_task = None;
67 } else {
68 let settings = EditorSettings::get_global(cx);
69 if !settings.hover_popover_sticky {
70 hide_hover(editor, cx);
71 return;
72 }
73
74 let mut getting_closer = false;
75 if let Some(mouse_position) = mouse_position {
76 getting_closer = editor.hover_state.is_mouse_getting_closer(mouse_position);
77 }
78
79 // If we are moving away and a timer is already running, just let it count down.
80 if !getting_closer && editor.hover_state.hiding_delay_task.is_some() {
81 return;
82 }
83
84 // If we are moving closer, or if no timer is running at all, start/restart the timer.
85 let delay = Duration::from_millis(settings.hover_popover_hiding_delay.0);
86 let task = cx.spawn(async move |this, cx| {
87 cx.background_executor().timer(delay).await;
88 this.update(cx, |editor, cx| {
89 hide_hover(editor, cx);
90 })
91 .ok();
92 });
93 editor.hover_state.hiding_delay_task = Some(task);
94 }
95 }
96}
97
98pub fn show_keyboard_hover(
99 editor: &mut Editor,
100 window: &mut Window,
101 cx: &mut Context<Editor>,
102) -> bool {
103 if let Some(anchor) = editor.hover_state.info_popovers.iter().find_map(|p| {
104 if *p.keyboard_grace.borrow() {
105 p.anchor
106 } else {
107 None
108 }
109 }) {
110 show_hover(editor, anchor, false, window, cx);
111 return true;
112 }
113
114 if let Some(anchor) = editor
115 .hover_state
116 .diagnostic_popover
117 .as_ref()
118 .and_then(|d| {
119 if *d.keyboard_grace.borrow() {
120 Some(d.anchor)
121 } else {
122 None
123 }
124 })
125 {
126 show_hover(editor, anchor, false, window, cx);
127 return true;
128 }
129
130 false
131}
132
133pub struct InlayHover {
134 pub(crate) range: InlayHighlight,
135 pub tooltip: HoverBlock,
136}
137
138pub fn find_hovered_hint_part(
139 label_parts: Vec<InlayHintLabelPart>,
140 hint_start: InlayOffset,
141 hovered_offset: InlayOffset,
142) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
143 if hovered_offset >= hint_start {
144 let mut offset_in_hint = hovered_offset - hint_start;
145 let mut part_start = hint_start;
146 for part in label_parts {
147 let part_len = part.value.len();
148 if offset_in_hint >= part_len {
149 offset_in_hint -= part_len;
150 part_start.0 += part_len;
151 } else {
152 let part_end = InlayOffset(part_start.0 + part_len);
153 return Some((part, part_start..part_end));
154 }
155 }
156 }
157 None
158}
159
160pub fn hover_at_inlay(
161 editor: &mut Editor,
162 inlay_hover: InlayHover,
163 window: &mut Window,
164 cx: &mut Context<Editor>,
165) {
166 if EditorSettings::get_global(cx).hover_popover_enabled {
167 if editor.pending_rename.is_some() {
168 return;
169 }
170
171 let Some(project) = editor.project.clone() else {
172 return;
173 };
174
175 if editor
176 .hover_state
177 .info_popovers
178 .iter()
179 .any(|InfoPopover { symbol_range, .. }| {
180 if let RangeInEditor::Inlay(range) = symbol_range
181 && range == &inlay_hover.range
182 {
183 // Hover triggered from same location as last time. Don't show again.
184 return true;
185 }
186 false
187 })
188 {
189 return;
190 }
191
192 let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
193
194 editor.hover_state.hiding_delay_task = None;
195 editor.hover_state.closest_mouse_distance = None;
196
197 let task = cx.spawn_in(window, async move |this, cx| {
198 async move {
199 cx.background_executor()
200 .timer(Duration::from_millis(hover_popover_delay))
201 .await;
202 this.update(cx, |this, _| {
203 this.hover_state.diagnostic_popover = None;
204 })?;
205
206 let language_registry = project.read_with(cx, |p, _| p.languages().clone());
207 let blocks = vec![inlay_hover.tooltip];
208 let parsed_content =
209 parse_blocks(&blocks, Some(&language_registry), None, cx).await;
210
211 let scroll_handle = ScrollHandle::new();
212
213 let subscription = this
214 .update(cx, |_, cx| {
215 parsed_content.as_ref().map(|parsed_content| {
216 cx.observe(parsed_content, |_, _, cx| cx.notify())
217 })
218 })
219 .ok()
220 .flatten();
221
222 let hover_popover = InfoPopover {
223 symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
224 parsed_content,
225 scroll_handle,
226 keyboard_grace: Rc::new(RefCell::new(false)),
227 anchor: None,
228 last_bounds: Rc::new(Cell::new(None)),
229 _subscription: subscription,
230 };
231
232 this.update(cx, |this, cx| {
233 // TODO: no background highlights happen for inlays currently
234 this.hover_state.info_popovers = vec![hover_popover];
235 cx.notify();
236 })?;
237
238 anyhow::Ok(())
239 }
240 .log_err()
241 .await
242 });
243
244 editor.hover_state.info_task = Some(task);
245 }
246}
247
248/// Hides the type information popup.
249/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
250/// selections changed.
251pub fn hide_hover(editor: &mut Editor, cx: &mut Context<Editor>) -> bool {
252 let info_popovers = editor.hover_state.info_popovers.drain(..);
253 let diagnostics_popover = editor.hover_state.diagnostic_popover.take();
254 let did_hide = info_popovers.count() > 0 || diagnostics_popover.is_some();
255
256 editor.hover_state.info_task = None;
257 editor.hover_state.triggered_from = None;
258 editor.hover_state.hiding_delay_task = None;
259 editor.hover_state.closest_mouse_distance = None;
260
261 editor.clear_background_highlights(HighlightKey::HoverState, cx);
262
263 if did_hide {
264 cx.notify();
265 }
266
267 did_hide
268}
269
270/// Queries the LSP and shows type info and documentation
271/// about the symbol the mouse is currently hovering over.
272/// Triggered by the `Hover` action when the cursor may be over a symbol.
273fn show_hover(
274 editor: &mut Editor,
275 anchor: Anchor,
276 ignore_timeout: bool,
277 window: &mut Window,
278 cx: &mut Context<Editor>,
279) -> Option<()> {
280 if editor.pending_rename.is_some() {
281 return None;
282 }
283
284 let snapshot = editor.snapshot(window, cx);
285
286 let (buffer_position, _) = editor
287 .buffer
288 .read(cx)
289 .snapshot(cx)
290 .anchor_to_buffer_anchor(anchor)?;
291 let buffer = editor.buffer.read(cx).buffer(buffer_position.buffer_id)?;
292
293 let language_registry = editor
294 .project()
295 .map(|project| project.read(cx).languages().clone());
296 let provider = editor.semantics_provider.clone()?;
297
298 editor.hover_state.hiding_delay_task = None;
299 editor.hover_state.closest_mouse_distance = None;
300
301 if !ignore_timeout {
302 if same_info_hover(editor, &snapshot, anchor)
303 || same_diagnostic_hover(editor, &snapshot, anchor)
304 || editor.hover_state.diagnostic_popover.is_some()
305 {
306 // Hover triggered from same location as last time. Don't show again.
307 return None;
308 } else {
309 hide_hover(editor, cx);
310 }
311 }
312
313 // Don't request again if the location is the same as the previous request
314 if let Some(triggered_from) = &editor.hover_state.triggered_from
315 && triggered_from
316 .cmp(&anchor, &snapshot.buffer_snapshot())
317 .is_eq()
318 {
319 return None;
320 }
321
322 let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
323 let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
324 let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics {
325 Some(group.group_id)
326 } else {
327 None
328 };
329
330 let renderer = GlobalDiagnosticRenderer::global(cx);
331 let task = cx.spawn_in(window, async move |this, cx| {
332 async move {
333 // If we need to delay, delay a set amount initially before making the lsp request
334 let delay = if ignore_timeout {
335 None
336 } else {
337 let lsp_request_early = hover_popover_delay / 2;
338 cx.background_executor()
339 .timer(Duration::from_millis(
340 hover_popover_delay - lsp_request_early,
341 ))
342 .await;
343
344 // Construct delay task to wait for later
345 let total_delay = Some(
346 cx.background_executor()
347 .timer(Duration::from_millis(lsp_request_early)),
348 );
349 total_delay
350 };
351
352 let hover_request = cx.update(|_, cx| provider.hover(&buffer, buffer_position, cx))?;
353
354 if let Some(delay) = delay {
355 delay.await;
356 }
357 let offset = anchor.to_offset(&snapshot.buffer_snapshot());
358 let local_diagnostic = if all_diagnostics_active {
359 None
360 } else {
361 snapshot
362 .buffer_snapshot()
363 .diagnostics_with_buffer_ids_in_range::<MultiBufferOffset>(offset..offset)
364 .filter(|(_, diagnostic)| {
365 Some(diagnostic.diagnostic.group_id) != active_group_id
366 })
367 // Find the entry with the most specific range
368 .min_by_key(|(_, entry)| entry.range.end - entry.range.start)
369 };
370
371 let diagnostic_popover = if let Some((buffer_id, local_diagnostic)) = local_diagnostic {
372 let group = snapshot
373 .buffer_snapshot()
374 .diagnostic_group(buffer_id, local_diagnostic.diagnostic.group_id)
375 .collect::<Vec<_>>();
376 let point_range = local_diagnostic
377 .range
378 .start
379 .to_point(&snapshot.buffer_snapshot())
380 ..local_diagnostic
381 .range
382 .end
383 .to_point(&snapshot.buffer_snapshot());
384 let markdown = cx.update(|_, cx| {
385 renderer
386 .as_ref()
387 .and_then(|renderer| {
388 renderer.render_hover(
389 group,
390 point_range,
391 buffer_id,
392 language_registry.clone(),
393 cx,
394 )
395 })
396 .context("no rendered diagnostic")
397 })??;
398
399 let (background_color, border_color) = cx.update(|_, cx| {
400 let status_colors = cx.theme().status();
401 match local_diagnostic.diagnostic.severity {
402 DiagnosticSeverity::ERROR => {
403 (status_colors.error_background, status_colors.error_border)
404 }
405 DiagnosticSeverity::WARNING => (
406 status_colors.warning_background,
407 status_colors.warning_border,
408 ),
409 DiagnosticSeverity::INFORMATION => {
410 (status_colors.info_background, status_colors.info_border)
411 }
412 DiagnosticSeverity::HINT => {
413 (status_colors.hint_background, status_colors.hint_border)
414 }
415 _ => (
416 status_colors.ignored_background,
417 status_colors.ignored_border,
418 ),
419 }
420 })?;
421
422 let subscription =
423 this.update(cx, |_, cx| cx.observe(&markdown, |_, _, cx| cx.notify()))?;
424
425 let local_diagnostic = DiagnosticEntry {
426 diagnostic: local_diagnostic.diagnostic.to_owned(),
427 range: snapshot
428 .buffer_snapshot()
429 .anchor_before(local_diagnostic.range.start)
430 ..snapshot
431 .buffer_snapshot()
432 .anchor_after(local_diagnostic.range.end),
433 };
434
435 let scroll_handle = ScrollHandle::new();
436
437 Some(DiagnosticPopover {
438 local_diagnostic,
439 markdown,
440 border_color,
441 scroll_handle,
442 background_color,
443 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
444 anchor,
445 last_bounds: Rc::new(Cell::new(None)),
446 _subscription: subscription,
447 })
448 } else {
449 None
450 };
451
452 this.update(cx, |this, _| {
453 this.hover_state.diagnostic_popover = diagnostic_popover;
454 })?;
455
456 let invisible_char = if let Some(invisible) = snapshot
457 .buffer_snapshot()
458 .chars_at(anchor)
459 .next()
460 .filter(|&c| is_invisible(c))
461 {
462 let after = snapshot.buffer_snapshot().anchor_after(
463 anchor.to_offset(&snapshot.buffer_snapshot()) + invisible.len_utf8(),
464 );
465 Some((invisible, anchor..after))
466 } else if let Some(invisible) = snapshot
467 .buffer_snapshot()
468 .reversed_chars_at(anchor)
469 .next()
470 .filter(|&c| is_invisible(c))
471 {
472 let before = snapshot.buffer_snapshot().anchor_before(
473 anchor.to_offset(&snapshot.buffer_snapshot()) - invisible.len_utf8(),
474 );
475
476 Some((invisible, before..anchor))
477 } else {
478 None
479 };
480
481 let hovers_response = if let Some(hover_request) = hover_request {
482 hover_request.await.unwrap_or_default()
483 } else {
484 Vec::new()
485 };
486 let snapshot = this.update_in(cx, |this, window, cx| this.snapshot(window, cx))?;
487 let mut hover_highlights = Vec::with_capacity(hovers_response.len());
488 let mut info_popovers = Vec::with_capacity(
489 hovers_response.len() + if invisible_char.is_some() { 1 } else { 0 },
490 );
491
492 if let Some((invisible, range)) = invisible_char {
493 let blocks = vec![HoverBlock {
494 text: format!("Unicode character U+{:02X}", invisible as u32),
495 kind: HoverBlockKind::PlainText,
496 }];
497 let parsed_content =
498 parse_blocks(&blocks, language_registry.as_ref(), None, cx).await;
499 let scroll_handle = ScrollHandle::new();
500 let subscription = this
501 .update(cx, |_, cx| {
502 parsed_content.as_ref().map(|parsed_content| {
503 cx.observe(parsed_content, |_, _, cx| cx.notify())
504 })
505 })
506 .ok()
507 .flatten();
508 info_popovers.push(InfoPopover {
509 symbol_range: RangeInEditor::Text(range),
510 parsed_content,
511 scroll_handle,
512 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
513 anchor: Some(anchor),
514 last_bounds: Rc::new(Cell::new(None)),
515 _subscription: subscription,
516 })
517 }
518
519 for hover_result in hovers_response {
520 // Create symbol range of anchors for highlighting and filtering of future requests.
521 let range = hover_result
522 .range
523 .and_then(|range| {
524 let range = snapshot
525 .buffer_snapshot()
526 .buffer_anchor_range_to_anchor_range(range)?;
527 Some(range)
528 })
529 .or_else(|| {
530 let snapshot = &snapshot.buffer_snapshot();
531 let range = snapshot.syntax_ancestor(anchor..anchor)?.1;
532 Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
533 })
534 .unwrap_or_else(|| anchor..anchor);
535
536 let blocks = hover_result.contents;
537 let language = hover_result.language;
538 let parsed_content =
539 parse_blocks(&blocks, language_registry.as_ref(), language, cx).await;
540 let scroll_handle = ScrollHandle::new();
541 hover_highlights.push(range.clone());
542 let subscription = this
543 .update(cx, |_, cx| {
544 parsed_content.as_ref().map(|parsed_content| {
545 cx.observe(parsed_content, |_, _, cx| cx.notify())
546 })
547 })
548 .ok()
549 .flatten();
550 info_popovers.push(InfoPopover {
551 symbol_range: RangeInEditor::Text(range),
552 parsed_content,
553 scroll_handle,
554 keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
555 anchor: Some(anchor),
556 last_bounds: Rc::new(Cell::new(None)),
557 _subscription: subscription,
558 });
559 }
560
561 this.update_in(cx, |editor, window, cx| {
562 if hover_highlights.is_empty() {
563 editor.clear_background_highlights(HighlightKey::HoverState, cx);
564 } else {
565 // Highlight the selected symbol using a background highlight
566 editor.highlight_background(
567 HighlightKey::HoverState,
568 &hover_highlights,
569 |_, theme| theme.colors().element_hover, // todo update theme
570 cx,
571 );
572 }
573
574 editor.hover_state.info_popovers = info_popovers;
575 cx.notify();
576 window.refresh();
577 })?;
578
579 anyhow::Ok(())
580 }
581 .log_err()
582 .await
583 });
584
585 editor.hover_state.info_task = Some(task);
586 None
587}
588
589fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
590 editor
591 .hover_state
592 .info_popovers
593 .iter()
594 .any(|InfoPopover { symbol_range, .. }| {
595 symbol_range
596 .as_text_range()
597 .map(|range| {
598 let hover_range = range.to_offset(&snapshot.buffer_snapshot());
599 let offset = anchor.to_offset(&snapshot.buffer_snapshot());
600 // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
601 // use an inclusive range here to check if we should dismiss the popover
602 (hover_range.start..=hover_range.end).contains(&offset)
603 })
604 .unwrap_or(false)
605 })
606}
607
608fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
609 editor
610 .hover_state
611 .diagnostic_popover
612 .as_ref()
613 .map(|diagnostic| {
614 let hover_range = diagnostic
615 .local_diagnostic
616 .range
617 .to_offset(&snapshot.buffer_snapshot());
618 let offset = anchor.to_offset(&snapshot.buffer_snapshot());
619
620 // Here we do basically the same as in `same_info_hover`, see comment there for an explanation
621 (hover_range.start..=hover_range.end).contains(&offset)
622 })
623 .unwrap_or(false)
624}
625
626async fn parse_blocks(
627 blocks: &[HoverBlock],
628 language_registry: Option<&Arc<LanguageRegistry>>,
629 language: Option<Arc<Language>>,
630 cx: &mut AsyncWindowContext,
631) -> Option<Entity<Markdown>> {
632 let combined_text = blocks
633 .iter()
634 .map(|block| match &block.kind {
635 project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
636 Cow::Borrowed(block.text.trim())
637 }
638 project::HoverBlockKind::Code { language } => {
639 Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
640 }
641 })
642 .join("\n\n");
643
644 cx.new_window_entity(|_window, cx| {
645 Markdown::new(
646 combined_text.into(),
647 language_registry.cloned(),
648 language.map(|language| language.name()),
649 cx,
650 )
651 })
652 .ok()
653}
654
655pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
656 let settings = ThemeSettings::get_global(cx);
657 let ui_font_family = settings.ui_font.family.clone();
658 let ui_font_features = settings.ui_font.features.clone();
659 let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
660 let buffer_font_family = settings.buffer_font.family.clone();
661 let buffer_font_features = settings.buffer_font.features.clone();
662 let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
663 let buffer_font_weight = settings.buffer_font.weight;
664
665 let mut base_text_style = window.text_style();
666 base_text_style.refine(&TextStyleRefinement {
667 font_family: Some(ui_font_family),
668 font_features: Some(ui_font_features),
669 font_fallbacks: ui_font_fallbacks,
670 color: Some(cx.theme().colors().editor_foreground),
671 ..Default::default()
672 });
673 MarkdownStyle {
674 base_text_style,
675 code_block: StyleRefinement::default()
676 .my(rems(1.))
677 .font_buffer(cx)
678 .font_features(buffer_font_features.clone())
679 .font_weight(buffer_font_weight),
680 inline_code: TextStyleRefinement {
681 background_color: Some(cx.theme().colors().background),
682 font_family: Some(buffer_font_family),
683 font_features: Some(buffer_font_features),
684 font_fallbacks: buffer_font_fallbacks,
685 font_weight: Some(buffer_font_weight),
686 ..Default::default()
687 },
688 rule_color: cx.theme().colors().border,
689 block_quote_border_color: Color::Muted.color(cx),
690 block_quote: TextStyleRefinement {
691 color: Some(Color::Muted.color(cx)),
692 ..Default::default()
693 },
694 link: TextStyleRefinement {
695 color: Some(cx.theme().colors().editor_foreground),
696 underline: Some(gpui::UnderlineStyle {
697 thickness: px(1.),
698 color: Some(cx.theme().colors().editor_foreground),
699 wavy: false,
700 }),
701 ..Default::default()
702 },
703 syntax: cx.theme().syntax().clone(),
704 selection_background_color: cx.theme().colors().element_selection_background,
705 heading: StyleRefinement::default()
706 .font_weight(FontWeight::BOLD)
707 .text_base()
708 .mt(rems(1.))
709 .mb_0(),
710 table_columns_min_size: true,
711 ..Default::default()
712 }
713}
714
715pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
716 let settings = ThemeSettings::get_global(cx);
717 let ui_font_family = settings.ui_font.family.clone();
718 let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
719 let ui_font_features = settings.ui_font.features.clone();
720 let buffer_font_family = settings.buffer_font.family.clone();
721 let buffer_font_features = settings.buffer_font.features.clone();
722 let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
723
724 let mut base_text_style = window.text_style();
725 base_text_style.refine(&TextStyleRefinement {
726 font_family: Some(ui_font_family),
727 font_features: Some(ui_font_features),
728 font_fallbacks: ui_font_fallbacks,
729 color: Some(cx.theme().colors().editor_foreground),
730 ..Default::default()
731 });
732 MarkdownStyle {
733 base_text_style,
734 code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
735 inline_code: TextStyleRefinement {
736 background_color: Some(cx.theme().colors().editor_background.opacity(0.5)),
737 font_family: Some(buffer_font_family),
738 font_features: Some(buffer_font_features),
739 font_fallbacks: buffer_font_fallbacks,
740 ..Default::default()
741 },
742 rule_color: cx.theme().colors().border,
743 block_quote_border_color: Color::Muted.color(cx),
744 block_quote: TextStyleRefinement {
745 color: Some(Color::Muted.color(cx)),
746 ..Default::default()
747 },
748 link: TextStyleRefinement {
749 color: Some(cx.theme().colors().editor_foreground),
750 underline: Some(gpui::UnderlineStyle {
751 thickness: px(1.),
752 color: Some(cx.theme().colors().editor_foreground),
753 wavy: false,
754 }),
755 ..Default::default()
756 },
757 syntax: cx.theme().syntax().clone(),
758 selection_background_color: cx.theme().colors().element_selection_background,
759 height_is_multiple_of_line_height: true,
760 heading: StyleRefinement::default()
761 .font_weight(FontWeight::BOLD)
762 .text_base()
763 .mb_0(),
764 table_columns_min_size: true,
765 ..Default::default()
766 }
767}
768
769pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
770 if let Ok(uri) = Url::parse(&link)
771 && uri.scheme() == "file"
772 && let Some(workspace) = Workspace::for_window(window, cx)
773 {
774 workspace.update(cx, |workspace, cx| {
775 let task = workspace.open_abs_path(
776 PathBuf::from(uri.path()),
777 OpenOptions {
778 visible: Some(OpenVisible::None),
779 ..Default::default()
780 },
781 window,
782 cx,
783 );
784
785 cx.spawn_in(window, async move |_, cx| {
786 let item = task.await?;
787 // Ruby LSP uses URLs with #L1,1-4,4
788 // we'll just take the first number and assume it's a line number
789 let Some(fragment) = uri.fragment() else {
790 return anyhow::Ok(());
791 };
792 let mut accum = 0u32;
793 for c in fragment.chars() {
794 if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
795 accum *= 10;
796 accum += c as u32 - '0' as u32;
797 } else if accum > 0 {
798 break;
799 }
800 }
801 if accum == 0 {
802 return Ok(());
803 }
804 let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
805 return Ok(());
806 };
807 editor.update_in(cx, |editor, window, cx| {
808 editor.change_selections(Default::default(), window, cx, |selections| {
809 selections.select_ranges([
810 text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0)
811 ]);
812 });
813 })
814 })
815 .detach_and_log_err(cx);
816 });
817 return;
818 }
819 cx.open_url(&link);
820}
821
822#[derive(Default)]
823pub struct HoverState {
824 pub info_popovers: Vec<InfoPopover>,
825 pub diagnostic_popover: Option<DiagnosticPopover>,
826 pub triggered_from: Option<Anchor>,
827 pub info_task: Option<Task<Option<()>>>,
828 pub closest_mouse_distance: Option<Pixels>,
829 pub hiding_delay_task: Option<Task<()>>,
830}
831
832impl HoverState {
833 pub fn visible(&self) -> bool {
834 !self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
835 }
836
837 pub fn is_mouse_getting_closer(&mut self, mouse_position: gpui::Point<Pixels>) -> bool {
838 if !self.visible() {
839 return false;
840 }
841
842 let mut popover_bounds = Vec::new();
843 for info_popover in &self.info_popovers {
844 if let Some(bounds) = info_popover.last_bounds.get() {
845 popover_bounds.push(bounds);
846 }
847 }
848 if let Some(diagnostic_popover) = &self.diagnostic_popover {
849 if let Some(bounds) = diagnostic_popover.last_bounds.get() {
850 popover_bounds.push(bounds);
851 }
852 }
853
854 if popover_bounds.is_empty() {
855 return false;
856 }
857
858 let distance = popover_bounds
859 .iter()
860 .map(|bounds| self.distance_from_point_to_bounds(mouse_position, *bounds))
861 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
862 .unwrap_or(px(f32::MAX));
863
864 if let Some(closest_distance) = self.closest_mouse_distance {
865 if distance > closest_distance + px(4.0) {
866 return false;
867 }
868 }
869
870 self.closest_mouse_distance =
871 Some(distance.min(self.closest_mouse_distance.unwrap_or(distance)));
872 true
873 }
874
875 fn distance_from_point_to_bounds(
876 &self,
877 point: gpui::Point<Pixels>,
878 bounds: Bounds<Pixels>,
879 ) -> Pixels {
880 let center_x = bounds.origin.x + bounds.size.width / 2.;
881 let center_y = bounds.origin.y + bounds.size.height / 2.;
882 let dx: f32 = ((point.x - center_x).abs() - bounds.size.width / 2.)
883 .max(px(0.0))
884 .into();
885 let dy: f32 = ((point.y - center_y).abs() - bounds.size.height / 2.)
886 .max(px(0.0))
887 .into();
888 px((dx.powi(2) + dy.powi(2)).sqrt())
889 }
890
891 pub(crate) fn render(
892 &mut self,
893 snapshot: &EditorSnapshot,
894 visible_rows: Range<DisplayRow>,
895 max_size: Size<Pixels>,
896 text_layout_details: &TextLayoutDetails,
897 window: &mut Window,
898 cx: &mut Context<Editor>,
899 ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
900 if !self.visible() {
901 return None;
902 }
903 // If there is a diagnostic, position the popovers based on that.
904 // Otherwise use the start of the hover range
905 let anchor = self
906 .diagnostic_popover
907 .as_ref()
908 .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
909 .or_else(|| {
910 self.info_popovers.iter().find_map(|info_popover| {
911 match &info_popover.symbol_range {
912 RangeInEditor::Text(range) => Some(&range.start),
913 RangeInEditor::Inlay(_) => None,
914 }
915 })
916 })
917 .or_else(|| {
918 self.info_popovers.iter().find_map(|info_popover| {
919 match &info_popover.symbol_range {
920 RangeInEditor::Text(_) => None,
921 RangeInEditor::Inlay(range) => Some(&range.inlay_position),
922 }
923 })
924 })?;
925 let mut point = anchor.to_display_point(&snapshot.display_snapshot);
926 // Clamp the point within the visible rows in case the popup source spans multiple lines
927 if visible_rows.end <= point.row() {
928 point = crate::movement::up_by_rows(
929 &snapshot.display_snapshot,
930 point,
931 1 + (point.row() - visible_rows.end).0,
932 text::SelectionGoal::None,
933 true,
934 text_layout_details,
935 )
936 .0;
937 } else if point.row() < visible_rows.start {
938 point = crate::movement::down_by_rows(
939 &snapshot.display_snapshot,
940 point,
941 (visible_rows.start - point.row()).0,
942 text::SelectionGoal::None,
943 true,
944 text_layout_details,
945 )
946 .0;
947 }
948
949 if !visible_rows.contains(&point.row()) {
950 log::error!("Hover popover point out of bounds after moving");
951 return None;
952 }
953
954 let mut elements = Vec::new();
955
956 if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
957 elements.push(diagnostic_popover.render(max_size, window, cx));
958 }
959 for info_popover in &mut self.info_popovers {
960 elements.push(info_popover.render(max_size, window, cx));
961 }
962
963 Some((point, elements))
964 }
965
966 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
967 let mut hover_popover_is_focused = false;
968 for info_popover in &self.info_popovers {
969 if let Some(markdown_view) = &info_popover.parsed_content
970 && markdown_view.focus_handle(cx).is_focused(window)
971 {
972 hover_popover_is_focused = true;
973 }
974 }
975 if let Some(diagnostic_popover) = &self.diagnostic_popover
976 && diagnostic_popover
977 .markdown
978 .focus_handle(cx)
979 .is_focused(window)
980 {
981 hover_popover_is_focused = true;
982 }
983 hover_popover_is_focused
984 }
985}
986
987pub struct InfoPopover {
988 pub symbol_range: RangeInEditor,
989 pub parsed_content: Option<Entity<Markdown>>,
990 pub scroll_handle: ScrollHandle,
991 pub keyboard_grace: Rc<RefCell<bool>>,
992 pub anchor: Option<Anchor>,
993 pub last_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
994 _subscription: Option<Subscription>,
995}
996
997impl InfoPopover {
998 pub(crate) fn render(
999 &mut self,
1000 max_size: Size<Pixels>,
1001 window: &mut Window,
1002 cx: &mut Context<Editor>,
1003 ) -> AnyElement {
1004 let keyboard_grace = Rc::clone(&self.keyboard_grace);
1005 let this = cx.entity().downgrade();
1006 let bounds_cell = self.last_bounds.clone();
1007 div()
1008 .id("info_popover")
1009 .occlude()
1010 .elevation_2(cx)
1011 .child(
1012 canvas(
1013 {
1014 move |bounds, _window, _cx| {
1015 bounds_cell.set(Some(bounds));
1016 }
1017 },
1018 |_, _, _, _| {},
1019 )
1020 .absolute()
1021 .size_full(),
1022 )
1023 // Prevent a mouse down/move on the popover from being propagated to the editor,
1024 // because that would dismiss the popover.
1025 .on_mouse_move({
1026 move |_, _, cx: &mut App| {
1027 this.update(cx, |editor, _| {
1028 editor.hover_state.closest_mouse_distance = Some(px(0.0));
1029 editor.hover_state.hiding_delay_task = None;
1030 })
1031 .ok();
1032 cx.stop_propagation()
1033 }
1034 })
1035 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
1036 let mut keyboard_grace = keyboard_grace.borrow_mut();
1037 *keyboard_grace = false;
1038 cx.stop_propagation();
1039 })
1040 .when_some(self.parsed_content.clone(), |this, markdown| {
1041 this.child(
1042 div()
1043 .id("info-md-container")
1044 .overflow_y_scroll()
1045 .max_w(max_size.width)
1046 .max_h(max_size.height)
1047 .track_scroll(&self.scroll_handle)
1048 .child(
1049 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1050 .code_block_renderer(markdown::CodeBlockRenderer::Default {
1051 copy_button_visibility: CopyButtonVisibility::Hidden,
1052 border: false,
1053 })
1054 .on_url_click(open_markdown_url)
1055 .p_2(),
1056 ),
1057 )
1058 .custom_scrollbars(
1059 Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1060 .tracked_scroll_handle(&self.scroll_handle),
1061 window,
1062 cx,
1063 )
1064 })
1065 .into_any_element()
1066 }
1067
1068 pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
1069 let mut current = self.scroll_handle.offset();
1070 current.y -= amount.pixels(
1071 window.line_height(),
1072 self.scroll_handle.bounds().size.height - px(16.),
1073 ) / 2.0;
1074 cx.notify();
1075 self.scroll_handle.set_offset(current);
1076 }
1077}
1078
1079pub struct DiagnosticPopover {
1080 pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
1081 markdown: Entity<Markdown>,
1082 border_color: Hsla,
1083 background_color: Hsla,
1084 pub keyboard_grace: Rc<RefCell<bool>>,
1085 pub anchor: Anchor,
1086 pub last_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
1087 _subscription: Subscription,
1088 pub scroll_handle: ScrollHandle,
1089}
1090
1091impl DiagnosticPopover {
1092 pub fn render(
1093 &self,
1094 max_size: Size<Pixels>,
1095 window: &mut Window,
1096 cx: &mut Context<Editor>,
1097 ) -> AnyElement {
1098 let keyboard_grace = Rc::clone(&self.keyboard_grace);
1099 let this = cx.entity().downgrade();
1100 let bounds_cell = self.last_bounds.clone();
1101 div()
1102 .id("diagnostic")
1103 .occlude()
1104 .elevation_2_borderless(cx)
1105 .child(
1106 canvas(
1107 {
1108 move |bounds, _window, _cx| {
1109 bounds_cell.set(Some(bounds));
1110 }
1111 },
1112 |_, _, _, _| {},
1113 )
1114 .absolute()
1115 .size_full(),
1116 )
1117 // Don't draw the background color if the theme
1118 // allows transparent surfaces.
1119 .when(theme_is_transparent(cx), |this| {
1120 this.bg(gpui::transparent_black())
1121 })
1122 // Prevent a mouse move on the popover from being propagated to the editor,
1123 // because that would dismiss the popover.
1124 .on_mouse_move({
1125 let this = this.clone();
1126 move |_, _, cx: &mut App| {
1127 this.update(cx, |editor, _| {
1128 editor.hover_state.closest_mouse_distance = Some(px(0.0));
1129 editor.hover_state.hiding_delay_task = None;
1130 })
1131 .ok();
1132 cx.stop_propagation()
1133 }
1134 })
1135 // Prevent a mouse down on the popover from being propagated to the editor,
1136 // because that would move the cursor.
1137 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
1138 let mut keyboard_grace = keyboard_grace.borrow_mut();
1139 *keyboard_grace = false;
1140 cx.stop_propagation();
1141 })
1142 .child(
1143 div()
1144 .relative()
1145 .py_1()
1146 .pl_2()
1147 .pr_8()
1148 .bg(self.background_color)
1149 .border_1()
1150 .border_color(self.border_color)
1151 .rounded_lg()
1152 .child(
1153 div()
1154 .id("diagnostic-content-container")
1155 .max_w(max_size.width)
1156 .max_h(max_size.height)
1157 .overflow_y_scroll()
1158 .track_scroll(&self.scroll_handle)
1159 .child(
1160 MarkdownElement::new(
1161 self.markdown.clone(),
1162 diagnostics_markdown_style(window, cx),
1163 )
1164 .code_block_renderer(markdown::CodeBlockRenderer::Default {
1165 copy_button_visibility: CopyButtonVisibility::Hidden,
1166 border: false,
1167 })
1168 .on_url_click(
1169 move |link, window, cx| {
1170 if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
1171 {
1172 this.update(cx, |this, cx| {
1173 renderer.as_ref().open_link(this, link, window, cx);
1174 })
1175 .ok();
1176 }
1177 },
1178 ),
1179 ),
1180 )
1181 .child(div().absolute().top_1().right_1().child({
1182 let message = self.local_diagnostic.diagnostic.message.clone();
1183 CopyButton::new("copy-diagnostic", message).tooltip_label("Copy Diagnostic")
1184 }))
1185 .custom_scrollbars(
1186 Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1187 .tracked_scroll_handle(&self.scroll_handle),
1188 window,
1189 cx,
1190 ),
1191 )
1192 .into_any_element()
1193 }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use crate::{
1200 PointForPosition,
1201 actions::ConfirmCompletion,
1202 editor_tests::{handle_completion_request, init_test},
1203 inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
1204 test::editor_lsp_test_context::EditorLspTestContext,
1205 };
1206 use collections::BTreeSet;
1207 use gpui::App;
1208 use indoc::indoc;
1209 use markdown::parser::MarkdownEvent;
1210 use project::InlayId;
1211 use settings::InlayHintSettingsContent;
1212 use settings::{DelayMs, SettingsStore};
1213 use smol::stream::StreamExt;
1214 use std::sync::atomic;
1215 use std::sync::atomic::AtomicUsize;
1216 use text::Bias;
1217
1218 fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
1219 cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 })
1220 }
1221
1222 impl InfoPopover {
1223 fn get_rendered_text(&self, cx: &gpui::App) -> String {
1224 let mut rendered_text = String::new();
1225 if let Some(parsed_content) = self.parsed_content.clone() {
1226 let markdown = parsed_content.read(cx);
1227 let text = markdown.parsed_markdown().source().to_string();
1228 let data = markdown.parsed_markdown().events();
1229 let slice = data;
1230
1231 for (range, event) in slice.iter() {
1232 match event {
1233 MarkdownEvent::SubstitutedText(parsed) => {
1234 rendered_text.push_str(parsed.as_str())
1235 }
1236 MarkdownEvent::Text | MarkdownEvent::Code => {
1237 rendered_text.push_str(&text[range.clone()])
1238 }
1239 _ => {}
1240 }
1241 }
1242 }
1243 rendered_text
1244 }
1245 }
1246
1247 #[gpui::test]
1248 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
1249 cx: &mut gpui::TestAppContext,
1250 ) {
1251 init_test(cx, |_| {});
1252
1253 let mut cx = EditorLspTestContext::new_rust(
1254 lsp::ServerCapabilities {
1255 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1256 completion_provider: Some(lsp::CompletionOptions {
1257 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
1258 resolve_provider: Some(true),
1259 ..Default::default()
1260 }),
1261 ..Default::default()
1262 },
1263 cx,
1264 )
1265 .await;
1266 let counter = Arc::new(AtomicUsize::new(0));
1267 // Basic hover delays and then pops without moving the mouse
1268 cx.set_state(indoc! {"
1269 oneˇ
1270 two
1271 three
1272 fn test() { println!(); }
1273 "});
1274
1275 //prompt autocompletion menu
1276 cx.simulate_keystroke(".");
1277 handle_completion_request(
1278 indoc! {"
1279 one.|<>
1280 two
1281 three
1282 "},
1283 vec!["first_completion", "second_completion"],
1284 true,
1285 counter.clone(),
1286 &mut cx,
1287 )
1288 .await;
1289 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
1290 .await;
1291 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
1292
1293 let hover_point = cx.display_point(indoc! {"
1294 one.
1295 two
1296 three
1297 fn test() { printˇln!(); }
1298 "});
1299 cx.update_editor(|editor, window, cx| {
1300 let snapshot = editor.snapshot(window, cx);
1301 let anchor = snapshot
1302 .buffer_snapshot()
1303 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1304 hover_at(editor, Some(anchor), None, window, cx)
1305 });
1306 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1307
1308 // After delay, hover should be visible.
1309 let symbol_range = cx.lsp_range(indoc! {"
1310 one.
1311 two
1312 three
1313 fn test() { «println!»(); }
1314 "});
1315 let mut requests =
1316 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1317 Ok(Some(lsp::Hover {
1318 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1319 kind: lsp::MarkupKind::Markdown,
1320 value: "some basic docs".to_string(),
1321 }),
1322 range: Some(symbol_range),
1323 }))
1324 });
1325 cx.background_executor
1326 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1327 requests.next().await;
1328
1329 cx.editor(|editor, _window, cx| {
1330 assert!(editor.hover_state.visible());
1331 assert_eq!(
1332 editor.hover_state.info_popovers.len(),
1333 1,
1334 "Expected exactly one hover but got: {:?}",
1335 editor.hover_state.info_popovers.len()
1336 );
1337 let rendered_text = editor
1338 .hover_state
1339 .info_popovers
1340 .first()
1341 .unwrap()
1342 .get_rendered_text(cx);
1343 assert_eq!(rendered_text, "some basic docs".to_string())
1344 });
1345
1346 // check that the completion menu is still visible and that there still has only been 1 completion request
1347 cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
1348 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1349
1350 //apply a completion and check it was successfully applied
1351 let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1352 editor.context_menu_next(&Default::default(), window, cx);
1353 editor
1354 .confirm_completion(&ConfirmCompletion::default(), window, cx)
1355 .unwrap()
1356 });
1357 cx.assert_editor_state(indoc! {"
1358 one.second_completionˇ
1359 two
1360 three
1361 fn test() { println!(); }
1362 "});
1363
1364 // check that the completion menu is no longer visible and that there still has only been 1 completion request
1365 cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1366 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1367
1368 //verify the information popover is still visible and unchanged
1369 cx.editor(|editor, _, cx| {
1370 assert!(editor.hover_state.visible());
1371 assert_eq!(
1372 editor.hover_state.info_popovers.len(),
1373 1,
1374 "Expected exactly one hover but got: {:?}",
1375 editor.hover_state.info_popovers.len()
1376 );
1377 let rendered_text = editor
1378 .hover_state
1379 .info_popovers
1380 .first()
1381 .unwrap()
1382 .get_rendered_text(cx);
1383
1384 assert_eq!(rendered_text, "some basic docs".to_string())
1385 });
1386
1387 // Mouse moved with no hover response dismisses
1388 let hover_point = cx.display_point(indoc! {"
1389 one.second_completionˇ
1390 two
1391 three
1392 fn teˇst() { println!(); }
1393 "});
1394 let mut request = cx
1395 .lsp
1396 .set_request_handler::<lsp::request::HoverRequest, _, _>(
1397 |_, _| async move { Ok(None) },
1398 );
1399 cx.update_editor(|editor, window, cx| {
1400 let snapshot = editor.snapshot(window, cx);
1401 let anchor = snapshot
1402 .buffer_snapshot()
1403 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1404 hover_at(editor, Some(anchor), None, window, cx)
1405 });
1406 cx.background_executor
1407 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1408 request.next().await;
1409
1410 // verify that the information popover is no longer visible
1411 cx.editor(|editor, _, _| {
1412 assert!(!editor.hover_state.visible());
1413 });
1414 }
1415
1416 #[gpui::test]
1417 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1418 init_test(cx, |_| {});
1419
1420 let mut cx = EditorLspTestContext::new_rust(
1421 lsp::ServerCapabilities {
1422 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1423 ..Default::default()
1424 },
1425 cx,
1426 )
1427 .await;
1428
1429 // Basic hover delays and then pops without moving the mouse
1430 cx.set_state(indoc! {"
1431 fn ˇtest() { println!(); }
1432 "});
1433 let hover_point = cx.display_point(indoc! {"
1434 fn test() { printˇln!(); }
1435 "});
1436
1437 cx.update_editor(|editor, window, cx| {
1438 let snapshot = editor.snapshot(window, cx);
1439 let anchor = snapshot
1440 .buffer_snapshot()
1441 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1442 hover_at(editor, Some(anchor), None, window, cx)
1443 });
1444 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1445
1446 // After delay, hover should be visible.
1447 let symbol_range = cx.lsp_range(indoc! {"
1448 fn test() { «println!»(); }
1449 "});
1450 let mut requests =
1451 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1452 Ok(Some(lsp::Hover {
1453 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1454 kind: lsp::MarkupKind::Markdown,
1455 value: "some basic docs".to_string(),
1456 }),
1457 range: Some(symbol_range),
1458 }))
1459 });
1460 cx.background_executor
1461 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1462 requests.next().await;
1463
1464 cx.editor(|editor, _, cx| {
1465 assert!(editor.hover_state.visible());
1466 assert_eq!(
1467 editor.hover_state.info_popovers.len(),
1468 1,
1469 "Expected exactly one hover but got: {:?}",
1470 editor.hover_state.info_popovers.len()
1471 );
1472 let rendered_text = editor
1473 .hover_state
1474 .info_popovers
1475 .first()
1476 .unwrap()
1477 .get_rendered_text(cx);
1478
1479 assert_eq!(rendered_text, "some basic docs".to_string())
1480 });
1481
1482 // Mouse moved with no hover response dismisses
1483 let hover_point = cx.display_point(indoc! {"
1484 fn teˇst() { println!(); }
1485 "});
1486 let mut request = cx
1487 .lsp
1488 .set_request_handler::<lsp::request::HoverRequest, _, _>(
1489 |_, _| async move { Ok(None) },
1490 );
1491 cx.update_editor(|editor, window, cx| {
1492 let snapshot = editor.snapshot(window, cx);
1493 let anchor = snapshot
1494 .buffer_snapshot()
1495 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1496 hover_at(editor, Some(anchor), None, window, cx)
1497 });
1498 cx.background_executor
1499 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1500 request.next().await;
1501 cx.editor(|editor, _, _| {
1502 assert!(!editor.hover_state.visible());
1503 });
1504 }
1505
1506 #[gpui::test]
1507 async fn test_mouse_hover_cancelled_before_delay(cx: &mut gpui::TestAppContext) {
1508 init_test(cx, |_| {});
1509
1510 let mut cx = EditorLspTestContext::new_rust(
1511 lsp::ServerCapabilities {
1512 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1513 ..Default::default()
1514 },
1515 cx,
1516 )
1517 .await;
1518
1519 cx.set_state(indoc! {"
1520 fn ˇtest() { println!(); }
1521 "});
1522 let hover_point = cx.display_point(indoc! {"
1523 fn test() { printˇln!(); }
1524 "});
1525
1526 cx.update_editor(|editor, window, cx| {
1527 let snapshot = editor.snapshot(window, cx);
1528 let anchor = snapshot
1529 .buffer_snapshot()
1530 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1531 hover_at(editor, Some(anchor), None, window, cx);
1532 hover_at(editor, None, None, window, cx);
1533 });
1534
1535 let request_count = Arc::new(AtomicUsize::new(0));
1536 cx.set_request_handler::<lsp::request::HoverRequest, _, _>({
1537 let request_count = request_count.clone();
1538 move |_, _, _| {
1539 let request_count = request_count.clone();
1540 async move {
1541 request_count.fetch_add(1, atomic::Ordering::Release);
1542 Ok(Some(lsp::Hover {
1543 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1544 kind: lsp::MarkupKind::Markdown,
1545 value: "some basic docs".to_string(),
1546 }),
1547 range: None,
1548 }))
1549 }
1550 }
1551 });
1552
1553 cx.background_executor
1554 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1555 cx.background_executor.run_until_parked();
1556 cx.run_until_parked();
1557
1558 assert_eq!(request_count.load(atomic::Ordering::Acquire), 0);
1559 cx.editor(|editor, _, _| {
1560 assert!(!editor.hover_state.visible());
1561 });
1562 }
1563
1564 #[gpui::test]
1565 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1566 init_test(cx, |_| {});
1567
1568 let mut cx = EditorLspTestContext::new_rust(
1569 lsp::ServerCapabilities {
1570 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1571 ..Default::default()
1572 },
1573 cx,
1574 )
1575 .await;
1576
1577 // Hover with keyboard has no delay
1578 cx.set_state(indoc! {"
1579 fˇn test() { println!(); }
1580 "});
1581 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1582 let symbol_range = cx.lsp_range(indoc! {"
1583 «fn» test() { println!(); }
1584 "});
1585
1586 cx.editor(|editor, _window, _cx| {
1587 assert!(!editor.hover_state.visible());
1588
1589 assert_eq!(
1590 editor.hover_state.info_popovers.len(),
1591 0,
1592 "Expected no hovers but got but got: {:?}",
1593 editor.hover_state.info_popovers.len()
1594 );
1595 });
1596
1597 let mut requests =
1598 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1599 Ok(Some(lsp::Hover {
1600 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1601 kind: lsp::MarkupKind::Markdown,
1602 value: "some other basic docs".to_string(),
1603 }),
1604 range: Some(symbol_range),
1605 }))
1606 });
1607
1608 requests.next().await;
1609 cx.dispatch_action(Hover);
1610
1611 cx.condition(|editor, _| editor.hover_state.visible()).await;
1612 cx.editor(|editor, _, cx| {
1613 assert_eq!(
1614 editor.hover_state.info_popovers.len(),
1615 1,
1616 "Expected exactly one hover but got: {:?}",
1617 editor.hover_state.info_popovers.len()
1618 );
1619
1620 let rendered_text = editor
1621 .hover_state
1622 .info_popovers
1623 .first()
1624 .unwrap()
1625 .get_rendered_text(cx);
1626
1627 assert_eq!(rendered_text, "some other basic docs".to_string())
1628 });
1629 }
1630
1631 #[gpui::test]
1632 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1633 init_test(cx, |_| {});
1634
1635 let mut cx = EditorLspTestContext::new_rust(
1636 lsp::ServerCapabilities {
1637 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1638 ..Default::default()
1639 },
1640 cx,
1641 )
1642 .await;
1643
1644 // Hover with keyboard has no delay
1645 cx.set_state(indoc! {"
1646 fˇn test() { println!(); }
1647 "});
1648 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1649 let symbol_range = cx.lsp_range(indoc! {"
1650 «fn» test() { println!(); }
1651 "});
1652 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1653 Ok(Some(lsp::Hover {
1654 contents: lsp::HoverContents::Array(vec![
1655 lsp::MarkedString::String("regular text for hover to show".to_string()),
1656 lsp::MarkedString::String("".to_string()),
1657 lsp::MarkedString::LanguageString(lsp::LanguageString {
1658 language: "Rust".to_string(),
1659 value: "".to_string(),
1660 }),
1661 ]),
1662 range: Some(symbol_range),
1663 }))
1664 })
1665 .next()
1666 .await;
1667 cx.dispatch_action(Hover);
1668
1669 cx.condition(|editor, _| editor.hover_state.visible()).await;
1670 cx.editor(|editor, _, cx| {
1671 assert_eq!(
1672 editor.hover_state.info_popovers.len(),
1673 1,
1674 "Expected exactly one hover but got: {:?}",
1675 editor.hover_state.info_popovers.len()
1676 );
1677 let rendered_text = editor
1678 .hover_state
1679 .info_popovers
1680 .first()
1681 .unwrap()
1682 .get_rendered_text(cx);
1683
1684 assert_eq!(
1685 rendered_text,
1686 "regular text for hover to show".to_string(),
1687 "No empty string hovers should be shown"
1688 );
1689 });
1690 }
1691
1692 #[gpui::test]
1693 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1694 init_test(cx, |_| {});
1695
1696 let mut cx = EditorLspTestContext::new_rust(
1697 lsp::ServerCapabilities {
1698 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1699 ..Default::default()
1700 },
1701 cx,
1702 )
1703 .await;
1704
1705 // Hover with keyboard has no delay
1706 cx.set_state(indoc! {"
1707 fˇn test() { println!(); }
1708 "});
1709 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1710 let symbol_range = cx.lsp_range(indoc! {"
1711 «fn» test() { println!(); }
1712 "});
1713
1714 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1715 let markdown_string = format!("\n```rust\n{code_str}```");
1716
1717 let closure_markdown_string = markdown_string.clone();
1718 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1719 let future_markdown_string = closure_markdown_string.clone();
1720 async move {
1721 Ok(Some(lsp::Hover {
1722 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1723 kind: lsp::MarkupKind::Markdown,
1724 value: future_markdown_string,
1725 }),
1726 range: Some(symbol_range),
1727 }))
1728 }
1729 })
1730 .next()
1731 .await;
1732
1733 cx.dispatch_action(Hover);
1734
1735 cx.condition(|editor, _| editor.hover_state.visible()).await;
1736 cx.editor(|editor, _, cx| {
1737 assert_eq!(
1738 editor.hover_state.info_popovers.len(),
1739 1,
1740 "Expected exactly one hover but got: {:?}",
1741 editor.hover_state.info_popovers.len()
1742 );
1743 let rendered_text = editor
1744 .hover_state
1745 .info_popovers
1746 .first()
1747 .unwrap()
1748 .get_rendered_text(cx);
1749
1750 assert_eq!(
1751 rendered_text, code_str,
1752 "Should not have extra line breaks at end of rendered hover"
1753 );
1754 });
1755 }
1756
1757 #[gpui::test]
1758 // https://github.com/zed-industries/zed/issues/15498
1759 async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1760 init_test(cx, |_| {});
1761
1762 let mut cx = EditorLspTestContext::new_rust(
1763 lsp::ServerCapabilities {
1764 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1765 ..Default::default()
1766 },
1767 cx,
1768 )
1769 .await;
1770
1771 cx.set_state(indoc! {"
1772 fn fuˇnc(abc def: i32) -> u32 {
1773 }
1774 "});
1775
1776 cx.lsp
1777 .set_request_handler::<lsp::request::HoverRequest, _, _>({
1778 |_, _| async move {
1779 Ok(Some(lsp::Hover {
1780 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1781 kind: lsp::MarkupKind::Markdown,
1782 value: indoc!(
1783 r#"
1784 ### function `errands_data_read`
1785
1786 ---
1787 → `char *`
1788 Function to read a file into a string
1789
1790 ---
1791 ```cpp
1792 static char *errands_data_read()
1793 ```
1794 "#
1795 )
1796 .to_string(),
1797 }),
1798 range: None,
1799 }))
1800 }
1801 });
1802 cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1803 cx.run_until_parked();
1804
1805 cx.update_editor(|editor, _, cx| {
1806 let popover = editor.hover_state.info_popovers.first().unwrap();
1807 let content = popover.get_rendered_text(cx);
1808
1809 assert!(content.contains("Function to read a file"));
1810 });
1811 }
1812
1813 #[gpui::test]
1814 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1815 init_test(cx, |settings| {
1816 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1817 show_value_hints: Some(true),
1818 enabled: Some(true),
1819 edit_debounce_ms: Some(0),
1820 scroll_debounce_ms: Some(0),
1821 show_type_hints: Some(true),
1822 show_parameter_hints: Some(true),
1823 show_other_hints: Some(true),
1824 show_background: Some(false),
1825 toggle_on_modifiers_press: None,
1826 })
1827 });
1828
1829 let mut cx = EditorLspTestContext::new_rust(
1830 lsp::ServerCapabilities {
1831 inlay_hint_provider: Some(lsp::OneOf::Right(
1832 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1833 resolve_provider: Some(true),
1834 ..Default::default()
1835 }),
1836 )),
1837 ..Default::default()
1838 },
1839 cx,
1840 )
1841 .await;
1842
1843 cx.set_state(indoc! {"
1844 struct TestStruct;
1845
1846 // ==================
1847
1848 struct TestNewType<T>(T);
1849
1850 fn main() {
1851 let variableˇ = TestNewType(TestStruct);
1852 }
1853 "});
1854
1855 let hint_start_offset = cx.ranges(indoc! {"
1856 struct TestStruct;
1857
1858 // ==================
1859
1860 struct TestNewType<T>(T);
1861
1862 fn main() {
1863 let variableˇ = TestNewType(TestStruct);
1864 }
1865 "})[0]
1866 .start;
1867 let hint_position = cx.to_lsp(MultiBufferOffset(hint_start_offset));
1868 let new_type_target_range = cx.lsp_range(indoc! {"
1869 struct TestStruct;
1870
1871 // ==================
1872
1873 struct «TestNewType»<T>(T);
1874
1875 fn main() {
1876 let variable = TestNewType(TestStruct);
1877 }
1878 "});
1879 let struct_target_range = cx.lsp_range(indoc! {"
1880 struct «TestStruct»;
1881
1882 // ==================
1883
1884 struct TestNewType<T>(T);
1885
1886 fn main() {
1887 let variable = TestNewType(TestStruct);
1888 }
1889 "});
1890
1891 let uri = cx.buffer_lsp_url.clone();
1892 let new_type_label = "TestNewType";
1893 let struct_label = "TestStruct";
1894 let entire_hint_label = ": TestNewType<TestStruct>";
1895 let closure_uri = uri.clone();
1896 cx.lsp
1897 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1898 let task_uri = closure_uri.clone();
1899 async move {
1900 assert_eq!(params.text_document.uri, task_uri);
1901 Ok(Some(vec![lsp::InlayHint {
1902 position: hint_position,
1903 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1904 value: entire_hint_label.to_string(),
1905 ..Default::default()
1906 }]),
1907 kind: Some(lsp::InlayHintKind::TYPE),
1908 text_edits: None,
1909 tooltip: None,
1910 padding_left: Some(false),
1911 padding_right: Some(false),
1912 data: None,
1913 }]))
1914 }
1915 })
1916 .next()
1917 .await;
1918 cx.background_executor.run_until_parked();
1919 cx.update_editor(|editor, _, cx| {
1920 let expected_layers = vec![entire_hint_label.to_string()];
1921 assert_eq!(expected_layers, cached_hint_labels(editor, cx));
1922 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1923 });
1924
1925 let inlay_range = cx
1926 .ranges(indoc! {"
1927 struct TestStruct;
1928
1929 // ==================
1930
1931 struct TestNewType<T>(T);
1932
1933 fn main() {
1934 let variable« »= TestNewType(TestStruct);
1935 }
1936 "})
1937 .first()
1938 .cloned()
1939 .unwrap();
1940 let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1941 let snapshot = editor.snapshot(window, cx);
1942 let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
1943 let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
1944 assert_eq!(previous_valid.row(), next_valid.row());
1945 assert!(previous_valid.column() < next_valid.column());
1946 let exact_unclipped = DisplayPoint::new(
1947 previous_valid.row(),
1948 previous_valid.column()
1949 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1950 as u32,
1951 );
1952 PointForPosition {
1953 previous_valid,
1954 next_valid,
1955 exact_unclipped,
1956 column_overshoot_after_line_end: 0,
1957 }
1958 });
1959 cx.update_editor(|editor, window, cx| {
1960 editor.update_inlay_link_and_hover_points(
1961 &editor.snapshot(window, cx),
1962 new_type_hint_part_hover_position,
1963 None,
1964 true,
1965 false,
1966 window,
1967 cx,
1968 );
1969 });
1970
1971 let resolve_closure_uri = uri.clone();
1972 cx.lsp
1973 .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
1974 move |mut hint_to_resolve, _| {
1975 let mut resolved_hint_positions = BTreeSet::new();
1976 let task_uri = resolve_closure_uri.clone();
1977 async move {
1978 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1979 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1980
1981 // `: TestNewType<TestStruct>`
1982 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1983 lsp::InlayHintLabelPart {
1984 value: ": ".to_string(),
1985 ..Default::default()
1986 },
1987 lsp::InlayHintLabelPart {
1988 value: new_type_label.to_string(),
1989 location: Some(lsp::Location {
1990 uri: task_uri.clone(),
1991 range: new_type_target_range,
1992 }),
1993 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1994 "A tooltip for `{new_type_label}`"
1995 ))),
1996 ..Default::default()
1997 },
1998 lsp::InlayHintLabelPart {
1999 value: "<".to_string(),
2000 ..Default::default()
2001 },
2002 lsp::InlayHintLabelPart {
2003 value: struct_label.to_string(),
2004 location: Some(lsp::Location {
2005 uri: task_uri,
2006 range: struct_target_range,
2007 }),
2008 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
2009 lsp::MarkupContent {
2010 kind: lsp::MarkupKind::Markdown,
2011 value: format!("A tooltip for `{struct_label}`"),
2012 },
2013 )),
2014 ..Default::default()
2015 },
2016 lsp::InlayHintLabelPart {
2017 value: ">".to_string(),
2018 ..Default::default()
2019 },
2020 ]);
2021
2022 Ok(hint_to_resolve)
2023 }
2024 },
2025 )
2026 .next()
2027 .await;
2028 cx.background_executor.run_until_parked();
2029
2030 cx.update_editor(|editor, window, cx| {
2031 editor.update_inlay_link_and_hover_points(
2032 &editor.snapshot(window, cx),
2033 new_type_hint_part_hover_position,
2034 None,
2035 true,
2036 false,
2037 window,
2038 cx,
2039 );
2040 });
2041 cx.background_executor
2042 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2043 cx.background_executor.run_until_parked();
2044 cx.update_editor(|editor, _, cx| {
2045 let hover_state = &editor.hover_state;
2046 assert!(
2047 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2048 );
2049 let popover = hover_state.info_popovers.first().unwrap();
2050 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2051 assert_eq!(
2052 popover.symbol_range,
2053 RangeInEditor::Inlay(InlayHighlight {
2054 inlay: InlayId::Hint(0),
2055 inlay_position: buffer_snapshot
2056 .anchor_after(MultiBufferOffset(inlay_range.start)),
2057 range: ": ".len()..": ".len() + new_type_label.len(),
2058 }),
2059 "Popover range should match the new type label part"
2060 );
2061 assert_eq!(
2062 popover.get_rendered_text(cx),
2063 format!("A tooltip for {new_type_label}"),
2064 );
2065 });
2066
2067 let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
2068 let snapshot = editor.snapshot(window, cx);
2069 let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
2070 let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
2071 assert_eq!(previous_valid.row(), next_valid.row());
2072 assert!(previous_valid.column() < next_valid.column());
2073 let exact_unclipped = DisplayPoint::new(
2074 previous_valid.row(),
2075 previous_valid.column()
2076 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
2077 as u32,
2078 );
2079 PointForPosition {
2080 previous_valid,
2081 next_valid,
2082 exact_unclipped,
2083 column_overshoot_after_line_end: 0,
2084 }
2085 });
2086 cx.update_editor(|editor, window, cx| {
2087 editor.update_inlay_link_and_hover_points(
2088 &editor.snapshot(window, cx),
2089 struct_hint_part_hover_position,
2090 None,
2091 true,
2092 false,
2093 window,
2094 cx,
2095 );
2096 });
2097 cx.background_executor
2098 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2099 cx.background_executor.run_until_parked();
2100 cx.update_editor(|editor, _, cx| {
2101 let hover_state = &editor.hover_state;
2102 assert!(
2103 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2104 );
2105 let popover = hover_state.info_popovers.first().unwrap();
2106 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2107 assert_eq!(
2108 popover.symbol_range,
2109 RangeInEditor::Inlay(InlayHighlight {
2110 inlay: InlayId::Hint(0),
2111 inlay_position: buffer_snapshot
2112 .anchor_after(MultiBufferOffset(inlay_range.start)),
2113 range: ": ".len() + new_type_label.len() + "<".len()
2114 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
2115 }),
2116 "Popover range should match the struct label part"
2117 );
2118 assert_eq!(
2119 popover.get_rendered_text(cx),
2120 format!("A tooltip for {struct_label}"),
2121 "Rendered markdown element should remove backticks from text"
2122 );
2123 });
2124 }
2125
2126 #[test]
2127 fn test_find_hovered_hint_part_with_multibyte_characters() {
2128 use crate::display_map::InlayOffset;
2129 use multi_buffer::MultiBufferOffset;
2130 use project::InlayHintLabelPart;
2131
2132 // Test with multi-byte UTF-8 character "→" (3 bytes, 1 character)
2133 let label = "→ app/Livewire/UserProfile.php";
2134 let label_parts = vec![InlayHintLabelPart {
2135 value: label.to_string(),
2136 tooltip: None,
2137 location: None,
2138 }];
2139
2140 let hint_start = InlayOffset(MultiBufferOffset(100));
2141
2142 // Verify the label has more bytes than characters (due to "→")
2143 assert_eq!(label.len(), 32); // bytes
2144 assert_eq!(label.chars().count(), 30); // characters
2145
2146 // Test hovering at the last byte (should find the part)
2147 let last_byte_offset = InlayOffset(MultiBufferOffset(100 + label.len() - 1));
2148 let result = find_hovered_hint_part(label_parts.clone(), hint_start, last_byte_offset);
2149 assert!(
2150 result.is_some(),
2151 "Should find part when hovering at last byte"
2152 );
2153 let (part, range) = result.unwrap();
2154 assert_eq!(part.value, label);
2155 assert_eq!(range.start, hint_start);
2156 assert_eq!(range.end, InlayOffset(MultiBufferOffset(100 + label.len())));
2157
2158 // Test hovering at the first byte of "→" (byte 0)
2159 let first_byte_offset = InlayOffset(MultiBufferOffset(100));
2160 let result = find_hovered_hint_part(label_parts.clone(), hint_start, first_byte_offset);
2161 assert!(
2162 result.is_some(),
2163 "Should find part when hovering at first byte"
2164 );
2165
2166 // Test hovering in the middle of "→" (byte 1, still part of the arrow character)
2167 let mid_arrow_offset = InlayOffset(MultiBufferOffset(101));
2168 let result = find_hovered_hint_part(label_parts, hint_start, mid_arrow_offset);
2169 assert!(
2170 result.is_some(),
2171 "Should find part when hovering in middle of multi-byte char"
2172 );
2173
2174 // Test with multiple parts containing multi-byte characters
2175 // Part ranges are [start, end) - start inclusive, end exclusive
2176 // "→ " occupies bytes [0, 4), "path" occupies bytes [4, 8)
2177 let parts = vec![
2178 InlayHintLabelPart {
2179 value: "→ ".to_string(), // 4 bytes (3 + 1)
2180 tooltip: None,
2181 location: None,
2182 },
2183 InlayHintLabelPart {
2184 value: "path".to_string(), // 4 bytes
2185 tooltip: None,
2186 location: None,
2187 },
2188 ];
2189
2190 // Hover at byte 3 (last byte of "→ ", the space character)
2191 let arrow_last_byte = InlayOffset(MultiBufferOffset(100 + 3));
2192 let result = find_hovered_hint_part(parts.clone(), hint_start, arrow_last_byte);
2193 assert!(result.is_some(), "Should find first part at its last byte");
2194 let (part, range) = result.unwrap();
2195 assert_eq!(part.value, "→ ");
2196 assert_eq!(
2197 range,
2198 InlayOffset(MultiBufferOffset(100))..InlayOffset(MultiBufferOffset(104))
2199 );
2200
2201 // Hover at byte 4 (first byte of "path", at the boundary)
2202 let path_start_offset = InlayOffset(MultiBufferOffset(100 + 4));
2203 let result = find_hovered_hint_part(parts.clone(), hint_start, path_start_offset);
2204 assert!(result.is_some(), "Should find second part at boundary");
2205 let (part, _) = result.unwrap();
2206 assert_eq!(part.value, "path");
2207
2208 // Hover at byte 7 (last byte of "path")
2209 let path_end_offset = InlayOffset(MultiBufferOffset(100 + 7));
2210 let result = find_hovered_hint_part(parts, hint_start, path_end_offset);
2211 assert!(result.is_some(), "Should find second part at last byte");
2212 let (part, range) = result.unwrap();
2213 assert_eq!(part.value, "path");
2214 assert_eq!(
2215 range,
2216 InlayOffset(MultiBufferOffset(104))..InlayOffset(MultiBufferOffset(108))
2217 );
2218 }
2219
2220 #[gpui::test]
2221 async fn test_hover_popover_hiding_delay(cx: &mut gpui::TestAppContext) {
2222 init_test(cx, |_| {});
2223
2224 let custom_delay_ms = 500u64;
2225 cx.update(|cx| {
2226 cx.update_global::<SettingsStore, _>(|settings, cx| {
2227 settings.update_user_settings(cx, |settings| {
2228 settings.editor.hover_popover_sticky = Some(true);
2229 settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2230 });
2231 });
2232 });
2233
2234 let mut cx = EditorLspTestContext::new_rust(
2235 lsp::ServerCapabilities {
2236 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2237 ..Default::default()
2238 },
2239 cx,
2240 )
2241 .await;
2242
2243 cx.set_state(indoc! {"
2244 fn ˇtest() { println!(); }
2245 "});
2246
2247 // Trigger hover on a symbol
2248 let hover_point = cx.display_point(indoc! {"
2249 fn test() { printˇln!(); }
2250 "});
2251 let symbol_range = cx.lsp_range(indoc! {"
2252 fn test() { «println!»(); }
2253 "});
2254 let mut requests =
2255 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2256 Ok(Some(lsp::Hover {
2257 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2258 kind: lsp::MarkupKind::Markdown,
2259 value: "some basic docs".to_string(),
2260 }),
2261 range: Some(symbol_range),
2262 }))
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 let snapshot = editor.snapshot(window, cx);
2266 let anchor = snapshot
2267 .buffer_snapshot()
2268 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2269 hover_at(editor, Some(anchor), None, window, cx)
2270 });
2271 cx.background_executor
2272 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2273 requests.next().await;
2274
2275 // Hover should be visible
2276 cx.editor(|editor, _, _| {
2277 assert!(editor.hover_state.visible());
2278 });
2279
2280 // Move mouse away (hover_at with None anchor triggers the hiding delay)
2281 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2282
2283 // Popover should still be visible before the custom hiding delay expires
2284 cx.background_executor
2285 .advance_clock(Duration::from_millis(custom_delay_ms - 100));
2286 cx.editor(|editor, _, _| {
2287 assert!(
2288 editor.hover_state.visible(),
2289 "Popover should remain visible before the hiding delay expires"
2290 );
2291 });
2292
2293 // After the full custom delay, the popover should be hidden
2294 cx.background_executor
2295 .advance_clock(Duration::from_millis(200));
2296 cx.editor(|editor, _, _| {
2297 assert!(
2298 !editor.hover_state.visible(),
2299 "Popover should be hidden after the hiding delay expires"
2300 );
2301 });
2302 }
2303
2304 #[gpui::test]
2305 async fn test_hover_popover_sticky_disabled(cx: &mut gpui::TestAppContext) {
2306 init_test(cx, |_| {});
2307
2308 cx.update(|cx| {
2309 cx.update_global::<SettingsStore, _>(|settings, cx| {
2310 settings.update_user_settings(cx, |settings| {
2311 settings.editor.hover_popover_sticky = Some(false);
2312 });
2313 });
2314 });
2315
2316 let mut cx = EditorLspTestContext::new_rust(
2317 lsp::ServerCapabilities {
2318 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2319 ..Default::default()
2320 },
2321 cx,
2322 )
2323 .await;
2324
2325 cx.set_state(indoc! {"
2326 fn ˇtest() { println!(); }
2327 "});
2328
2329 // Trigger hover on a symbol
2330 let hover_point = cx.display_point(indoc! {"
2331 fn test() { printˇln!(); }
2332 "});
2333 let symbol_range = cx.lsp_range(indoc! {"
2334 fn test() { «println!»(); }
2335 "});
2336 let mut requests =
2337 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2338 Ok(Some(lsp::Hover {
2339 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2340 kind: lsp::MarkupKind::Markdown,
2341 value: "some basic docs".to_string(),
2342 }),
2343 range: Some(symbol_range),
2344 }))
2345 });
2346 cx.update_editor(|editor, window, cx| {
2347 let snapshot = editor.snapshot(window, cx);
2348 let anchor = snapshot
2349 .buffer_snapshot()
2350 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2351 hover_at(editor, Some(anchor), None, window, cx)
2352 });
2353 cx.background_executor
2354 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2355 requests.next().await;
2356
2357 // Hover should be visible
2358 cx.editor(|editor, _, _| {
2359 assert!(editor.hover_state.visible());
2360 });
2361
2362 // Move mouse away — with sticky disabled, hide immediately
2363 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2364
2365 // Popover should be hidden immediately without any delay
2366 cx.editor(|editor, _, _| {
2367 assert!(
2368 !editor.hover_state.visible(),
2369 "Popover should be hidden immediately when sticky is disabled"
2370 );
2371 });
2372 }
2373
2374 #[gpui::test]
2375 async fn test_hover_popover_hiding_delay_restarts_when_mouse_gets_closer(
2376 cx: &mut gpui::TestAppContext,
2377 ) {
2378 init_test(cx, |_| {});
2379
2380 let custom_delay_ms = 600u64;
2381 cx.update(|cx| {
2382 cx.update_global::<SettingsStore, _>(|settings, cx| {
2383 settings.update_user_settings(cx, |settings| {
2384 settings.editor.hover_popover_sticky = Some(true);
2385 settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2386 });
2387 });
2388 });
2389
2390 let mut cx = EditorLspTestContext::new_rust(
2391 lsp::ServerCapabilities {
2392 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2393 ..Default::default()
2394 },
2395 cx,
2396 )
2397 .await;
2398
2399 cx.set_state(indoc! {"
2400 fn ˇtest() { println!(); }
2401 "});
2402
2403 let hover_point = cx.display_point(indoc! {"
2404 fn test() { printˇln!(); }
2405 "});
2406 let symbol_range = cx.lsp_range(indoc! {"
2407 fn test() { «println!»(); }
2408 "});
2409 let mut requests =
2410 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2411 Ok(Some(lsp::Hover {
2412 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2413 kind: lsp::MarkupKind::Markdown,
2414 value: "some basic docs".to_string(),
2415 }),
2416 range: Some(symbol_range),
2417 }))
2418 });
2419 cx.update_editor(|editor, window, cx| {
2420 let snapshot = editor.snapshot(window, cx);
2421 let anchor = snapshot
2422 .buffer_snapshot()
2423 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2424 hover_at(editor, Some(anchor), None, window, cx)
2425 });
2426 cx.background_executor
2427 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2428 requests.next().await;
2429
2430 cx.editor(|editor, _, _| {
2431 assert!(editor.hover_state.visible());
2432 });
2433
2434 cx.update_editor(|editor, _, _| {
2435 let popover = editor.hover_state.info_popovers.first().unwrap();
2436 popover.last_bounds.set(Some(Bounds {
2437 origin: gpui::Point {
2438 x: px(100.0),
2439 y: px(100.0),
2440 },
2441 size: Size {
2442 width: px(100.0),
2443 height: px(60.0),
2444 },
2445 }));
2446 });
2447
2448 let far_point = gpui::Point {
2449 x: px(260.0),
2450 y: px(130.0),
2451 };
2452 cx.update_editor(|editor, window, cx| hover_at(editor, None, Some(far_point), window, cx));
2453
2454 cx.background_executor
2455 .advance_clock(Duration::from_millis(400));
2456 cx.background_executor.run_until_parked();
2457
2458 let closer_point = gpui::Point {
2459 x: px(220.0),
2460 y: px(130.0),
2461 };
2462 cx.update_editor(|editor, window, cx| {
2463 hover_at(editor, None, Some(closer_point), window, cx)
2464 });
2465
2466 cx.background_executor
2467 .advance_clock(Duration::from_millis(250));
2468 cx.background_executor.run_until_parked();
2469
2470 cx.editor(|editor, _, _| {
2471 assert!(
2472 editor.hover_state.visible(),
2473 "Popover should remain visible because moving closer restarts the hiding timer"
2474 );
2475 });
2476
2477 cx.background_executor
2478 .advance_clock(Duration::from_millis(350));
2479 cx.background_executor.run_until_parked();
2480
2481 cx.editor(|editor, _, _| {
2482 assert!(
2483 !editor.hover_state.visible(),
2484 "Popover should hide after the restarted hiding timer expires"
2485 );
2486 });
2487 }
2488
2489 #[gpui::test]
2490 async fn test_hover_popover_cancel_hide_on_rehover(cx: &mut gpui::TestAppContext) {
2491 init_test(cx, |_| {});
2492
2493 let custom_delay_ms = 500u64;
2494 cx.update(|cx| {
2495 cx.update_global::<SettingsStore, _>(|settings, cx| {
2496 settings.update_user_settings(cx, |settings| {
2497 settings.editor.hover_popover_sticky = Some(true);
2498 settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2499 });
2500 });
2501 });
2502
2503 let mut cx = EditorLspTestContext::new_rust(
2504 lsp::ServerCapabilities {
2505 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2506 ..Default::default()
2507 },
2508 cx,
2509 )
2510 .await;
2511
2512 cx.set_state(indoc! {"
2513 fn ˇtest() { println!(); }
2514 "});
2515
2516 let hover_point = cx.display_point(indoc! {"
2517 fn test() { printˇln!(); }
2518 "});
2519 let symbol_range = cx.lsp_range(indoc! {"
2520 fn test() { «println!»(); }
2521 "});
2522 let mut requests =
2523 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2524 Ok(Some(lsp::Hover {
2525 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2526 kind: lsp::MarkupKind::Markdown,
2527 value: "some basic docs".to_string(),
2528 }),
2529 range: Some(symbol_range),
2530 }))
2531 });
2532 cx.update_editor(|editor, window, cx| {
2533 let snapshot = editor.snapshot(window, cx);
2534 let anchor = snapshot
2535 .buffer_snapshot()
2536 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2537 hover_at(editor, Some(anchor), None, window, cx)
2538 });
2539 cx.background_executor
2540 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2541 requests.next().await;
2542
2543 cx.editor(|editor, _, _| {
2544 assert!(editor.hover_state.visible());
2545 });
2546
2547 // Move mouse away — starts the 500ms hide timer
2548 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2549
2550 cx.background_executor
2551 .advance_clock(Duration::from_millis(300));
2552 cx.background_executor.run_until_parked();
2553 cx.editor(|editor, _, _| {
2554 assert!(
2555 editor.hover_state.visible(),
2556 "Popover should still be visible before hiding delay expires"
2557 );
2558 });
2559
2560 // Move back to the symbol — should cancel the hiding timer
2561 cx.update_editor(|editor, window, cx| {
2562 let snapshot = editor.snapshot(window, cx);
2563 let anchor = snapshot
2564 .buffer_snapshot()
2565 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2566 hover_at(editor, Some(anchor), None, window, cx)
2567 });
2568
2569 // Advance past the original deadline — popover should still be visible
2570 // because re-hovering cleared the hiding_delay_task
2571 cx.background_executor
2572 .advance_clock(Duration::from_millis(300));
2573 cx.background_executor.run_until_parked();
2574 cx.editor(|editor, _, _| {
2575 assert!(
2576 editor.hover_state.visible(),
2577 "Popover should remain visible after re-hovering the symbol"
2578 );
2579 assert!(
2580 editor.hover_state.hiding_delay_task.is_none(),
2581 "Hiding delay task should have been cleared by re-hover"
2582 );
2583 });
2584
2585 // Move away again — starts a fresh 500ms timer
2586 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2587
2588 cx.background_executor
2589 .advance_clock(Duration::from_millis(custom_delay_ms + 100));
2590 cx.background_executor.run_until_parked();
2591 cx.editor(|editor, _, _| {
2592 assert!(
2593 !editor.hover_state.visible(),
2594 "Popover should hide after the new hiding timer expires"
2595 );
2596 });
2597 }
2598
2599 #[gpui::test]
2600 async fn test_hover_popover_enabled_false_ignores_sticky(cx: &mut gpui::TestAppContext) {
2601 init_test(cx, |_| {});
2602
2603 cx.update(|cx| {
2604 cx.update_global::<SettingsStore, _>(|settings, cx| {
2605 settings.update_user_settings(cx, |settings| {
2606 settings.editor.hover_popover_enabled = Some(false);
2607 settings.editor.hover_popover_sticky = Some(true);
2608 settings.editor.hover_popover_hiding_delay = Some(DelayMs(500));
2609 });
2610 });
2611 });
2612
2613 let mut cx = EditorLspTestContext::new_rust(
2614 lsp::ServerCapabilities {
2615 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2616 ..Default::default()
2617 },
2618 cx,
2619 )
2620 .await;
2621
2622 cx.set_state(indoc! {"
2623 fn ˇtest() { println!(); }
2624 "});
2625
2626 let hover_point = cx.display_point(indoc! {"
2627 fn test() { printˇln!(); }
2628 "});
2629
2630 // Trigger hover_at — should be gated by hover_popover_enabled=false
2631 cx.update_editor(|editor, window, cx| {
2632 let snapshot = editor.snapshot(window, cx);
2633 let anchor = snapshot
2634 .buffer_snapshot()
2635 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2636 hover_at(editor, Some(anchor), None, window, cx)
2637 });
2638
2639 // No need to advance clock or wait for LSP — the gate should prevent any work
2640 cx.editor(|editor, _, _| {
2641 assert!(
2642 !editor.hover_state.visible(),
2643 "Popover should not appear when hover_popover_enabled is false"
2644 );
2645 assert!(
2646 editor.hover_state.info_task.is_none(),
2647 "No hover info task should be scheduled when hover is disabled"
2648 );
2649 assert!(
2650 editor.hover_state.triggered_from.is_none(),
2651 "No hover trigger should be recorded when hover is disabled"
2652 );
2653 });
2654 }
2655}