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