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