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