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