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