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