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