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