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