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 // Clamp the point within the visible rows in case the popup source spans multiple lines
801 if visible_rows.end <= point.row() {
802 point = crate::movement::up_by_rows(
803 &snapshot.display_snapshot,
804 point,
805 1 + (point.row() - visible_rows.end).0,
806 text::SelectionGoal::None,
807 true,
808 text_layout_details,
809 )
810 .0;
811 } else if point.row() < visible_rows.start {
812 point = crate::movement::down_by_rows(
813 &snapshot.display_snapshot,
814 point,
815 (visible_rows.start - point.row()).0,
816 text::SelectionGoal::None,
817 true,
818 text_layout_details,
819 )
820 .0;
821 }
822
823 if !visible_rows.contains(&point.row()) {
824 log::error!("Hover popover point out of bounds after moving");
825 return None;
826 }
827
828 let mut elements = Vec::new();
829
830 if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
831 elements.push(diagnostic_popover.render(max_size, window, cx));
832 }
833 for info_popover in &mut self.info_popovers {
834 elements.push(info_popover.render(max_size, window, cx));
835 }
836
837 Some((point, elements))
838 }
839
840 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
841 let mut hover_popover_is_focused = false;
842 for info_popover in &self.info_popovers {
843 if let Some(markdown_view) = &info_popover.parsed_content
844 && markdown_view.focus_handle(cx).is_focused(window)
845 {
846 hover_popover_is_focused = true;
847 }
848 }
849 if let Some(diagnostic_popover) = &self.diagnostic_popover
850 && diagnostic_popover
851 .markdown
852 .focus_handle(cx)
853 .is_focused(window)
854 {
855 hover_popover_is_focused = true;
856 }
857 hover_popover_is_focused
858 }
859}
860
861pub struct InfoPopover {
862 pub symbol_range: RangeInEditor,
863 pub parsed_content: Option<Entity<Markdown>>,
864 pub scroll_handle: ScrollHandle,
865 pub keyboard_grace: Rc<RefCell<bool>>,
866 pub anchor: Option<Anchor>,
867 _subscription: Option<Subscription>,
868}
869
870impl InfoPopover {
871 pub(crate) fn render(
872 &mut self,
873 max_size: Size<Pixels>,
874 window: &mut Window,
875 cx: &mut Context<Editor>,
876 ) -> AnyElement {
877 let keyboard_grace = Rc::clone(&self.keyboard_grace);
878 div()
879 .id("info_popover")
880 .occlude()
881 .elevation_2(cx)
882 // Prevent a mouse down/move on the popover from being propagated to the editor,
883 // because that would dismiss the popover.
884 .on_mouse_move(|_, _, cx| cx.stop_propagation())
885 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
886 let mut keyboard_grace = keyboard_grace.borrow_mut();
887 *keyboard_grace = false;
888 cx.stop_propagation();
889 })
890 .p_2()
891 .when_some(self.parsed_content.clone(), |this, markdown| {
892 this.child(
893 div()
894 .id("info-md-container")
895 .overflow_y_scroll()
896 .max_w(max_size.width)
897 .max_h(max_size.height)
898 .track_scroll(&self.scroll_handle)
899 .child(
900 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
901 .code_block_renderer(markdown::CodeBlockRenderer::Default {
902 copy_button: false,
903 copy_button_on_hover: false,
904 border: false,
905 })
906 .on_url_click(open_markdown_url),
907 ),
908 )
909 .custom_scrollbars(
910 Scrollbars::for_settings::<EditorSettings>()
911 .tracked_scroll_handle(self.scroll_handle.clone()),
912 window,
913 cx,
914 )
915 })
916 .into_any_element()
917 }
918
919 pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
920 let mut current = self.scroll_handle.offset();
921 current.y -= amount.pixels(
922 window.line_height(),
923 self.scroll_handle.bounds().size.height - px(16.),
924 ) / 2.0;
925 cx.notify();
926 self.scroll_handle.set_offset(current);
927 }
928}
929
930pub struct DiagnosticPopover {
931 pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
932 markdown: Entity<Markdown>,
933 border_color: Hsla,
934 background_color: Hsla,
935 pub keyboard_grace: Rc<RefCell<bool>>,
936 pub anchor: Anchor,
937 _subscription: Subscription,
938 pub scroll_handle: ScrollHandle,
939}
940
941impl DiagnosticPopover {
942 pub fn render(
943 &self,
944 max_size: Size<Pixels>,
945 window: &mut Window,
946 cx: &mut Context<Editor>,
947 ) -> AnyElement {
948 let keyboard_grace = Rc::clone(&self.keyboard_grace);
949 let this = cx.entity().downgrade();
950 div()
951 .id("diagnostic")
952 .occlude()
953 .elevation_2_borderless(cx)
954 // Don't draw the background color if the theme
955 // allows transparent surfaces.
956 .when(theme_is_transparent(cx), |this| {
957 this.bg(gpui::transparent_black())
958 })
959 // Prevent a mouse move on the popover from being propagated to the editor,
960 // because that would dismiss the popover.
961 .on_mouse_move(|_, _, cx| cx.stop_propagation())
962 // Prevent a mouse down on the popover from being propagated to the editor,
963 // because that would move the cursor.
964 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
965 let mut keyboard_grace = keyboard_grace.borrow_mut();
966 *keyboard_grace = false;
967 cx.stop_propagation();
968 })
969 .child(
970 div()
971 .py_1()
972 .px_2()
973 .bg(self.background_color)
974 .border_1()
975 .border_color(self.border_color)
976 .rounded_lg()
977 .child(
978 div()
979 .id("diagnostic-content-container")
980 .overflow_y_scroll()
981 .max_w(max_size.width)
982 .max_h(max_size.height)
983 .track_scroll(&self.scroll_handle)
984 .child(
985 MarkdownElement::new(
986 self.markdown.clone(),
987 diagnostics_markdown_style(window, cx),
988 )
989 .on_url_click(
990 move |link, window, cx| {
991 if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
992 {
993 this.update(cx, |this, cx| {
994 renderer.as_ref().open_link(this, link, window, cx);
995 })
996 .ok();
997 }
998 },
999 ),
1000 ),
1001 )
1002 .custom_scrollbars(
1003 Scrollbars::for_settings::<EditorSettings>()
1004 .tracked_scroll_handle(self.scroll_handle.clone()),
1005 window,
1006 cx,
1007 ),
1008 )
1009 .into_any_element()
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016 use crate::{
1017 PointForPosition,
1018 actions::ConfirmCompletion,
1019 editor_tests::{handle_completion_request, init_test},
1020 inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
1021 test::editor_lsp_test_context::EditorLspTestContext,
1022 };
1023 use collections::BTreeSet;
1024 use gpui::App;
1025 use indoc::indoc;
1026 use markdown::parser::MarkdownEvent;
1027 use project::InlayId;
1028 use settings::InlayHintSettingsContent;
1029 use smol::stream::StreamExt;
1030 use std::sync::atomic;
1031 use std::sync::atomic::AtomicUsize;
1032 use text::Bias;
1033
1034 fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
1035 cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 })
1036 }
1037
1038 impl InfoPopover {
1039 fn get_rendered_text(&self, cx: &gpui::App) -> String {
1040 let mut rendered_text = String::new();
1041 if let Some(parsed_content) = self.parsed_content.clone() {
1042 let markdown = parsed_content.read(cx);
1043 let text = markdown.parsed_markdown().source().to_string();
1044 let data = markdown.parsed_markdown().events();
1045 let slice = data;
1046
1047 for (range, event) in slice.iter() {
1048 match event {
1049 MarkdownEvent::SubstitutedText(parsed) => {
1050 rendered_text.push_str(parsed.as_str())
1051 }
1052 MarkdownEvent::Text | MarkdownEvent::Code => {
1053 rendered_text.push_str(&text[range.clone()])
1054 }
1055 _ => {}
1056 }
1057 }
1058 }
1059 rendered_text
1060 }
1061 }
1062
1063 #[gpui::test]
1064 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
1065 cx: &mut gpui::TestAppContext,
1066 ) {
1067 init_test(cx, |_| {});
1068
1069 let mut cx = EditorLspTestContext::new_rust(
1070 lsp::ServerCapabilities {
1071 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1072 completion_provider: Some(lsp::CompletionOptions {
1073 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
1074 resolve_provider: Some(true),
1075 ..Default::default()
1076 }),
1077 ..Default::default()
1078 },
1079 cx,
1080 )
1081 .await;
1082 let counter = Arc::new(AtomicUsize::new(0));
1083 // Basic hover delays and then pops without moving the mouse
1084 cx.set_state(indoc! {"
1085 oneˇ
1086 two
1087 three
1088 fn test() { println!(); }
1089 "});
1090
1091 //prompt autocompletion menu
1092 cx.simulate_keystroke(".");
1093 handle_completion_request(
1094 indoc! {"
1095 one.|<>
1096 two
1097 three
1098 "},
1099 vec!["first_completion", "second_completion"],
1100 true,
1101 counter.clone(),
1102 &mut cx,
1103 )
1104 .await;
1105 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
1106 .await;
1107 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
1108
1109 let hover_point = cx.display_point(indoc! {"
1110 one.
1111 two
1112 three
1113 fn test() { printˇln!(); }
1114 "});
1115 cx.update_editor(|editor, window, cx| {
1116 let snapshot = editor.snapshot(window, cx);
1117 let anchor = snapshot
1118 .buffer_snapshot()
1119 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1120 hover_at(editor, Some(anchor), window, cx)
1121 });
1122 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1123
1124 // After delay, hover should be visible.
1125 let symbol_range = cx.lsp_range(indoc! {"
1126 one.
1127 two
1128 three
1129 fn test() { «println!»(); }
1130 "});
1131 let mut requests =
1132 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1133 Ok(Some(lsp::Hover {
1134 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1135 kind: lsp::MarkupKind::Markdown,
1136 value: "some basic docs".to_string(),
1137 }),
1138 range: Some(symbol_range),
1139 }))
1140 });
1141 cx.background_executor
1142 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1143 requests.next().await;
1144
1145 cx.editor(|editor, _window, cx| {
1146 assert!(editor.hover_state.visible());
1147 assert_eq!(
1148 editor.hover_state.info_popovers.len(),
1149 1,
1150 "Expected exactly one hover but got: {:?}",
1151 editor.hover_state.info_popovers.len()
1152 );
1153 let rendered_text = editor
1154 .hover_state
1155 .info_popovers
1156 .first()
1157 .unwrap()
1158 .get_rendered_text(cx);
1159 assert_eq!(rendered_text, "some basic docs".to_string())
1160 });
1161
1162 // check that the completion menu is still visible and that there still has only been 1 completion request
1163 cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
1164 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1165
1166 //apply a completion and check it was successfully applied
1167 let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1168 editor.context_menu_next(&Default::default(), window, cx);
1169 editor
1170 .confirm_completion(&ConfirmCompletion::default(), window, cx)
1171 .unwrap()
1172 });
1173 cx.assert_editor_state(indoc! {"
1174 one.second_completionˇ
1175 two
1176 three
1177 fn test() { println!(); }
1178 "});
1179
1180 // check that the completion menu is no longer visible and that there still has only been 1 completion request
1181 cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1182 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1183
1184 //verify the information popover is still visible and unchanged
1185 cx.editor(|editor, _, cx| {
1186 assert!(editor.hover_state.visible());
1187 assert_eq!(
1188 editor.hover_state.info_popovers.len(),
1189 1,
1190 "Expected exactly one hover but got: {:?}",
1191 editor.hover_state.info_popovers.len()
1192 );
1193 let rendered_text = editor
1194 .hover_state
1195 .info_popovers
1196 .first()
1197 .unwrap()
1198 .get_rendered_text(cx);
1199
1200 assert_eq!(rendered_text, "some basic docs".to_string())
1201 });
1202
1203 // Mouse moved with no hover response dismisses
1204 let hover_point = cx.display_point(indoc! {"
1205 one.second_completionˇ
1206 two
1207 three
1208 fn teˇst() { println!(); }
1209 "});
1210 let mut request = cx
1211 .lsp
1212 .set_request_handler::<lsp::request::HoverRequest, _, _>(
1213 |_, _| async move { Ok(None) },
1214 );
1215 cx.update_editor(|editor, window, cx| {
1216 let snapshot = editor.snapshot(window, cx);
1217 let anchor = snapshot
1218 .buffer_snapshot()
1219 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1220 hover_at(editor, Some(anchor), window, cx)
1221 });
1222 cx.background_executor
1223 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1224 request.next().await;
1225
1226 // verify that the information popover is no longer visible
1227 cx.editor(|editor, _, _| {
1228 assert!(!editor.hover_state.visible());
1229 });
1230 }
1231
1232 #[gpui::test]
1233 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1234 init_test(cx, |_| {});
1235
1236 let mut cx = EditorLspTestContext::new_rust(
1237 lsp::ServerCapabilities {
1238 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1239 ..Default::default()
1240 },
1241 cx,
1242 )
1243 .await;
1244
1245 // Basic hover delays and then pops without moving the mouse
1246 cx.set_state(indoc! {"
1247 fn ˇtest() { println!(); }
1248 "});
1249 let hover_point = cx.display_point(indoc! {"
1250 fn test() { printˇln!(); }
1251 "});
1252
1253 cx.update_editor(|editor, window, cx| {
1254 let snapshot = editor.snapshot(window, cx);
1255 let anchor = snapshot
1256 .buffer_snapshot()
1257 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1258 hover_at(editor, Some(anchor), window, cx)
1259 });
1260 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1261
1262 // After delay, hover should be visible.
1263 let symbol_range = cx.lsp_range(indoc! {"
1264 fn test() { «println!»(); }
1265 "});
1266 let mut requests =
1267 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1268 Ok(Some(lsp::Hover {
1269 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1270 kind: lsp::MarkupKind::Markdown,
1271 value: "some basic docs".to_string(),
1272 }),
1273 range: Some(symbol_range),
1274 }))
1275 });
1276 cx.background_executor
1277 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1278 requests.next().await;
1279
1280 cx.editor(|editor, _, cx| {
1281 assert!(editor.hover_state.visible());
1282 assert_eq!(
1283 editor.hover_state.info_popovers.len(),
1284 1,
1285 "Expected exactly one hover but got: {:?}",
1286 editor.hover_state.info_popovers.len()
1287 );
1288 let rendered_text = editor
1289 .hover_state
1290 .info_popovers
1291 .first()
1292 .unwrap()
1293 .get_rendered_text(cx);
1294
1295 assert_eq!(rendered_text, "some basic docs".to_string())
1296 });
1297
1298 // Mouse moved with no hover response dismisses
1299 let hover_point = cx.display_point(indoc! {"
1300 fn teˇst() { println!(); }
1301 "});
1302 let mut request = cx
1303 .lsp
1304 .set_request_handler::<lsp::request::HoverRequest, _, _>(
1305 |_, _| async move { Ok(None) },
1306 );
1307 cx.update_editor(|editor, window, cx| {
1308 let snapshot = editor.snapshot(window, cx);
1309 let anchor = snapshot
1310 .buffer_snapshot()
1311 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1312 hover_at(editor, Some(anchor), window, cx)
1313 });
1314 cx.background_executor
1315 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1316 request.next().await;
1317 cx.editor(|editor, _, _| {
1318 assert!(!editor.hover_state.visible());
1319 });
1320 }
1321
1322 #[gpui::test]
1323 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1324 init_test(cx, |_| {});
1325
1326 let mut cx = EditorLspTestContext::new_rust(
1327 lsp::ServerCapabilities {
1328 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1329 ..Default::default()
1330 },
1331 cx,
1332 )
1333 .await;
1334
1335 // Hover with keyboard has no delay
1336 cx.set_state(indoc! {"
1337 fˇn test() { println!(); }
1338 "});
1339 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1340 let symbol_range = cx.lsp_range(indoc! {"
1341 «fn» test() { println!(); }
1342 "});
1343
1344 cx.editor(|editor, _window, _cx| {
1345 assert!(!editor.hover_state.visible());
1346
1347 assert_eq!(
1348 editor.hover_state.info_popovers.len(),
1349 0,
1350 "Expected no hovers but got but got: {:?}",
1351 editor.hover_state.info_popovers.len()
1352 );
1353 });
1354
1355 let mut requests =
1356 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1357 Ok(Some(lsp::Hover {
1358 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1359 kind: lsp::MarkupKind::Markdown,
1360 value: "some other basic docs".to_string(),
1361 }),
1362 range: Some(symbol_range),
1363 }))
1364 });
1365
1366 requests.next().await;
1367 cx.dispatch_action(Hover);
1368
1369 cx.condition(|editor, _| editor.hover_state.visible()).await;
1370 cx.editor(|editor, _, cx| {
1371 assert_eq!(
1372 editor.hover_state.info_popovers.len(),
1373 1,
1374 "Expected exactly one hover but got: {:?}",
1375 editor.hover_state.info_popovers.len()
1376 );
1377
1378 let rendered_text = editor
1379 .hover_state
1380 .info_popovers
1381 .first()
1382 .unwrap()
1383 .get_rendered_text(cx);
1384
1385 assert_eq!(rendered_text, "some other basic docs".to_string())
1386 });
1387 }
1388
1389 #[gpui::test]
1390 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1391 init_test(cx, |_| {});
1392
1393 let mut cx = EditorLspTestContext::new_rust(
1394 lsp::ServerCapabilities {
1395 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1396 ..Default::default()
1397 },
1398 cx,
1399 )
1400 .await;
1401
1402 // Hover with keyboard has no delay
1403 cx.set_state(indoc! {"
1404 fˇn test() { println!(); }
1405 "});
1406 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1407 let symbol_range = cx.lsp_range(indoc! {"
1408 «fn» test() { println!(); }
1409 "});
1410 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1411 Ok(Some(lsp::Hover {
1412 contents: lsp::HoverContents::Array(vec![
1413 lsp::MarkedString::String("regular text for hover to show".to_string()),
1414 lsp::MarkedString::String("".to_string()),
1415 lsp::MarkedString::LanguageString(lsp::LanguageString {
1416 language: "Rust".to_string(),
1417 value: "".to_string(),
1418 }),
1419 ]),
1420 range: Some(symbol_range),
1421 }))
1422 })
1423 .next()
1424 .await;
1425 cx.dispatch_action(Hover);
1426
1427 cx.condition(|editor, _| editor.hover_state.visible()).await;
1428 cx.editor(|editor, _, cx| {
1429 assert_eq!(
1430 editor.hover_state.info_popovers.len(),
1431 1,
1432 "Expected exactly one hover but got: {:?}",
1433 editor.hover_state.info_popovers.len()
1434 );
1435 let rendered_text = editor
1436 .hover_state
1437 .info_popovers
1438 .first()
1439 .unwrap()
1440 .get_rendered_text(cx);
1441
1442 assert_eq!(
1443 rendered_text,
1444 "regular text for hover to show".to_string(),
1445 "No empty string hovers should be shown"
1446 );
1447 });
1448 }
1449
1450 #[gpui::test]
1451 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1452 init_test(cx, |_| {});
1453
1454 let mut cx = EditorLspTestContext::new_rust(
1455 lsp::ServerCapabilities {
1456 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1457 ..Default::default()
1458 },
1459 cx,
1460 )
1461 .await;
1462
1463 // Hover with keyboard has no delay
1464 cx.set_state(indoc! {"
1465 fˇn test() { println!(); }
1466 "});
1467 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1468 let symbol_range = cx.lsp_range(indoc! {"
1469 «fn» test() { println!(); }
1470 "});
1471
1472 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1473 let markdown_string = format!("\n```rust\n{code_str}```");
1474
1475 let closure_markdown_string = markdown_string.clone();
1476 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1477 let future_markdown_string = closure_markdown_string.clone();
1478 async move {
1479 Ok(Some(lsp::Hover {
1480 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1481 kind: lsp::MarkupKind::Markdown,
1482 value: future_markdown_string,
1483 }),
1484 range: Some(symbol_range),
1485 }))
1486 }
1487 })
1488 .next()
1489 .await;
1490
1491 cx.dispatch_action(Hover);
1492
1493 cx.condition(|editor, _| editor.hover_state.visible()).await;
1494 cx.editor(|editor, _, cx| {
1495 assert_eq!(
1496 editor.hover_state.info_popovers.len(),
1497 1,
1498 "Expected exactly one hover but got: {:?}",
1499 editor.hover_state.info_popovers.len()
1500 );
1501 let rendered_text = editor
1502 .hover_state
1503 .info_popovers
1504 .first()
1505 .unwrap()
1506 .get_rendered_text(cx);
1507
1508 assert_eq!(
1509 rendered_text, code_str,
1510 "Should not have extra line breaks at end of rendered hover"
1511 );
1512 });
1513 }
1514
1515 #[gpui::test]
1516 // https://github.com/zed-industries/zed/issues/15498
1517 async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1518 init_test(cx, |_| {});
1519
1520 let mut cx = EditorLspTestContext::new_rust(
1521 lsp::ServerCapabilities {
1522 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1523 ..Default::default()
1524 },
1525 cx,
1526 )
1527 .await;
1528
1529 cx.set_state(indoc! {"
1530 fn fuˇnc(abc def: i32) -> u32 {
1531 }
1532 "});
1533
1534 cx.lsp
1535 .set_request_handler::<lsp::request::HoverRequest, _, _>({
1536 |_, _| async move {
1537 Ok(Some(lsp::Hover {
1538 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1539 kind: lsp::MarkupKind::Markdown,
1540 value: indoc!(
1541 r#"
1542 ### function `errands_data_read`
1543
1544 ---
1545 → `char *`
1546 Function to read a file into a string
1547
1548 ---
1549 ```cpp
1550 static char *errands_data_read()
1551 ```
1552 "#
1553 )
1554 .to_string(),
1555 }),
1556 range: None,
1557 }))
1558 }
1559 });
1560 cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1561 cx.run_until_parked();
1562
1563 cx.update_editor(|editor, _, cx| {
1564 let popover = editor.hover_state.info_popovers.first().unwrap();
1565 let content = popover.get_rendered_text(cx);
1566
1567 assert!(content.contains("Function to read a file"));
1568 });
1569 }
1570
1571 #[gpui::test]
1572 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1573 init_test(cx, |settings| {
1574 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1575 show_value_hints: Some(true),
1576 enabled: Some(true),
1577 edit_debounce_ms: Some(0),
1578 scroll_debounce_ms: Some(0),
1579 show_type_hints: Some(true),
1580 show_parameter_hints: Some(true),
1581 show_other_hints: Some(true),
1582 show_background: Some(false),
1583 toggle_on_modifiers_press: None,
1584 })
1585 });
1586
1587 let mut cx = EditorLspTestContext::new_rust(
1588 lsp::ServerCapabilities {
1589 inlay_hint_provider: Some(lsp::OneOf::Right(
1590 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1591 resolve_provider: Some(true),
1592 ..Default::default()
1593 }),
1594 )),
1595 ..Default::default()
1596 },
1597 cx,
1598 )
1599 .await;
1600
1601 cx.set_state(indoc! {"
1602 struct TestStruct;
1603
1604 // ==================
1605
1606 struct TestNewType<T>(T);
1607
1608 fn main() {
1609 let variableˇ = TestNewType(TestStruct);
1610 }
1611 "});
1612
1613 let hint_start_offset = cx.ranges(indoc! {"
1614 struct TestStruct;
1615
1616 // ==================
1617
1618 struct TestNewType<T>(T);
1619
1620 fn main() {
1621 let variableˇ = TestNewType(TestStruct);
1622 }
1623 "})[0]
1624 .start;
1625 let hint_position = cx.to_lsp(hint_start_offset);
1626 let new_type_target_range = cx.lsp_range(indoc! {"
1627 struct TestStruct;
1628
1629 // ==================
1630
1631 struct «TestNewType»<T>(T);
1632
1633 fn main() {
1634 let variable = TestNewType(TestStruct);
1635 }
1636 "});
1637 let struct_target_range = cx.lsp_range(indoc! {"
1638 struct «TestStruct»;
1639
1640 // ==================
1641
1642 struct TestNewType<T>(T);
1643
1644 fn main() {
1645 let variable = TestNewType(TestStruct);
1646 }
1647 "});
1648
1649 let uri = cx.buffer_lsp_url.clone();
1650 let new_type_label = "TestNewType";
1651 let struct_label = "TestStruct";
1652 let entire_hint_label = ": TestNewType<TestStruct>";
1653 let closure_uri = uri.clone();
1654 cx.lsp
1655 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1656 let task_uri = closure_uri.clone();
1657 async move {
1658 assert_eq!(params.text_document.uri, task_uri);
1659 Ok(Some(vec![lsp::InlayHint {
1660 position: hint_position,
1661 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1662 value: entire_hint_label.to_string(),
1663 ..Default::default()
1664 }]),
1665 kind: Some(lsp::InlayHintKind::TYPE),
1666 text_edits: None,
1667 tooltip: None,
1668 padding_left: Some(false),
1669 padding_right: Some(false),
1670 data: None,
1671 }]))
1672 }
1673 })
1674 .next()
1675 .await;
1676 cx.background_executor.run_until_parked();
1677 cx.update_editor(|editor, _, cx| {
1678 let expected_layers = vec![entire_hint_label.to_string()];
1679 assert_eq!(expected_layers, cached_hint_labels(editor, cx));
1680 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1681 });
1682
1683 let inlay_range = cx
1684 .ranges(indoc! {"
1685 struct TestStruct;
1686
1687 // ==================
1688
1689 struct TestNewType<T>(T);
1690
1691 fn main() {
1692 let variable« »= TestNewType(TestStruct);
1693 }
1694 "})
1695 .first()
1696 .cloned()
1697 .unwrap();
1698 let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1699 let snapshot = editor.snapshot(window, cx);
1700 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1701 let next_valid = inlay_range.end.to_display_point(&snapshot);
1702 assert_eq!(previous_valid.row(), next_valid.row());
1703 assert!(previous_valid.column() < next_valid.column());
1704 let exact_unclipped = DisplayPoint::new(
1705 previous_valid.row(),
1706 previous_valid.column()
1707 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1708 as u32,
1709 );
1710 PointForPosition {
1711 previous_valid,
1712 next_valid,
1713 exact_unclipped,
1714 column_overshoot_after_line_end: 0,
1715 }
1716 });
1717 cx.update_editor(|editor, window, cx| {
1718 editor.update_inlay_link_and_hover_points(
1719 &editor.snapshot(window, cx),
1720 new_type_hint_part_hover_position,
1721 true,
1722 false,
1723 window,
1724 cx,
1725 );
1726 });
1727
1728 let resolve_closure_uri = uri.clone();
1729 cx.lsp
1730 .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
1731 move |mut hint_to_resolve, _| {
1732 let mut resolved_hint_positions = BTreeSet::new();
1733 let task_uri = resolve_closure_uri.clone();
1734 async move {
1735 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1736 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1737
1738 // `: TestNewType<TestStruct>`
1739 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1740 lsp::InlayHintLabelPart {
1741 value: ": ".to_string(),
1742 ..Default::default()
1743 },
1744 lsp::InlayHintLabelPart {
1745 value: new_type_label.to_string(),
1746 location: Some(lsp::Location {
1747 uri: task_uri.clone(),
1748 range: new_type_target_range,
1749 }),
1750 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1751 "A tooltip for `{new_type_label}`"
1752 ))),
1753 ..Default::default()
1754 },
1755 lsp::InlayHintLabelPart {
1756 value: "<".to_string(),
1757 ..Default::default()
1758 },
1759 lsp::InlayHintLabelPart {
1760 value: struct_label.to_string(),
1761 location: Some(lsp::Location {
1762 uri: task_uri,
1763 range: struct_target_range,
1764 }),
1765 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1766 lsp::MarkupContent {
1767 kind: lsp::MarkupKind::Markdown,
1768 value: format!("A tooltip for `{struct_label}`"),
1769 },
1770 )),
1771 ..Default::default()
1772 },
1773 lsp::InlayHintLabelPart {
1774 value: ">".to_string(),
1775 ..Default::default()
1776 },
1777 ]);
1778
1779 Ok(hint_to_resolve)
1780 }
1781 },
1782 )
1783 .next()
1784 .await;
1785 cx.background_executor.run_until_parked();
1786
1787 cx.update_editor(|editor, window, cx| {
1788 editor.update_inlay_link_and_hover_points(
1789 &editor.snapshot(window, cx),
1790 new_type_hint_part_hover_position,
1791 true,
1792 false,
1793 window,
1794 cx,
1795 );
1796 });
1797 cx.background_executor
1798 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1799 cx.background_executor.run_until_parked();
1800 cx.update_editor(|editor, _, cx| {
1801 let hover_state = &editor.hover_state;
1802 assert!(
1803 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1804 );
1805 let popover = hover_state.info_popovers.first().unwrap();
1806 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1807 assert_eq!(
1808 popover.symbol_range,
1809 RangeInEditor::Inlay(InlayHighlight {
1810 inlay: InlayId::Hint(0),
1811 inlay_position: buffer_snapshot.anchor_after(inlay_range.start),
1812 range: ": ".len()..": ".len() + new_type_label.len(),
1813 }),
1814 "Popover range should match the new type label part"
1815 );
1816 assert_eq!(
1817 popover.get_rendered_text(cx),
1818 format!("A tooltip for {new_type_label}"),
1819 );
1820 });
1821
1822 let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1823 let snapshot = editor.snapshot(window, cx);
1824 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1825 let next_valid = inlay_range.end.to_display_point(&snapshot);
1826 assert_eq!(previous_valid.row(), next_valid.row());
1827 assert!(previous_valid.column() < next_valid.column());
1828 let exact_unclipped = DisplayPoint::new(
1829 previous_valid.row(),
1830 previous_valid.column()
1831 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1832 as u32,
1833 );
1834 PointForPosition {
1835 previous_valid,
1836 next_valid,
1837 exact_unclipped,
1838 column_overshoot_after_line_end: 0,
1839 }
1840 });
1841 cx.update_editor(|editor, window, cx| {
1842 editor.update_inlay_link_and_hover_points(
1843 &editor.snapshot(window, cx),
1844 struct_hint_part_hover_position,
1845 true,
1846 false,
1847 window,
1848 cx,
1849 );
1850 });
1851 cx.background_executor
1852 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1853 cx.background_executor.run_until_parked();
1854 cx.update_editor(|editor, _, cx| {
1855 let hover_state = &editor.hover_state;
1856 assert!(
1857 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1858 );
1859 let popover = hover_state.info_popovers.first().unwrap();
1860 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1861 assert_eq!(
1862 popover.symbol_range,
1863 RangeInEditor::Inlay(InlayHighlight {
1864 inlay: InlayId::Hint(0),
1865 inlay_position: buffer_snapshot.anchor_after(inlay_range.start),
1866 range: ": ".len() + new_type_label.len() + "<".len()
1867 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1868 }),
1869 "Popover range should match the struct label part"
1870 );
1871 assert_eq!(
1872 popover.get_rendered_text(cx),
1873 format!("A tooltip for {struct_label}"),
1874 "Rendered markdown element should remove backticks from text"
1875 );
1876 });
1877 }
1878}