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