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