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