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