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 nearest_valid: previous_valid,
1956 exact_unclipped,
1957 column_overshoot_after_line_end: 0,
1958 }
1959 });
1960 cx.update_editor(|editor, window, cx| {
1961 editor.update_inlay_link_and_hover_points(
1962 &editor.snapshot(window, cx),
1963 new_type_hint_part_hover_position,
1964 None,
1965 true,
1966 false,
1967 window,
1968 cx,
1969 );
1970 });
1971
1972 let resolve_closure_uri = uri.clone();
1973 cx.lsp
1974 .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
1975 move |mut hint_to_resolve, _| {
1976 let mut resolved_hint_positions = BTreeSet::new();
1977 let task_uri = resolve_closure_uri.clone();
1978 async move {
1979 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1980 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1981
1982 // `: TestNewType<TestStruct>`
1983 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1984 lsp::InlayHintLabelPart {
1985 value: ": ".to_string(),
1986 ..Default::default()
1987 },
1988 lsp::InlayHintLabelPart {
1989 value: new_type_label.to_string(),
1990 location: Some(lsp::Location {
1991 uri: task_uri.clone(),
1992 range: new_type_target_range,
1993 }),
1994 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1995 "A tooltip for `{new_type_label}`"
1996 ))),
1997 ..Default::default()
1998 },
1999 lsp::InlayHintLabelPart {
2000 value: "<".to_string(),
2001 ..Default::default()
2002 },
2003 lsp::InlayHintLabelPart {
2004 value: struct_label.to_string(),
2005 location: Some(lsp::Location {
2006 uri: task_uri,
2007 range: struct_target_range,
2008 }),
2009 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
2010 lsp::MarkupContent {
2011 kind: lsp::MarkupKind::Markdown,
2012 value: format!("A tooltip for `{struct_label}`"),
2013 },
2014 )),
2015 ..Default::default()
2016 },
2017 lsp::InlayHintLabelPart {
2018 value: ">".to_string(),
2019 ..Default::default()
2020 },
2021 ]);
2022
2023 Ok(hint_to_resolve)
2024 }
2025 },
2026 )
2027 .next()
2028 .await;
2029 cx.background_executor.run_until_parked();
2030
2031 cx.update_editor(|editor, window, cx| {
2032 editor.update_inlay_link_and_hover_points(
2033 &editor.snapshot(window, cx),
2034 new_type_hint_part_hover_position,
2035 None,
2036 true,
2037 false,
2038 window,
2039 cx,
2040 );
2041 });
2042 cx.background_executor
2043 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2044 cx.background_executor.run_until_parked();
2045 cx.update_editor(|editor, _, cx| {
2046 let hover_state = &editor.hover_state;
2047 assert!(
2048 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2049 );
2050 let popover = hover_state.info_popovers.first().unwrap();
2051 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2052 assert_eq!(
2053 popover.symbol_range,
2054 RangeInEditor::Inlay(InlayHighlight {
2055 inlay: InlayId::Hint(0),
2056 inlay_position: buffer_snapshot
2057 .anchor_after(MultiBufferOffset(inlay_range.start)),
2058 range: ": ".len()..": ".len() + new_type_label.len(),
2059 }),
2060 "Popover range should match the new type label part"
2061 );
2062 assert_eq!(
2063 popover.get_rendered_text(cx),
2064 format!("A tooltip for {new_type_label}"),
2065 );
2066 });
2067
2068 let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
2069 let snapshot = editor.snapshot(window, cx);
2070 let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
2071 let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
2072 assert_eq!(previous_valid.row(), next_valid.row());
2073 assert!(previous_valid.column() < next_valid.column());
2074 let exact_unclipped = DisplayPoint::new(
2075 previous_valid.row(),
2076 previous_valid.column()
2077 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
2078 as u32,
2079 );
2080 PointForPosition {
2081 previous_valid,
2082 next_valid,
2083 nearest_valid: previous_valid,
2084 exact_unclipped,
2085 column_overshoot_after_line_end: 0,
2086 }
2087 });
2088 cx.update_editor(|editor, window, cx| {
2089 editor.update_inlay_link_and_hover_points(
2090 &editor.snapshot(window, cx),
2091 struct_hint_part_hover_position,
2092 None,
2093 true,
2094 false,
2095 window,
2096 cx,
2097 );
2098 });
2099 cx.background_executor
2100 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2101 cx.background_executor.run_until_parked();
2102 cx.update_editor(|editor, _, cx| {
2103 let hover_state = &editor.hover_state;
2104 assert!(
2105 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2106 );
2107 let popover = hover_state.info_popovers.first().unwrap();
2108 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2109 assert_eq!(
2110 popover.symbol_range,
2111 RangeInEditor::Inlay(InlayHighlight {
2112 inlay: InlayId::Hint(0),
2113 inlay_position: buffer_snapshot
2114 .anchor_after(MultiBufferOffset(inlay_range.start)),
2115 range: ": ".len() + new_type_label.len() + "<".len()
2116 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
2117 }),
2118 "Popover range should match the struct label part"
2119 );
2120 assert_eq!(
2121 popover.get_rendered_text(cx),
2122 format!("A tooltip for {struct_label}"),
2123 "Rendered markdown element should remove backticks from text"
2124 );
2125 });
2126 }
2127
2128 #[test]
2129 fn test_find_hovered_hint_part_with_multibyte_characters() {
2130 use crate::display_map::InlayOffset;
2131 use multi_buffer::MultiBufferOffset;
2132 use project::InlayHintLabelPart;
2133
2134 // Test with multi-byte UTF-8 character "→" (3 bytes, 1 character)
2135 let label = "→ app/Livewire/UserProfile.php";
2136 let label_parts = vec![InlayHintLabelPart {
2137 value: label.to_string(),
2138 tooltip: None,
2139 location: None,
2140 }];
2141
2142 let hint_start = InlayOffset(MultiBufferOffset(100));
2143
2144 // Verify the label has more bytes than characters (due to "→")
2145 assert_eq!(label.len(), 32); // bytes
2146 assert_eq!(label.chars().count(), 30); // characters
2147
2148 // Test hovering at the last byte (should find the part)
2149 let last_byte_offset = InlayOffset(MultiBufferOffset(100 + label.len() - 1));
2150 let result = find_hovered_hint_part(label_parts.clone(), hint_start, last_byte_offset);
2151 assert!(
2152 result.is_some(),
2153 "Should find part when hovering at last byte"
2154 );
2155 let (part, range) = result.unwrap();
2156 assert_eq!(part.value, label);
2157 assert_eq!(range.start, hint_start);
2158 assert_eq!(range.end, InlayOffset(MultiBufferOffset(100 + label.len())));
2159
2160 // Test hovering at the first byte of "→" (byte 0)
2161 let first_byte_offset = InlayOffset(MultiBufferOffset(100));
2162 let result = find_hovered_hint_part(label_parts.clone(), hint_start, first_byte_offset);
2163 assert!(
2164 result.is_some(),
2165 "Should find part when hovering at first byte"
2166 );
2167
2168 // Test hovering in the middle of "→" (byte 1, still part of the arrow character)
2169 let mid_arrow_offset = InlayOffset(MultiBufferOffset(101));
2170 let result = find_hovered_hint_part(label_parts, hint_start, mid_arrow_offset);
2171 assert!(
2172 result.is_some(),
2173 "Should find part when hovering in middle of multi-byte char"
2174 );
2175
2176 // Test with multiple parts containing multi-byte characters
2177 // Part ranges are [start, end) - start inclusive, end exclusive
2178 // "→ " occupies bytes [0, 4), "path" occupies bytes [4, 8)
2179 let parts = vec![
2180 InlayHintLabelPart {
2181 value: "→ ".to_string(), // 4 bytes (3 + 1)
2182 tooltip: None,
2183 location: None,
2184 },
2185 InlayHintLabelPart {
2186 value: "path".to_string(), // 4 bytes
2187 tooltip: None,
2188 location: None,
2189 },
2190 ];
2191
2192 // Hover at byte 3 (last byte of "→ ", the space character)
2193 let arrow_last_byte = InlayOffset(MultiBufferOffset(100 + 3));
2194 let result = find_hovered_hint_part(parts.clone(), hint_start, arrow_last_byte);
2195 assert!(result.is_some(), "Should find first part at its last byte");
2196 let (part, range) = result.unwrap();
2197 assert_eq!(part.value, "→ ");
2198 assert_eq!(
2199 range,
2200 InlayOffset(MultiBufferOffset(100))..InlayOffset(MultiBufferOffset(104))
2201 );
2202
2203 // Hover at byte 4 (first byte of "path", at the boundary)
2204 let path_start_offset = InlayOffset(MultiBufferOffset(100 + 4));
2205 let result = find_hovered_hint_part(parts.clone(), hint_start, path_start_offset);
2206 assert!(result.is_some(), "Should find second part at boundary");
2207 let (part, _) = result.unwrap();
2208 assert_eq!(part.value, "path");
2209
2210 // Hover at byte 7 (last byte of "path")
2211 let path_end_offset = InlayOffset(MultiBufferOffset(100 + 7));
2212 let result = find_hovered_hint_part(parts, hint_start, path_end_offset);
2213 assert!(result.is_some(), "Should find second part at last byte");
2214 let (part, range) = result.unwrap();
2215 assert_eq!(part.value, "path");
2216 assert_eq!(
2217 range,
2218 InlayOffset(MultiBufferOffset(104))..InlayOffset(MultiBufferOffset(108))
2219 );
2220 }
2221
2222 #[gpui::test]
2223 async fn test_hover_popover_hiding_delay(cx: &mut gpui::TestAppContext) {
2224 init_test(cx, |_| {});
2225
2226 let custom_delay_ms = 500u64;
2227 cx.update(|cx| {
2228 cx.update_global::<SettingsStore, _>(|settings, cx| {
2229 settings.update_user_settings(cx, |settings| {
2230 settings.editor.hover_popover_sticky = Some(true);
2231 settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2232 });
2233 });
2234 });
2235
2236 let mut cx = EditorLspTestContext::new_rust(
2237 lsp::ServerCapabilities {
2238 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2239 ..Default::default()
2240 },
2241 cx,
2242 )
2243 .await;
2244
2245 cx.set_state(indoc! {"
2246 fn ˇtest() { println!(); }
2247 "});
2248
2249 // Trigger hover on a symbol
2250 let hover_point = cx.display_point(indoc! {"
2251 fn test() { printˇln!(); }
2252 "});
2253 let symbol_range = cx.lsp_range(indoc! {"
2254 fn test() { «println!»(); }
2255 "});
2256 let mut requests =
2257 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2258 Ok(Some(lsp::Hover {
2259 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2260 kind: lsp::MarkupKind::Markdown,
2261 value: "some basic docs".to_string(),
2262 }),
2263 range: Some(symbol_range),
2264 }))
2265 });
2266 cx.update_editor(|editor, window, cx| {
2267 let snapshot = editor.snapshot(window, cx);
2268 let anchor = snapshot
2269 .buffer_snapshot()
2270 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2271 hover_at(editor, Some(anchor), None, window, cx)
2272 });
2273 cx.background_executor
2274 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2275 requests.next().await;
2276
2277 // Hover should be visible
2278 cx.editor(|editor, _, _| {
2279 assert!(editor.hover_state.visible());
2280 });
2281
2282 // Move mouse away (hover_at with None anchor triggers the hiding delay)
2283 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2284
2285 // Popover should still be visible before the custom hiding delay expires
2286 cx.background_executor
2287 .advance_clock(Duration::from_millis(custom_delay_ms - 100));
2288 cx.editor(|editor, _, _| {
2289 assert!(
2290 editor.hover_state.visible(),
2291 "Popover should remain visible before the hiding delay expires"
2292 );
2293 });
2294
2295 // After the full custom delay, the popover should be hidden
2296 cx.background_executor
2297 .advance_clock(Duration::from_millis(200));
2298 cx.editor(|editor, _, _| {
2299 assert!(
2300 !editor.hover_state.visible(),
2301 "Popover should be hidden after the hiding delay expires"
2302 );
2303 });
2304 }
2305
2306 #[gpui::test]
2307 async fn test_hover_popover_sticky_disabled(cx: &mut gpui::TestAppContext) {
2308 init_test(cx, |_| {});
2309
2310 cx.update(|cx| {
2311 cx.update_global::<SettingsStore, _>(|settings, cx| {
2312 settings.update_user_settings(cx, |settings| {
2313 settings.editor.hover_popover_sticky = Some(false);
2314 });
2315 });
2316 });
2317
2318 let mut cx = EditorLspTestContext::new_rust(
2319 lsp::ServerCapabilities {
2320 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2321 ..Default::default()
2322 },
2323 cx,
2324 )
2325 .await;
2326
2327 cx.set_state(indoc! {"
2328 fn ˇtest() { println!(); }
2329 "});
2330
2331 // Trigger hover on a symbol
2332 let hover_point = cx.display_point(indoc! {"
2333 fn test() { printˇln!(); }
2334 "});
2335 let symbol_range = cx.lsp_range(indoc! {"
2336 fn test() { «println!»(); }
2337 "});
2338 let mut requests =
2339 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2340 Ok(Some(lsp::Hover {
2341 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2342 kind: lsp::MarkupKind::Markdown,
2343 value: "some basic docs".to_string(),
2344 }),
2345 range: Some(symbol_range),
2346 }))
2347 });
2348 cx.update_editor(|editor, window, cx| {
2349 let snapshot = editor.snapshot(window, cx);
2350 let anchor = snapshot
2351 .buffer_snapshot()
2352 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2353 hover_at(editor, Some(anchor), None, window, cx)
2354 });
2355 cx.background_executor
2356 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2357 requests.next().await;
2358
2359 // Hover should be visible
2360 cx.editor(|editor, _, _| {
2361 assert!(editor.hover_state.visible());
2362 });
2363
2364 // Move mouse away — with sticky disabled, hide immediately
2365 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2366
2367 // Popover should be hidden immediately without any delay
2368 cx.editor(|editor, _, _| {
2369 assert!(
2370 !editor.hover_state.visible(),
2371 "Popover should be hidden immediately when sticky is disabled"
2372 );
2373 });
2374 }
2375
2376 #[gpui::test]
2377 async fn test_hover_popover_hiding_delay_restarts_when_mouse_gets_closer(
2378 cx: &mut gpui::TestAppContext,
2379 ) {
2380 init_test(cx, |_| {});
2381
2382 let custom_delay_ms = 600u64;
2383 cx.update(|cx| {
2384 cx.update_global::<SettingsStore, _>(|settings, cx| {
2385 settings.update_user_settings(cx, |settings| {
2386 settings.editor.hover_popover_sticky = Some(true);
2387 settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2388 });
2389 });
2390 });
2391
2392 let mut cx = EditorLspTestContext::new_rust(
2393 lsp::ServerCapabilities {
2394 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2395 ..Default::default()
2396 },
2397 cx,
2398 )
2399 .await;
2400
2401 cx.set_state(indoc! {"
2402 fn ˇtest() { println!(); }
2403 "});
2404
2405 let hover_point = cx.display_point(indoc! {"
2406 fn test() { printˇln!(); }
2407 "});
2408 let symbol_range = cx.lsp_range(indoc! {"
2409 fn test() { «println!»(); }
2410 "});
2411 let mut requests =
2412 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2413 Ok(Some(lsp::Hover {
2414 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2415 kind: lsp::MarkupKind::Markdown,
2416 value: "some basic docs".to_string(),
2417 }),
2418 range: Some(symbol_range),
2419 }))
2420 });
2421 cx.update_editor(|editor, window, cx| {
2422 let snapshot = editor.snapshot(window, cx);
2423 let anchor = snapshot
2424 .buffer_snapshot()
2425 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2426 hover_at(editor, Some(anchor), None, window, cx)
2427 });
2428 cx.background_executor
2429 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2430 requests.next().await;
2431
2432 cx.editor(|editor, _, _| {
2433 assert!(editor.hover_state.visible());
2434 });
2435
2436 cx.update_editor(|editor, _, _| {
2437 let popover = editor.hover_state.info_popovers.first().unwrap();
2438 popover.last_bounds.set(Some(Bounds {
2439 origin: gpui::Point {
2440 x: px(100.0),
2441 y: px(100.0),
2442 },
2443 size: Size {
2444 width: px(100.0),
2445 height: px(60.0),
2446 },
2447 }));
2448 });
2449
2450 let far_point = gpui::Point {
2451 x: px(260.0),
2452 y: px(130.0),
2453 };
2454 cx.update_editor(|editor, window, cx| hover_at(editor, None, Some(far_point), window, cx));
2455
2456 cx.background_executor
2457 .advance_clock(Duration::from_millis(400));
2458 cx.background_executor.run_until_parked();
2459
2460 let closer_point = gpui::Point {
2461 x: px(220.0),
2462 y: px(130.0),
2463 };
2464 cx.update_editor(|editor, window, cx| {
2465 hover_at(editor, None, Some(closer_point), window, cx)
2466 });
2467
2468 cx.background_executor
2469 .advance_clock(Duration::from_millis(250));
2470 cx.background_executor.run_until_parked();
2471
2472 cx.editor(|editor, _, _| {
2473 assert!(
2474 editor.hover_state.visible(),
2475 "Popover should remain visible because moving closer restarts the hiding timer"
2476 );
2477 });
2478
2479 cx.background_executor
2480 .advance_clock(Duration::from_millis(350));
2481 cx.background_executor.run_until_parked();
2482
2483 cx.editor(|editor, _, _| {
2484 assert!(
2485 !editor.hover_state.visible(),
2486 "Popover should hide after the restarted hiding timer expires"
2487 );
2488 });
2489 }
2490
2491 #[gpui::test]
2492 async fn test_hover_popover_cancel_hide_on_rehover(cx: &mut gpui::TestAppContext) {
2493 init_test(cx, |_| {});
2494
2495 let custom_delay_ms = 500u64;
2496 cx.update(|cx| {
2497 cx.update_global::<SettingsStore, _>(|settings, cx| {
2498 settings.update_user_settings(cx, |settings| {
2499 settings.editor.hover_popover_sticky = Some(true);
2500 settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2501 });
2502 });
2503 });
2504
2505 let mut cx = EditorLspTestContext::new_rust(
2506 lsp::ServerCapabilities {
2507 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2508 ..Default::default()
2509 },
2510 cx,
2511 )
2512 .await;
2513
2514 cx.set_state(indoc! {"
2515 fn ˇtest() { println!(); }
2516 "});
2517
2518 let hover_point = cx.display_point(indoc! {"
2519 fn test() { printˇln!(); }
2520 "});
2521 let symbol_range = cx.lsp_range(indoc! {"
2522 fn test() { «println!»(); }
2523 "});
2524 let mut requests =
2525 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2526 Ok(Some(lsp::Hover {
2527 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2528 kind: lsp::MarkupKind::Markdown,
2529 value: "some basic docs".to_string(),
2530 }),
2531 range: Some(symbol_range),
2532 }))
2533 });
2534 cx.update_editor(|editor, window, cx| {
2535 let snapshot = editor.snapshot(window, cx);
2536 let anchor = snapshot
2537 .buffer_snapshot()
2538 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2539 hover_at(editor, Some(anchor), None, window, cx)
2540 });
2541 cx.background_executor
2542 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2543 requests.next().await;
2544
2545 cx.editor(|editor, _, _| {
2546 assert!(editor.hover_state.visible());
2547 });
2548
2549 // Move mouse away — starts the 500ms hide timer
2550 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2551
2552 cx.background_executor
2553 .advance_clock(Duration::from_millis(300));
2554 cx.background_executor.run_until_parked();
2555 cx.editor(|editor, _, _| {
2556 assert!(
2557 editor.hover_state.visible(),
2558 "Popover should still be visible before hiding delay expires"
2559 );
2560 });
2561
2562 // Move back to the symbol — should cancel the hiding timer
2563 cx.update_editor(|editor, window, cx| {
2564 let snapshot = editor.snapshot(window, cx);
2565 let anchor = snapshot
2566 .buffer_snapshot()
2567 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2568 hover_at(editor, Some(anchor), None, window, cx)
2569 });
2570
2571 // Advance past the original deadline — popover should still be visible
2572 // because re-hovering cleared the hiding_delay_task
2573 cx.background_executor
2574 .advance_clock(Duration::from_millis(300));
2575 cx.background_executor.run_until_parked();
2576 cx.editor(|editor, _, _| {
2577 assert!(
2578 editor.hover_state.visible(),
2579 "Popover should remain visible after re-hovering the symbol"
2580 );
2581 assert!(
2582 editor.hover_state.hiding_delay_task.is_none(),
2583 "Hiding delay task should have been cleared by re-hover"
2584 );
2585 });
2586
2587 // Move away again — starts a fresh 500ms timer
2588 cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2589
2590 cx.background_executor
2591 .advance_clock(Duration::from_millis(custom_delay_ms + 100));
2592 cx.background_executor.run_until_parked();
2593 cx.editor(|editor, _, _| {
2594 assert!(
2595 !editor.hover_state.visible(),
2596 "Popover should hide after the new hiding timer expires"
2597 );
2598 });
2599 }
2600
2601 #[gpui::test]
2602 async fn test_hover_popover_enabled_false_ignores_sticky(cx: &mut gpui::TestAppContext) {
2603 init_test(cx, |_| {});
2604
2605 cx.update(|cx| {
2606 cx.update_global::<SettingsStore, _>(|settings, cx| {
2607 settings.update_user_settings(cx, |settings| {
2608 settings.editor.hover_popover_enabled = Some(false);
2609 settings.editor.hover_popover_sticky = Some(true);
2610 settings.editor.hover_popover_hiding_delay = Some(DelayMs(500));
2611 });
2612 });
2613 });
2614
2615 let mut cx = EditorLspTestContext::new_rust(
2616 lsp::ServerCapabilities {
2617 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2618 ..Default::default()
2619 },
2620 cx,
2621 )
2622 .await;
2623
2624 cx.set_state(indoc! {"
2625 fn ˇtest() { println!(); }
2626 "});
2627
2628 let hover_point = cx.display_point(indoc! {"
2629 fn test() { printˇln!(); }
2630 "});
2631
2632 // Trigger hover_at — should be gated by hover_popover_enabled=false
2633 cx.update_editor(|editor, window, cx| {
2634 let snapshot = editor.snapshot(window, cx);
2635 let anchor = snapshot
2636 .buffer_snapshot()
2637 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2638 hover_at(editor, Some(anchor), None, window, cx)
2639 });
2640
2641 // No need to advance clock or wait for LSP — the gate should prevent any work
2642 cx.editor(|editor, _, _| {
2643 assert!(
2644 !editor.hover_state.visible(),
2645 "Popover should not appear when hover_popover_enabled is false"
2646 );
2647 assert!(
2648 editor.hover_state.info_task.is_none(),
2649 "No hover info task should be scheduled when hover is disabled"
2650 );
2651 assert!(
2652 editor.hover_state.triggered_from.is_none(),
2653 "No hover trigger should be recorded when hover is disabled"
2654 );
2655 });
2656 }
2657}