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::SubstitutedText(parsed) => rendered_text.push_str(parsed),
955 MarkdownEvent::Text | MarkdownEvent::Code => {
956 rendered_text.push_str(&text[range.clone()])
957 }
958 _ => {}
959 }
960 }
961 }
962 rendered_text
963 }
964 }
965
966 #[gpui::test]
967 async fn test_mouse_hover_info_popover_with_autocomplete_popover(
968 cx: &mut gpui::TestAppContext,
969 ) {
970 init_test(cx, |_| {});
971
972 let mut cx = EditorLspTestContext::new_rust(
973 lsp::ServerCapabilities {
974 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
975 completion_provider: Some(lsp::CompletionOptions {
976 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
977 resolve_provider: Some(true),
978 ..Default::default()
979 }),
980 ..Default::default()
981 },
982 cx,
983 )
984 .await;
985 let counter = Arc::new(AtomicUsize::new(0));
986 // Basic hover delays and then pops without moving the mouse
987 cx.set_state(indoc! {"
988 oneˇ
989 two
990 three
991 fn test() { println!(); }
992 "});
993
994 //prompt autocompletion menu
995 cx.simulate_keystroke(".");
996 handle_completion_request(
997 &mut cx,
998 indoc! {"
999 one.|<>
1000 two
1001 three
1002 "},
1003 vec!["first_completion", "second_completion"],
1004 counter.clone(),
1005 )
1006 .await;
1007 cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
1008 .await;
1009 assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
1010
1011 let hover_point = cx.display_point(indoc! {"
1012 one.
1013 two
1014 three
1015 fn test() { printˇln!(); }
1016 "});
1017 cx.update_editor(|editor, window, cx| {
1018 let snapshot = editor.snapshot(window, cx);
1019 let anchor = snapshot
1020 .buffer_snapshot
1021 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1022 hover_at(editor, Some(anchor), window, cx)
1023 });
1024 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1025
1026 // After delay, hover should be visible.
1027 let symbol_range = cx.lsp_range(indoc! {"
1028 one.
1029 two
1030 three
1031 fn test() { «println!»(); }
1032 "});
1033 let mut requests =
1034 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1035 Ok(Some(lsp::Hover {
1036 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1037 kind: lsp::MarkupKind::Markdown,
1038 value: "some basic docs".to_string(),
1039 }),
1040 range: Some(symbol_range),
1041 }))
1042 });
1043 cx.background_executor
1044 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1045 requests.next().await;
1046
1047 cx.editor(|editor, _window, cx| {
1048 assert!(editor.hover_state.visible());
1049 assert_eq!(
1050 editor.hover_state.info_popovers.len(),
1051 1,
1052 "Expected exactly one hover but got: {:?}",
1053 editor.hover_state.info_popovers
1054 );
1055 let rendered_text = editor
1056 .hover_state
1057 .info_popovers
1058 .first()
1059 .unwrap()
1060 .get_rendered_text(cx);
1061 assert_eq!(rendered_text, "some basic docs".to_string())
1062 });
1063
1064 // check that the completion menu is still visible and that there still has only been 1 completion request
1065 cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
1066 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1067
1068 //apply a completion and check it was successfully applied
1069 let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1070 editor.context_menu_next(&Default::default(), window, cx);
1071 editor
1072 .confirm_completion(&ConfirmCompletion::default(), window, cx)
1073 .unwrap()
1074 });
1075 cx.assert_editor_state(indoc! {"
1076 one.second_completionˇ
1077 two
1078 three
1079 fn test() { println!(); }
1080 "});
1081
1082 // check that the completion menu is no longer visible and that there still has only been 1 completion request
1083 cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1084 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1085
1086 //verify the information popover is still visible and unchanged
1087 cx.editor(|editor, _, cx| {
1088 assert!(editor.hover_state.visible());
1089 assert_eq!(
1090 editor.hover_state.info_popovers.len(),
1091 1,
1092 "Expected exactly one hover but got: {:?}",
1093 editor.hover_state.info_popovers
1094 );
1095 let rendered_text = editor
1096 .hover_state
1097 .info_popovers
1098 .first()
1099 .unwrap()
1100 .get_rendered_text(cx);
1101
1102 assert_eq!(rendered_text, "some basic docs".to_string())
1103 });
1104
1105 // Mouse moved with no hover response dismisses
1106 let hover_point = cx.display_point(indoc! {"
1107 one.second_completionˇ
1108 two
1109 three
1110 fn teˇst() { println!(); }
1111 "});
1112 let mut request = cx
1113 .lsp
1114 .set_request_handler::<lsp::request::HoverRequest, _, _>(
1115 |_, _| async move { Ok(None) },
1116 );
1117 cx.update_editor(|editor, window, cx| {
1118 let snapshot = editor.snapshot(window, cx);
1119 let anchor = snapshot
1120 .buffer_snapshot
1121 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1122 hover_at(editor, Some(anchor), window, cx)
1123 });
1124 cx.background_executor
1125 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1126 request.next().await;
1127
1128 // verify that the information popover is no longer visible
1129 cx.editor(|editor, _, _| {
1130 assert!(!editor.hover_state.visible());
1131 });
1132 }
1133
1134 #[gpui::test]
1135 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1136 init_test(cx, |_| {});
1137
1138 let mut cx = EditorLspTestContext::new_rust(
1139 lsp::ServerCapabilities {
1140 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1141 ..Default::default()
1142 },
1143 cx,
1144 )
1145 .await;
1146
1147 // Basic hover delays and then pops without moving the mouse
1148 cx.set_state(indoc! {"
1149 fn ˇtest() { println!(); }
1150 "});
1151 let hover_point = cx.display_point(indoc! {"
1152 fn test() { printˇln!(); }
1153 "});
1154
1155 cx.update_editor(|editor, window, cx| {
1156 let snapshot = editor.snapshot(window, cx);
1157 let anchor = snapshot
1158 .buffer_snapshot
1159 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1160 hover_at(editor, Some(anchor), window, cx)
1161 });
1162 assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1163
1164 // After delay, hover should be visible.
1165 let symbol_range = cx.lsp_range(indoc! {"
1166 fn test() { «println!»(); }
1167 "});
1168 let mut requests =
1169 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1170 Ok(Some(lsp::Hover {
1171 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1172 kind: lsp::MarkupKind::Markdown,
1173 value: "some basic docs".to_string(),
1174 }),
1175 range: Some(symbol_range),
1176 }))
1177 });
1178 cx.background_executor
1179 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1180 requests.next().await;
1181
1182 cx.editor(|editor, _, cx| {
1183 assert!(editor.hover_state.visible());
1184 assert_eq!(
1185 editor.hover_state.info_popovers.len(),
1186 1,
1187 "Expected exactly one hover but got: {:?}",
1188 editor.hover_state.info_popovers
1189 );
1190 let rendered_text = editor
1191 .hover_state
1192 .info_popovers
1193 .first()
1194 .unwrap()
1195 .get_rendered_text(cx);
1196
1197 assert_eq!(rendered_text, "some basic docs".to_string())
1198 });
1199
1200 // Mouse moved with no hover response dismisses
1201 let hover_point = cx.display_point(indoc! {"
1202 fn teˇst() { println!(); }
1203 "});
1204 let mut request = cx
1205 .lsp
1206 .set_request_handler::<lsp::request::HoverRequest, _, _>(
1207 |_, _| async move { Ok(None) },
1208 );
1209 cx.update_editor(|editor, window, cx| {
1210 let snapshot = editor.snapshot(window, cx);
1211 let anchor = snapshot
1212 .buffer_snapshot
1213 .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1214 hover_at(editor, Some(anchor), window, cx)
1215 });
1216 cx.background_executor
1217 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1218 request.next().await;
1219 cx.editor(|editor, _, _| {
1220 assert!(!editor.hover_state.visible());
1221 });
1222 }
1223
1224 #[gpui::test]
1225 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1226 init_test(cx, |_| {});
1227
1228 let mut cx = EditorLspTestContext::new_rust(
1229 lsp::ServerCapabilities {
1230 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1231 ..Default::default()
1232 },
1233 cx,
1234 )
1235 .await;
1236
1237 // Hover with keyboard has no delay
1238 cx.set_state(indoc! {"
1239 fˇn test() { println!(); }
1240 "});
1241 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1242 let symbol_range = cx.lsp_range(indoc! {"
1243 «fn» test() { println!(); }
1244 "});
1245
1246 cx.editor(|editor, _window, _cx| {
1247 assert!(!editor.hover_state.visible());
1248
1249 assert_eq!(
1250 editor.hover_state.info_popovers.len(),
1251 0,
1252 "Expected no hovers but got but got: {:?}",
1253 editor.hover_state.info_popovers
1254 );
1255 });
1256
1257 let mut requests =
1258 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1259 Ok(Some(lsp::Hover {
1260 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1261 kind: lsp::MarkupKind::Markdown,
1262 value: "some other basic docs".to_string(),
1263 }),
1264 range: Some(symbol_range),
1265 }))
1266 });
1267
1268 requests.next().await;
1269 cx.dispatch_action(Hover);
1270
1271 cx.condition(|editor, _| editor.hover_state.visible()).await;
1272 cx.editor(|editor, _, cx| {
1273 assert_eq!(
1274 editor.hover_state.info_popovers.len(),
1275 1,
1276 "Expected exactly one hover but got: {:?}",
1277 editor.hover_state.info_popovers
1278 );
1279
1280 let rendered_text = editor
1281 .hover_state
1282 .info_popovers
1283 .first()
1284 .unwrap()
1285 .get_rendered_text(cx);
1286
1287 assert_eq!(rendered_text, "some other basic docs".to_string())
1288 });
1289 }
1290
1291 #[gpui::test]
1292 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1293 init_test(cx, |_| {});
1294
1295 let mut cx = EditorLspTestContext::new_rust(
1296 lsp::ServerCapabilities {
1297 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1298 ..Default::default()
1299 },
1300 cx,
1301 )
1302 .await;
1303
1304 // Hover with keyboard has no delay
1305 cx.set_state(indoc! {"
1306 fˇn test() { println!(); }
1307 "});
1308 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1309 let symbol_range = cx.lsp_range(indoc! {"
1310 «fn» test() { println!(); }
1311 "});
1312 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1313 Ok(Some(lsp::Hover {
1314 contents: lsp::HoverContents::Array(vec![
1315 lsp::MarkedString::String("regular text for hover to show".to_string()),
1316 lsp::MarkedString::String("".to_string()),
1317 lsp::MarkedString::LanguageString(lsp::LanguageString {
1318 language: "Rust".to_string(),
1319 value: "".to_string(),
1320 }),
1321 ]),
1322 range: Some(symbol_range),
1323 }))
1324 })
1325 .next()
1326 .await;
1327 cx.dispatch_action(Hover);
1328
1329 cx.condition(|editor, _| editor.hover_state.visible()).await;
1330 cx.editor(|editor, _, cx| {
1331 assert_eq!(
1332 editor.hover_state.info_popovers.len(),
1333 1,
1334 "Expected exactly one hover but got: {:?}",
1335 editor.hover_state.info_popovers
1336 );
1337 let rendered_text = editor
1338 .hover_state
1339 .info_popovers
1340 .first()
1341 .unwrap()
1342 .get_rendered_text(cx);
1343
1344 assert_eq!(
1345 rendered_text,
1346 "regular text for hover to show".to_string(),
1347 "No empty string hovers should be shown"
1348 );
1349 });
1350 }
1351
1352 #[gpui::test]
1353 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1354 init_test(cx, |_| {});
1355
1356 let mut cx = EditorLspTestContext::new_rust(
1357 lsp::ServerCapabilities {
1358 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1359 ..Default::default()
1360 },
1361 cx,
1362 )
1363 .await;
1364
1365 // Hover with keyboard has no delay
1366 cx.set_state(indoc! {"
1367 fˇn test() { println!(); }
1368 "});
1369 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1370 let symbol_range = cx.lsp_range(indoc! {"
1371 «fn» test() { println!(); }
1372 "});
1373
1374 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1375 let markdown_string = format!("\n```rust\n{code_str}```");
1376
1377 let closure_markdown_string = markdown_string.clone();
1378 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1379 let future_markdown_string = closure_markdown_string.clone();
1380 async move {
1381 Ok(Some(lsp::Hover {
1382 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1383 kind: lsp::MarkupKind::Markdown,
1384 value: future_markdown_string,
1385 }),
1386 range: Some(symbol_range),
1387 }))
1388 }
1389 })
1390 .next()
1391 .await;
1392
1393 cx.dispatch_action(Hover);
1394
1395 cx.condition(|editor, _| editor.hover_state.visible()).await;
1396 cx.editor(|editor, _, cx| {
1397 assert_eq!(
1398 editor.hover_state.info_popovers.len(),
1399 1,
1400 "Expected exactly one hover but got: {:?}",
1401 editor.hover_state.info_popovers
1402 );
1403 let rendered_text = editor
1404 .hover_state
1405 .info_popovers
1406 .first()
1407 .unwrap()
1408 .get_rendered_text(cx);
1409
1410 assert_eq!(
1411 rendered_text, code_str,
1412 "Should not have extra line breaks at end of rendered hover"
1413 );
1414 });
1415 }
1416
1417 #[gpui::test]
1418 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1419 init_test(cx, |_| {});
1420
1421 let mut cx = EditorLspTestContext::new_rust(
1422 lsp::ServerCapabilities {
1423 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1424 ..Default::default()
1425 },
1426 cx,
1427 )
1428 .await;
1429
1430 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1431 // info popover once request completes
1432 cx.set_state(indoc! {"
1433 fn teˇst() { println!(); }
1434 "});
1435
1436 // Send diagnostic to client
1437 let range = cx.text_anchor_range(indoc! {"
1438 fn «test»() { println!(); }
1439 "});
1440 cx.update_buffer(|buffer, cx| {
1441 let snapshot = buffer.text_snapshot();
1442 let set = DiagnosticSet::from_sorted_entries(
1443 vec![DiagnosticEntry {
1444 range,
1445 diagnostic: Diagnostic {
1446 message: "A test diagnostic message.".to_string(),
1447 ..Default::default()
1448 },
1449 }],
1450 &snapshot,
1451 );
1452 buffer.update_diagnostics(LanguageServerId(0), set, cx);
1453 });
1454
1455 // Hover pops diagnostic immediately
1456 cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1457 cx.background_executor.run_until_parked();
1458
1459 cx.editor(|Editor { hover_state, .. }, _, _| {
1460 assert!(
1461 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
1462 )
1463 });
1464
1465 // Info Popover shows after request responded to
1466 let range = cx.lsp_range(indoc! {"
1467 fn «test»() { println!(); }
1468 "});
1469 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1470 Ok(Some(lsp::Hover {
1471 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1472 kind: lsp::MarkupKind::Markdown,
1473 value: "some new docs".to_string(),
1474 }),
1475 range: Some(range),
1476 }))
1477 });
1478 cx.background_executor
1479 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1480
1481 cx.background_executor.run_until_parked();
1482 cx.editor(|Editor { hover_state, .. }, _, _| {
1483 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1484 });
1485 }
1486
1487 #[gpui::test]
1488 // https://github.com/zed-industries/zed/issues/15498
1489 async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1490 init_test(cx, |_| {});
1491
1492 let mut cx = EditorLspTestContext::new_rust(
1493 lsp::ServerCapabilities {
1494 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1495 ..Default::default()
1496 },
1497 cx,
1498 )
1499 .await;
1500
1501 cx.set_state(indoc! {"
1502 fn fuˇnc(abc def: i32) -> u32 {
1503 }
1504 "});
1505
1506 cx.lsp
1507 .set_request_handler::<lsp::request::HoverRequest, _, _>({
1508 |_, _| async move {
1509 Ok(Some(lsp::Hover {
1510 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1511 kind: lsp::MarkupKind::Markdown,
1512 value: indoc!(
1513 r#"
1514 ### function `errands_data_read`
1515
1516 ---
1517 → `char *`
1518 Function to read a file into a string
1519
1520 ---
1521 ```cpp
1522 static char *errands_data_read()
1523 ```
1524 "#
1525 )
1526 .to_string(),
1527 }),
1528 range: None,
1529 }))
1530 }
1531 });
1532 cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1533 cx.run_until_parked();
1534
1535 cx.update_editor(|editor, _, cx| {
1536 let popover = editor.hover_state.info_popovers.first().unwrap();
1537 let content = popover.get_rendered_text(cx);
1538
1539 assert!(content.contains("Function to read a file"));
1540 });
1541 }
1542
1543 #[gpui::test]
1544 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1545 init_test(cx, |settings| {
1546 settings.defaults.inlay_hints = Some(InlayHintSettings {
1547 enabled: true,
1548 edit_debounce_ms: 0,
1549 scroll_debounce_ms: 0,
1550 show_type_hints: true,
1551 show_parameter_hints: true,
1552 show_other_hints: true,
1553 show_background: false,
1554 toggle_on_modifiers_press: None,
1555 })
1556 });
1557
1558 let mut cx = EditorLspTestContext::new_rust(
1559 lsp::ServerCapabilities {
1560 inlay_hint_provider: Some(lsp::OneOf::Right(
1561 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1562 resolve_provider: Some(true),
1563 ..Default::default()
1564 }),
1565 )),
1566 ..Default::default()
1567 },
1568 cx,
1569 )
1570 .await;
1571
1572 cx.set_state(indoc! {"
1573 struct TestStruct;
1574
1575 // ==================
1576
1577 struct TestNewType<T>(T);
1578
1579 fn main() {
1580 let variableˇ = TestNewType(TestStruct);
1581 }
1582 "});
1583
1584 let hint_start_offset = cx.ranges(indoc! {"
1585 struct TestStruct;
1586
1587 // ==================
1588
1589 struct TestNewType<T>(T);
1590
1591 fn main() {
1592 let variableˇ = TestNewType(TestStruct);
1593 }
1594 "})[0]
1595 .start;
1596 let hint_position = cx.to_lsp(hint_start_offset);
1597 let new_type_target_range = cx.lsp_range(indoc! {"
1598 struct TestStruct;
1599
1600 // ==================
1601
1602 struct «TestNewType»<T>(T);
1603
1604 fn main() {
1605 let variable = TestNewType(TestStruct);
1606 }
1607 "});
1608 let struct_target_range = cx.lsp_range(indoc! {"
1609 struct «TestStruct»;
1610
1611 // ==================
1612
1613 struct TestNewType<T>(T);
1614
1615 fn main() {
1616 let variable = TestNewType(TestStruct);
1617 }
1618 "});
1619
1620 let uri = cx.buffer_lsp_url.clone();
1621 let new_type_label = "TestNewType";
1622 let struct_label = "TestStruct";
1623 let entire_hint_label = ": TestNewType<TestStruct>";
1624 let closure_uri = uri.clone();
1625 cx.lsp
1626 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1627 let task_uri = closure_uri.clone();
1628 async move {
1629 assert_eq!(params.text_document.uri, task_uri);
1630 Ok(Some(vec![lsp::InlayHint {
1631 position: hint_position,
1632 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1633 value: entire_hint_label.to_string(),
1634 ..Default::default()
1635 }]),
1636 kind: Some(lsp::InlayHintKind::TYPE),
1637 text_edits: None,
1638 tooltip: None,
1639 padding_left: Some(false),
1640 padding_right: Some(false),
1641 data: None,
1642 }]))
1643 }
1644 })
1645 .next()
1646 .await;
1647 cx.background_executor.run_until_parked();
1648 cx.update_editor(|editor, _, cx| {
1649 let expected_layers = vec![entire_hint_label.to_string()];
1650 assert_eq!(expected_layers, cached_hint_labels(editor));
1651 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1652 });
1653
1654 let inlay_range = cx
1655 .ranges(indoc! {"
1656 struct TestStruct;
1657
1658 // ==================
1659
1660 struct TestNewType<T>(T);
1661
1662 fn main() {
1663 let variable« »= TestNewType(TestStruct);
1664 }
1665 "})
1666 .first()
1667 .cloned()
1668 .unwrap();
1669 let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1670 let snapshot = editor.snapshot(window, cx);
1671 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1672 let next_valid = inlay_range.end.to_display_point(&snapshot);
1673 assert_eq!(previous_valid.row(), next_valid.row());
1674 assert!(previous_valid.column() < next_valid.column());
1675 let exact_unclipped = DisplayPoint::new(
1676 previous_valid.row(),
1677 previous_valid.column()
1678 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1679 as u32,
1680 );
1681 PointForPosition {
1682 previous_valid,
1683 next_valid,
1684 exact_unclipped,
1685 column_overshoot_after_line_end: 0,
1686 }
1687 });
1688 cx.update_editor(|editor, window, cx| {
1689 update_inlay_link_and_hover_points(
1690 &editor.snapshot(window, cx),
1691 new_type_hint_part_hover_position,
1692 editor,
1693 true,
1694 false,
1695 window,
1696 cx,
1697 );
1698 });
1699
1700 let resolve_closure_uri = uri.clone();
1701 cx.lsp
1702 .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
1703 move |mut hint_to_resolve, _| {
1704 let mut resolved_hint_positions = BTreeSet::new();
1705 let task_uri = resolve_closure_uri.clone();
1706 async move {
1707 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1708 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1709
1710 // `: TestNewType<TestStruct>`
1711 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1712 lsp::InlayHintLabelPart {
1713 value: ": ".to_string(),
1714 ..Default::default()
1715 },
1716 lsp::InlayHintLabelPart {
1717 value: new_type_label.to_string(),
1718 location: Some(lsp::Location {
1719 uri: task_uri.clone(),
1720 range: new_type_target_range,
1721 }),
1722 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1723 "A tooltip for `{new_type_label}`"
1724 ))),
1725 ..Default::default()
1726 },
1727 lsp::InlayHintLabelPart {
1728 value: "<".to_string(),
1729 ..Default::default()
1730 },
1731 lsp::InlayHintLabelPart {
1732 value: struct_label.to_string(),
1733 location: Some(lsp::Location {
1734 uri: task_uri,
1735 range: struct_target_range,
1736 }),
1737 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1738 lsp::MarkupContent {
1739 kind: lsp::MarkupKind::Markdown,
1740 value: format!("A tooltip for `{struct_label}`"),
1741 },
1742 )),
1743 ..Default::default()
1744 },
1745 lsp::InlayHintLabelPart {
1746 value: ">".to_string(),
1747 ..Default::default()
1748 },
1749 ]);
1750
1751 Ok(hint_to_resolve)
1752 }
1753 },
1754 )
1755 .next()
1756 .await;
1757 cx.background_executor.run_until_parked();
1758
1759 cx.update_editor(|editor, window, cx| {
1760 update_inlay_link_and_hover_points(
1761 &editor.snapshot(window, cx),
1762 new_type_hint_part_hover_position,
1763 editor,
1764 true,
1765 false,
1766 window,
1767 cx,
1768 );
1769 });
1770 cx.background_executor
1771 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1772 cx.background_executor.run_until_parked();
1773 cx.update_editor(|editor, _, cx| {
1774 let hover_state = &editor.hover_state;
1775 assert!(
1776 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1777 );
1778 let popover = hover_state.info_popovers.first().cloned().unwrap();
1779 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1780 assert_eq!(
1781 popover.symbol_range,
1782 RangeInEditor::Inlay(InlayHighlight {
1783 inlay: InlayId::Hint(0),
1784 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1785 range: ": ".len()..": ".len() + new_type_label.len(),
1786 }),
1787 "Popover range should match the new type label part"
1788 );
1789 assert_eq!(
1790 popover.get_rendered_text(cx),
1791 format!("A tooltip for {new_type_label}"),
1792 );
1793 });
1794
1795 let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
1796 let snapshot = editor.snapshot(window, cx);
1797 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1798 let next_valid = inlay_range.end.to_display_point(&snapshot);
1799 assert_eq!(previous_valid.row(), next_valid.row());
1800 assert!(previous_valid.column() < next_valid.column());
1801 let exact_unclipped = DisplayPoint::new(
1802 previous_valid.row(),
1803 previous_valid.column()
1804 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1805 as u32,
1806 );
1807 PointForPosition {
1808 previous_valid,
1809 next_valid,
1810 exact_unclipped,
1811 column_overshoot_after_line_end: 0,
1812 }
1813 });
1814 cx.update_editor(|editor, window, cx| {
1815 update_inlay_link_and_hover_points(
1816 &editor.snapshot(window, cx),
1817 struct_hint_part_hover_position,
1818 editor,
1819 true,
1820 false,
1821 window,
1822 cx,
1823 );
1824 });
1825 cx.background_executor
1826 .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1827 cx.background_executor.run_until_parked();
1828 cx.update_editor(|editor, _, cx| {
1829 let hover_state = &editor.hover_state;
1830 assert!(
1831 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1832 );
1833 let popover = hover_state.info_popovers.first().cloned().unwrap();
1834 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1835 assert_eq!(
1836 popover.symbol_range,
1837 RangeInEditor::Inlay(InlayHighlight {
1838 inlay: InlayId::Hint(0),
1839 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1840 range: ": ".len() + new_type_label.len() + "<".len()
1841 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1842 }),
1843 "Popover range should match the struct label part"
1844 );
1845 assert_eq!(
1846 popover.get_rendered_text(cx),
1847 format!("A tooltip for {struct_label}"),
1848 "Rendered markdown element should remove backticks from text"
1849 );
1850 });
1851 }
1852}