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