1use crate::{
2 display_map::{InlayOffset, ToDisplayPoint},
3 hover_links::{InlayHighlight, RangeInEditor},
4 Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
5 ExcerptId, Hover, RangeToAnchorExt,
6};
7use futures::{stream::FuturesUnordered, FutureExt};
8use gpui::{
9 div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
10 ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
11 ViewContext, WeakView,
12};
13use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
14
15use lsp::DiagnosticSeverity;
16use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
17use settings::Settings;
18use smol::stream::StreamExt;
19use std::{ops::Range, sync::Arc, time::Duration};
20use ui::{prelude::*, Tooltip};
21use util::TryFutureExt;
22use workspace::Workspace;
23
24pub const HOVER_DELAY_MILLIS: u64 = 350;
25pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
26
27pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
28pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
29pub const HOVER_POPOVER_GAP: Pixels = px(10.);
30
31/// Bindable action which uses the most recent selection head to trigger a hover
32pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
33 let head = editor.selections.newest_display(cx).head();
34 show_hover(editor, head, true, cx);
35}
36
37/// The internal hover action dispatches between `show_hover` or `hide_hover`
38/// depending on whether a point to hover over is provided.
39pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
40 if EditorSettings::get_global(cx).hover_popover_enabled {
41 if let Some(point) = point {
42 show_hover(editor, point, false, cx);
43 } else {
44 hide_hover(editor, cx);
45 }
46 }
47}
48
49pub struct InlayHover {
50 pub excerpt: ExcerptId,
51 pub range: InlayHighlight,
52 pub tooltip: HoverBlock,
53}
54
55pub fn find_hovered_hint_part(
56 label_parts: Vec<InlayHintLabelPart>,
57 hint_start: InlayOffset,
58 hovered_offset: InlayOffset,
59) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
60 if hovered_offset >= hint_start {
61 let mut hovered_character = (hovered_offset - hint_start).0;
62 let mut part_start = hint_start;
63 for part in label_parts {
64 let part_len = part.value.chars().count();
65 if hovered_character > part_len {
66 hovered_character -= part_len;
67 part_start.0 += part_len;
68 } else {
69 let part_end = InlayOffset(part_start.0 + part_len);
70 return Some((part, part_start..part_end));
71 }
72 }
73 }
74 None
75}
76
77pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
78 if EditorSettings::get_global(cx).hover_popover_enabled {
79 if editor.pending_rename.is_some() {
80 return;
81 }
82
83 let Some(project) = editor.project.clone() else {
84 return;
85 };
86
87 if editor
88 .hover_state
89 .info_popovers
90 .iter()
91 .any(|InfoPopover { symbol_range, .. }| {
92 if let RangeInEditor::Inlay(range) = symbol_range {
93 if range == &inlay_hover.range {
94 // Hover triggered from same location as last time. Don't show again.
95 return true;
96 }
97 }
98 false
99 })
100 {
101 hide_hover(editor, cx);
102 }
103
104 let task = cx.spawn(|this, mut cx| {
105 async move {
106 cx.background_executor()
107 .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
108 .await;
109 this.update(&mut cx, |this, _| {
110 this.hover_state.diagnostic_popover = None;
111 })?;
112
113 let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
114 let blocks = vec![inlay_hover.tooltip];
115 let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
116
117 let hover_popover = InfoPopover {
118 symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
119 parsed_content,
120 };
121
122 this.update(&mut cx, |this, cx| {
123 // TODO: no background highlights happen for inlays currently
124 this.hover_state.info_popovers = vec![hover_popover];
125 cx.notify();
126 })?;
127
128 anyhow::Ok(())
129 }
130 .log_err()
131 });
132
133 editor.hover_state.info_task = Some(task);
134 }
135}
136
137/// Hides the type information popup.
138/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
139/// selections changed.
140pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
141 let info_popovers = editor.hover_state.info_popovers.drain(..);
142 let diagnostics_popover = editor.hover_state.diagnostic_popover.take();
143 let did_hide = info_popovers.count() > 0 || diagnostics_popover.is_some();
144
145 editor.hover_state.info_task = None;
146 editor.hover_state.triggered_from = None;
147
148 editor.clear_background_highlights::<HoverState>(cx);
149
150 if did_hide {
151 cx.notify();
152 }
153
154 did_hide
155}
156
157/// Queries the LSP and shows type info and documentation
158/// about the symbol the mouse is currently hovering over.
159/// Triggered by the `Hover` action when the cursor may be over a symbol.
160fn show_hover(
161 editor: &mut Editor,
162 point: DisplayPoint,
163 ignore_timeout: bool,
164 cx: &mut ViewContext<Editor>,
165) {
166 if editor.pending_rename.is_some() {
167 return;
168 }
169
170 let snapshot = editor.snapshot(cx);
171 let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
172
173 let (buffer, buffer_position) = if let Some(output) = editor
174 .buffer
175 .read(cx)
176 .text_anchor_for_position(multibuffer_offset, cx)
177 {
178 output
179 } else {
180 return;
181 };
182
183 let excerpt_id = if let Some((excerpt_id, _, _)) = editor
184 .buffer()
185 .read(cx)
186 .excerpt_containing(multibuffer_offset, cx)
187 {
188 excerpt_id
189 } else {
190 return;
191 };
192
193 let project = if let Some(project) = editor.project.clone() {
194 project
195 } else {
196 return;
197 };
198
199 if !ignore_timeout {
200 if editor
201 .hover_state
202 .info_popovers
203 .iter()
204 .any(|InfoPopover { symbol_range, .. }| {
205 symbol_range
206 .as_text_range()
207 .map(|range| {
208 let hover_range = range.to_offset(&snapshot.buffer_snapshot);
209 // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
210 // use an inclusive range here to check if we should dismiss the popover
211 (hover_range.start..=hover_range.end).contains(&multibuffer_offset)
212 })
213 .unwrap_or(false)
214 })
215 {
216 // Hover triggered from same location as last time. Don't show again.
217 return;
218 } else {
219 hide_hover(editor, cx);
220 }
221 }
222
223 // Get input anchor
224 let anchor = snapshot
225 .buffer_snapshot
226 .anchor_at(multibuffer_offset, Bias::Left);
227
228 // Don't request again if the location is the same as the previous request
229 if let Some(triggered_from) = &editor.hover_state.triggered_from {
230 if triggered_from
231 .cmp(&anchor, &snapshot.buffer_snapshot)
232 .is_eq()
233 {
234 return;
235 }
236 }
237
238 let task = cx.spawn(|this, mut cx| {
239 async move {
240 // If we need to delay, delay a set amount initially before making the lsp request
241 let delay = if !ignore_timeout {
242 // Construct delay task to wait for later
243 let total_delay = Some(
244 cx.background_executor()
245 .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
246 );
247
248 cx.background_executor()
249 .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
250 .await;
251 total_delay
252 } else {
253 None
254 };
255
256 // query the LSP for hover info
257 let hover_request = cx.update(|cx| {
258 project.update(cx, |project, cx| {
259 project.hover(&buffer, buffer_position, cx)
260 })
261 })?;
262
263 if let Some(delay) = delay {
264 delay.await;
265 }
266
267 // If there's a diagnostic, assign it on the hover state and notify
268 let local_diagnostic = snapshot
269 .buffer_snapshot
270 .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
271 // Find the entry with the most specific range
272 .min_by_key(|entry| entry.range.end - entry.range.start)
273 .map(|entry| DiagnosticEntry {
274 diagnostic: entry.diagnostic,
275 range: entry.range.to_anchors(&snapshot.buffer_snapshot),
276 });
277
278 // Pull the primary diagnostic out so we can jump to it if the popover is clicked
279 let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
280 snapshot
281 .buffer_snapshot
282 .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
283 .find(|diagnostic| diagnostic.diagnostic.is_primary)
284 .map(|entry| DiagnosticEntry {
285 diagnostic: entry.diagnostic,
286 range: entry.range.to_anchors(&snapshot.buffer_snapshot),
287 })
288 });
289
290 this.update(&mut cx, |this, _| {
291 this.hover_state.diagnostic_popover =
292 local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
293 local_diagnostic,
294 primary_diagnostic,
295 });
296 })?;
297
298 let hovers_response = hover_request.await.ok().unwrap_or_default();
299 let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
300 let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
301 let mut hover_highlights = Vec::with_capacity(hovers_response.len());
302 let mut info_popovers = Vec::with_capacity(hovers_response.len());
303 let mut info_popover_tasks = hovers_response
304 .into_iter()
305 .map(|hover_result| async {
306 // Create symbol range of anchors for highlighting and filtering of future requests.
307 let range = hover_result
308 .range
309 .and_then(|range| {
310 let start = snapshot
311 .buffer_snapshot
312 .anchor_in_excerpt(excerpt_id, range.start)?;
313 let end = snapshot
314 .buffer_snapshot
315 .anchor_in_excerpt(excerpt_id, range.end)?;
316
317 Some(start..end)
318 })
319 .unwrap_or_else(|| anchor..anchor);
320
321 let blocks = hover_result.contents;
322 let language = hover_result.language;
323 let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
324
325 (
326 range.clone(),
327 InfoPopover {
328 symbol_range: RangeInEditor::Text(range),
329 parsed_content,
330 },
331 )
332 })
333 .collect::<FuturesUnordered<_>>();
334 while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
335 hover_highlights.push(highlight_range);
336 info_popovers.push(info_popover);
337 }
338
339 this.update(&mut cx, |editor, cx| {
340 if hover_highlights.is_empty() {
341 editor.clear_background_highlights::<HoverState>(cx);
342 } else {
343 // Highlight the selected symbol using a background highlight
344 editor.highlight_background::<HoverState>(
345 hover_highlights,
346 |theme| theme.element_hover, // todo update theme
347 cx,
348 );
349 }
350
351 editor.hover_state.info_popovers = info_popovers;
352 cx.notify();
353 cx.refresh();
354 })?;
355
356 anyhow::Ok(())
357 }
358 .log_err()
359 });
360
361 editor.hover_state.info_task = Some(task);
362}
363
364async fn parse_blocks(
365 blocks: &[HoverBlock],
366 language_registry: &Arc<LanguageRegistry>,
367 language: Option<Arc<Language>>,
368) -> markdown::ParsedMarkdown {
369 let mut text = String::new();
370 let mut highlights = Vec::new();
371 let mut region_ranges = Vec::new();
372 let mut regions = Vec::new();
373
374 for block in blocks {
375 match &block.kind {
376 HoverBlockKind::PlainText => {
377 markdown::new_paragraph(&mut text, &mut Vec::new());
378 text.push_str(&block.text);
379 }
380
381 HoverBlockKind::Markdown => {
382 markdown::parse_markdown_block(
383 &block.text,
384 language_registry,
385 language.clone(),
386 &mut text,
387 &mut highlights,
388 &mut region_ranges,
389 &mut regions,
390 )
391 .await
392 }
393
394 HoverBlockKind::Code { language } => {
395 if let Some(language) = language_registry
396 .language_for_name(language)
397 .now_or_never()
398 .and_then(Result::ok)
399 {
400 markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
401 } else {
402 text.push_str(&block.text);
403 }
404 }
405 }
406 }
407
408 let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
409 if leading_space > 0 {
410 highlights = highlights
411 .into_iter()
412 .map(|(range, style)| {
413 (
414 range.start.saturating_sub(leading_space)
415 ..range.end.saturating_sub(leading_space),
416 style,
417 )
418 })
419 .collect();
420 region_ranges = region_ranges
421 .into_iter()
422 .map(|range| {
423 range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
424 })
425 .collect();
426 }
427
428 ParsedMarkdown {
429 text: text.trim().to_string(),
430 highlights,
431 region_ranges,
432 regions,
433 }
434}
435
436#[derive(Default)]
437pub struct HoverState {
438 pub info_popovers: Vec<InfoPopover>,
439 pub diagnostic_popover: Option<DiagnosticPopover>,
440 pub triggered_from: Option<Anchor>,
441 pub info_task: Option<Task<Option<()>>>,
442}
443
444impl HoverState {
445 pub fn visible(&self) -> bool {
446 !self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
447 }
448
449 pub fn render(
450 &mut self,
451 snapshot: &EditorSnapshot,
452 style: &EditorStyle,
453 visible_rows: Range<u32>,
454 max_size: Size<Pixels>,
455 workspace: Option<WeakView<Workspace>>,
456 cx: &mut ViewContext<Editor>,
457 ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
458 // If there is a diagnostic, position the popovers based on that.
459 // Otherwise use the start of the hover range
460 let anchor = self
461 .diagnostic_popover
462 .as_ref()
463 .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
464 .or_else(|| {
465 self.info_popovers.iter().find_map(|info_popover| {
466 match &info_popover.symbol_range {
467 RangeInEditor::Text(range) => Some(&range.start),
468 RangeInEditor::Inlay(_) => None,
469 }
470 })
471 })
472 .or_else(|| {
473 self.info_popovers.iter().find_map(|info_popover| {
474 match &info_popover.symbol_range {
475 RangeInEditor::Text(_) => None,
476 RangeInEditor::Inlay(range) => Some(&range.inlay_position),
477 }
478 })
479 })?;
480 let point = anchor.to_display_point(&snapshot.display_snapshot);
481
482 // Don't render if the relevant point isn't on screen
483 if !self.visible() || !visible_rows.contains(&point.row()) {
484 return None;
485 }
486
487 let mut elements = Vec::new();
488
489 if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
490 elements.push(diagnostic_popover.render(style, max_size, cx));
491 }
492 for info_popover in &mut self.info_popovers {
493 elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
494 }
495
496 Some((point, elements))
497 }
498}
499
500#[derive(Debug, Clone)]
501pub struct InfoPopover {
502 symbol_range: RangeInEditor,
503 parsed_content: ParsedMarkdown,
504}
505
506impl InfoPopover {
507 pub fn render(
508 &mut self,
509 style: &EditorStyle,
510 max_size: Size<Pixels>,
511 workspace: Option<WeakView<Workspace>>,
512 cx: &mut ViewContext<Editor>,
513 ) -> AnyElement {
514 div()
515 .id("info_popover")
516 .elevation_2(cx)
517 .p_2()
518 .overflow_y_scroll()
519 .max_w(max_size.width)
520 .max_h(max_size.height)
521 // Prevent a mouse down/move on the popover from being propagated to the editor,
522 // because that would dismiss the popover.
523 .on_mouse_move(|_, cx| cx.stop_propagation())
524 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
525 .child(crate::render_parsed_markdown(
526 "content",
527 &self.parsed_content,
528 style,
529 workspace,
530 cx,
531 ))
532 .into_any_element()
533 }
534}
535
536#[derive(Debug, Clone)]
537pub struct DiagnosticPopover {
538 local_diagnostic: DiagnosticEntry<Anchor>,
539 primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
540}
541
542impl DiagnosticPopover {
543 pub fn render(
544 &self,
545 style: &EditorStyle,
546 max_size: Size<Pixels>,
547 cx: &mut ViewContext<Editor>,
548 ) -> AnyElement {
549 let text = match &self.local_diagnostic.diagnostic.source {
550 Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
551 None => self.local_diagnostic.diagnostic.message.clone(),
552 };
553
554 let status_colors = cx.theme().status();
555
556 struct DiagnosticColors {
557 pub background: Hsla,
558 pub border: Hsla,
559 }
560
561 let diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
562 DiagnosticSeverity::ERROR => DiagnosticColors {
563 background: status_colors.error_background,
564 border: status_colors.error_border,
565 },
566 DiagnosticSeverity::WARNING => DiagnosticColors {
567 background: status_colors.warning_background,
568 border: status_colors.warning_border,
569 },
570 DiagnosticSeverity::INFORMATION => DiagnosticColors {
571 background: status_colors.info_background,
572 border: status_colors.info_border,
573 },
574 DiagnosticSeverity::HINT => DiagnosticColors {
575 background: status_colors.hint_background,
576 border: status_colors.hint_border,
577 },
578 _ => DiagnosticColors {
579 background: status_colors.ignored_background,
580 border: status_colors.ignored_border,
581 },
582 };
583
584 div()
585 .id("diagnostic")
586 .block()
587 .elevation_2(cx)
588 .overflow_y_scroll()
589 .px_2()
590 .py_1()
591 .bg(diagnostic_colors.background)
592 .text_color(style.text.color)
593 .border_1()
594 .border_color(diagnostic_colors.border)
595 .rounded_md()
596 .max_w(max_size.width)
597 .max_h(max_size.height)
598 .cursor(CursorStyle::PointingHand)
599 .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
600 // Prevent a mouse move on the popover from being propagated to the editor,
601 // because that would dismiss the popover.
602 .on_mouse_move(|_, cx| cx.stop_propagation())
603 // Prevent a mouse down on the popover from being propagated to the editor,
604 // because that would move the cursor.
605 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
606 .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
607 .child(SharedString::from(text))
608 .into_any_element()
609 }
610
611 pub fn activation_info(&self) -> (usize, Anchor) {
612 let entry = self
613 .primary_diagnostic
614 .as_ref()
615 .unwrap_or(&self.local_diagnostic);
616
617 (entry.diagnostic.group_id, entry.range.start)
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use crate::{
625 editor_tests::init_test,
626 hover_links::update_inlay_link_and_hover_points,
627 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
628 test::editor_lsp_test_context::EditorLspTestContext,
629 InlayId, PointForPosition,
630 };
631 use collections::BTreeSet;
632 use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
633 use indoc::indoc;
634 use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
635 use lsp::LanguageServerId;
636 use project::{HoverBlock, HoverBlockKind};
637 use smol::stream::StreamExt;
638 use unindent::Unindent;
639 use util::test::marked_text_ranges;
640
641 #[gpui::test]
642 async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
643 init_test(cx, |_| {});
644
645 let mut cx = EditorLspTestContext::new_rust(
646 lsp::ServerCapabilities {
647 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
648 ..Default::default()
649 },
650 cx,
651 )
652 .await;
653
654 // Basic hover delays and then pops without moving the mouse
655 cx.set_state(indoc! {"
656 fn ˇtest() { println!(); }
657 "});
658 let hover_point = cx.display_point(indoc! {"
659 fn test() { printˇln!(); }
660 "});
661
662 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
663 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
664
665 // After delay, hover should be visible.
666 let symbol_range = cx.lsp_range(indoc! {"
667 fn test() { «println!»(); }
668 "});
669 let mut requests =
670 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
671 Ok(Some(lsp::Hover {
672 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
673 kind: lsp::MarkupKind::Markdown,
674 value: "some basic docs".to_string(),
675 }),
676 range: Some(symbol_range),
677 }))
678 });
679 cx.background_executor
680 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
681 requests.next().await;
682
683 cx.editor(|editor, _| {
684 assert!(editor.hover_state.visible());
685 assert_eq!(
686 editor.hover_state.info_popovers.len(),
687 1,
688 "Expected exactly one hover but got: {:?}",
689 editor.hover_state.info_popovers
690 );
691 let rendered = editor
692 .hover_state
693 .info_popovers
694 .first()
695 .cloned()
696 .unwrap()
697 .parsed_content;
698 assert_eq!(rendered.text, "some basic docs".to_string())
699 });
700
701 // Mouse moved with no hover response dismisses
702 let hover_point = cx.display_point(indoc! {"
703 fn teˇst() { println!(); }
704 "});
705 let mut request = cx
706 .lsp
707 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
708 cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
709 cx.background_executor
710 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
711 request.next().await;
712 cx.editor(|editor, _| {
713 assert!(!editor.hover_state.visible());
714 });
715 }
716
717 #[gpui::test]
718 async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
719 init_test(cx, |_| {});
720
721 let mut cx = EditorLspTestContext::new_rust(
722 lsp::ServerCapabilities {
723 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
724 ..Default::default()
725 },
726 cx,
727 )
728 .await;
729
730 // Hover with keyboard has no delay
731 cx.set_state(indoc! {"
732 fˇn test() { println!(); }
733 "});
734 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
735 let symbol_range = cx.lsp_range(indoc! {"
736 «fn» test() { println!(); }
737 "});
738 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
739 Ok(Some(lsp::Hover {
740 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
741 kind: lsp::MarkupKind::Markdown,
742 value: "some other basic docs".to_string(),
743 }),
744 range: Some(symbol_range),
745 }))
746 })
747 .next()
748 .await;
749
750 cx.condition(|editor, _| editor.hover_state.visible()).await;
751 cx.editor(|editor, _| {
752 assert_eq!(
753 editor.hover_state.info_popovers.len(),
754 1,
755 "Expected exactly one hover but got: {:?}",
756 editor.hover_state.info_popovers
757 );
758 let rendered = editor
759 .hover_state
760 .info_popovers
761 .first()
762 .cloned()
763 .unwrap()
764 .parsed_content;
765 assert_eq!(rendered.text, "some other basic docs".to_string())
766 });
767 }
768
769 #[gpui::test]
770 async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
771 init_test(cx, |_| {});
772
773 let mut cx = EditorLspTestContext::new_rust(
774 lsp::ServerCapabilities {
775 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
776 ..Default::default()
777 },
778 cx,
779 )
780 .await;
781
782 // Hover with keyboard has no delay
783 cx.set_state(indoc! {"
784 fˇn test() { println!(); }
785 "});
786 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
787 let symbol_range = cx.lsp_range(indoc! {"
788 «fn» test() { println!(); }
789 "});
790 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
791 Ok(Some(lsp::Hover {
792 contents: lsp::HoverContents::Array(vec![
793 lsp::MarkedString::String("regular text for hover to show".to_string()),
794 lsp::MarkedString::String("".to_string()),
795 lsp::MarkedString::LanguageString(lsp::LanguageString {
796 language: "Rust".to_string(),
797 value: "".to_string(),
798 }),
799 ]),
800 range: Some(symbol_range),
801 }))
802 })
803 .next()
804 .await;
805
806 cx.condition(|editor, _| editor.hover_state.visible()).await;
807 cx.editor(|editor, _| {
808 assert_eq!(
809 editor.hover_state.info_popovers.len(),
810 1,
811 "Expected exactly one hover but got: {:?}",
812 editor.hover_state.info_popovers
813 );
814 let rendered = editor
815 .hover_state
816 .info_popovers
817 .first()
818 .cloned()
819 .unwrap()
820 .parsed_content;
821 assert_eq!(
822 rendered.text,
823 "regular text for hover to show".to_string(),
824 "No empty string hovers should be shown"
825 );
826 });
827 }
828
829 #[gpui::test]
830 async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
831 init_test(cx, |_| {});
832
833 let mut cx = EditorLspTestContext::new_rust(
834 lsp::ServerCapabilities {
835 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
836 ..Default::default()
837 },
838 cx,
839 )
840 .await;
841
842 // Hover with keyboard has no delay
843 cx.set_state(indoc! {"
844 fˇn test() { println!(); }
845 "});
846 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
847 let symbol_range = cx.lsp_range(indoc! {"
848 «fn» test() { println!(); }
849 "});
850
851 let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
852 let markdown_string = format!("\n```rust\n{code_str}```");
853
854 let closure_markdown_string = markdown_string.clone();
855 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
856 let future_markdown_string = closure_markdown_string.clone();
857 async move {
858 Ok(Some(lsp::Hover {
859 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
860 kind: lsp::MarkupKind::Markdown,
861 value: future_markdown_string,
862 }),
863 range: Some(symbol_range),
864 }))
865 }
866 })
867 .next()
868 .await;
869
870 cx.condition(|editor, _| editor.hover_state.visible()).await;
871 cx.editor(|editor, _| {
872 assert_eq!(
873 editor.hover_state.info_popovers.len(),
874 1,
875 "Expected exactly one hover but got: {:?}",
876 editor.hover_state.info_popovers
877 );
878 let rendered = editor
879 .hover_state
880 .info_popovers
881 .first()
882 .cloned()
883 .unwrap()
884 .parsed_content;
885 assert_eq!(
886 rendered.text,
887 code_str.trim(),
888 "Should not have extra line breaks at end of rendered hover"
889 );
890 });
891 }
892
893 #[gpui::test]
894 async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
895 init_test(cx, |_| {});
896
897 let mut cx = EditorLspTestContext::new_rust(
898 lsp::ServerCapabilities {
899 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
900 ..Default::default()
901 },
902 cx,
903 )
904 .await;
905
906 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
907 // info popover once request completes
908 cx.set_state(indoc! {"
909 fn teˇst() { println!(); }
910 "});
911
912 // Send diagnostic to client
913 let range = cx.text_anchor_range(indoc! {"
914 fn «test»() { println!(); }
915 "});
916 cx.update_buffer(|buffer, cx| {
917 let snapshot = buffer.text_snapshot();
918 let set = DiagnosticSet::from_sorted_entries(
919 vec![DiagnosticEntry {
920 range,
921 diagnostic: Diagnostic {
922 message: "A test diagnostic message.".to_string(),
923 ..Default::default()
924 },
925 }],
926 &snapshot,
927 );
928 buffer.update_diagnostics(LanguageServerId(0), set, cx);
929 });
930
931 // Hover pops diagnostic immediately
932 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
933 cx.background_executor.run_until_parked();
934
935 cx.editor(|Editor { hover_state, .. }, _| {
936 assert!(
937 hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
938 )
939 });
940
941 // Info Popover shows after request responded to
942 let range = cx.lsp_range(indoc! {"
943 fn «test»() { println!(); }
944 "});
945 cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
946 Ok(Some(lsp::Hover {
947 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
948 kind: lsp::MarkupKind::Markdown,
949 value: "some new docs".to_string(),
950 }),
951 range: Some(range),
952 }))
953 });
954 cx.background_executor
955 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
956
957 cx.background_executor.run_until_parked();
958 cx.editor(|Editor { hover_state, .. }, _| {
959 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
960 });
961 }
962
963 #[gpui::test]
964 fn test_render_blocks(cx: &mut gpui::TestAppContext) {
965 init_test(cx, |_| {});
966
967 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
968 let editor = cx.add_window(|cx| Editor::single_line(cx));
969 editor
970 .update(cx, |editor, _cx| {
971 let style = editor.style.clone().unwrap();
972
973 struct Row {
974 blocks: Vec<HoverBlock>,
975 expected_marked_text: String,
976 expected_styles: Vec<HighlightStyle>,
977 }
978
979 let rows = &[
980 // Strong emphasis
981 Row {
982 blocks: vec![HoverBlock {
983 text: "one **two** three".to_string(),
984 kind: HoverBlockKind::Markdown,
985 }],
986 expected_marked_text: "one «two» three".to_string(),
987 expected_styles: vec![HighlightStyle {
988 font_weight: Some(FontWeight::BOLD),
989 ..Default::default()
990 }],
991 },
992 // Links
993 Row {
994 blocks: vec three".to_string(),
996 kind: HoverBlockKind::Markdown,
997 }],
998 expected_marked_text: "one «two» three".to_string(),
999 expected_styles: vec![HighlightStyle {
1000 underline: Some(UnderlineStyle {
1001 thickness: 1.0.into(),
1002 ..Default::default()
1003 }),
1004 ..Default::default()
1005 }],
1006 },
1007 // Lists
1008 Row {
1009 blocks: vec
1017 - d"
1018 .unindent(),
1019 kind: HoverBlockKind::Markdown,
1020 }],
1021 expected_marked_text: "
1022 lists:
1023 - one
1024 - a
1025 - b
1026 - two
1027 - «c»
1028 - d"
1029 .unindent(),
1030 expected_styles: vec![HighlightStyle {
1031 underline: Some(UnderlineStyle {
1032 thickness: 1.0.into(),
1033 ..Default::default()
1034 }),
1035 ..Default::default()
1036 }],
1037 },
1038 // Multi-paragraph list items
1039 Row {
1040 blocks: vec![HoverBlock {
1041 text: "
1042 * one two
1043 three
1044
1045 * four five
1046 * six seven
1047 eight
1048
1049 nine
1050 * ten
1051 * six"
1052 .unindent(),
1053 kind: HoverBlockKind::Markdown,
1054 }],
1055 expected_marked_text: "
1056 - one two three
1057 - four five
1058 - six seven eight
1059
1060 nine
1061 - ten
1062 - six"
1063 .unindent(),
1064 expected_styles: vec![HighlightStyle {
1065 underline: Some(UnderlineStyle {
1066 thickness: 1.0.into(),
1067 ..Default::default()
1068 }),
1069 ..Default::default()
1070 }],
1071 },
1072 ];
1073
1074 for Row {
1075 blocks,
1076 expected_marked_text,
1077 expected_styles,
1078 } in &rows[0..]
1079 {
1080 let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
1081
1082 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
1083 let expected_highlights = ranges
1084 .into_iter()
1085 .zip(expected_styles.iter().cloned())
1086 .collect::<Vec<_>>();
1087 assert_eq!(
1088 rendered.text, expected_text,
1089 "wrong text for input {blocks:?}"
1090 );
1091
1092 let rendered_highlights: Vec<_> = rendered
1093 .highlights
1094 .iter()
1095 .filter_map(|(range, highlight)| {
1096 let highlight = highlight.to_highlight_style(&style.syntax)?;
1097 Some((range.clone(), highlight))
1098 })
1099 .collect();
1100
1101 assert_eq!(
1102 rendered_highlights, expected_highlights,
1103 "wrong highlights for input {blocks:?}"
1104 );
1105 }
1106 })
1107 .unwrap();
1108 }
1109
1110 #[gpui::test]
1111 async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1112 init_test(cx, |settings| {
1113 settings.defaults.inlay_hints = Some(InlayHintSettings {
1114 enabled: true,
1115 edit_debounce_ms: 0,
1116 scroll_debounce_ms: 0,
1117 show_type_hints: true,
1118 show_parameter_hints: true,
1119 show_other_hints: true,
1120 })
1121 });
1122
1123 let mut cx = EditorLspTestContext::new_rust(
1124 lsp::ServerCapabilities {
1125 inlay_hint_provider: Some(lsp::OneOf::Right(
1126 lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1127 resolve_provider: Some(true),
1128 ..Default::default()
1129 }),
1130 )),
1131 ..Default::default()
1132 },
1133 cx,
1134 )
1135 .await;
1136
1137 cx.set_state(indoc! {"
1138 struct TestStruct;
1139
1140 // ==================
1141
1142 struct TestNewType<T>(T);
1143
1144 fn main() {
1145 let variableˇ = TestNewType(TestStruct);
1146 }
1147 "});
1148
1149 let hint_start_offset = cx.ranges(indoc! {"
1150 struct TestStruct;
1151
1152 // ==================
1153
1154 struct TestNewType<T>(T);
1155
1156 fn main() {
1157 let variableˇ = TestNewType(TestStruct);
1158 }
1159 "})[0]
1160 .start;
1161 let hint_position = cx.to_lsp(hint_start_offset);
1162 let new_type_target_range = cx.lsp_range(indoc! {"
1163 struct TestStruct;
1164
1165 // ==================
1166
1167 struct «TestNewType»<T>(T);
1168
1169 fn main() {
1170 let variable = TestNewType(TestStruct);
1171 }
1172 "});
1173 let struct_target_range = cx.lsp_range(indoc! {"
1174 struct «TestStruct»;
1175
1176 // ==================
1177
1178 struct TestNewType<T>(T);
1179
1180 fn main() {
1181 let variable = TestNewType(TestStruct);
1182 }
1183 "});
1184
1185 let uri = cx.buffer_lsp_url.clone();
1186 let new_type_label = "TestNewType";
1187 let struct_label = "TestStruct";
1188 let entire_hint_label = ": TestNewType<TestStruct>";
1189 let closure_uri = uri.clone();
1190 cx.lsp
1191 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1192 let task_uri = closure_uri.clone();
1193 async move {
1194 assert_eq!(params.text_document.uri, task_uri);
1195 Ok(Some(vec![lsp::InlayHint {
1196 position: hint_position,
1197 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1198 value: entire_hint_label.to_string(),
1199 ..Default::default()
1200 }]),
1201 kind: Some(lsp::InlayHintKind::TYPE),
1202 text_edits: None,
1203 tooltip: None,
1204 padding_left: Some(false),
1205 padding_right: Some(false),
1206 data: None,
1207 }]))
1208 }
1209 })
1210 .next()
1211 .await;
1212 cx.background_executor.run_until_parked();
1213 cx.update_editor(|editor, cx| {
1214 let expected_layers = vec![entire_hint_label.to_string()];
1215 assert_eq!(expected_layers, cached_hint_labels(editor));
1216 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1217 });
1218
1219 let inlay_range = cx
1220 .ranges(indoc! {"
1221 struct TestStruct;
1222
1223 // ==================
1224
1225 struct TestNewType<T>(T);
1226
1227 fn main() {
1228 let variable« »= TestNewType(TestStruct);
1229 }
1230 "})
1231 .get(0)
1232 .cloned()
1233 .unwrap();
1234 let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1235 let snapshot = editor.snapshot(cx);
1236 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1237 let next_valid = inlay_range.end.to_display_point(&snapshot);
1238 assert_eq!(previous_valid.row(), next_valid.row());
1239 assert!(previous_valid.column() < next_valid.column());
1240 let exact_unclipped = DisplayPoint::new(
1241 previous_valid.row(),
1242 previous_valid.column()
1243 + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1244 as u32,
1245 );
1246 PointForPosition {
1247 previous_valid,
1248 next_valid,
1249 exact_unclipped,
1250 column_overshoot_after_line_end: 0,
1251 }
1252 });
1253 cx.update_editor(|editor, cx| {
1254 update_inlay_link_and_hover_points(
1255 &editor.snapshot(cx),
1256 new_type_hint_part_hover_position,
1257 editor,
1258 true,
1259 false,
1260 cx,
1261 );
1262 });
1263
1264 let resolve_closure_uri = uri.clone();
1265 cx.lsp
1266 .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1267 move |mut hint_to_resolve, _| {
1268 let mut resolved_hint_positions = BTreeSet::new();
1269 let task_uri = resolve_closure_uri.clone();
1270 async move {
1271 let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1272 assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1273
1274 // `: TestNewType<TestStruct>`
1275 hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1276 lsp::InlayHintLabelPart {
1277 value: ": ".to_string(),
1278 ..Default::default()
1279 },
1280 lsp::InlayHintLabelPart {
1281 value: new_type_label.to_string(),
1282 location: Some(lsp::Location {
1283 uri: task_uri.clone(),
1284 range: new_type_target_range,
1285 }),
1286 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1287 "A tooltip for `{new_type_label}`"
1288 ))),
1289 ..Default::default()
1290 },
1291 lsp::InlayHintLabelPart {
1292 value: "<".to_string(),
1293 ..Default::default()
1294 },
1295 lsp::InlayHintLabelPart {
1296 value: struct_label.to_string(),
1297 location: Some(lsp::Location {
1298 uri: task_uri,
1299 range: struct_target_range,
1300 }),
1301 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1302 lsp::MarkupContent {
1303 kind: lsp::MarkupKind::Markdown,
1304 value: format!("A tooltip for `{struct_label}`"),
1305 },
1306 )),
1307 ..Default::default()
1308 },
1309 lsp::InlayHintLabelPart {
1310 value: ">".to_string(),
1311 ..Default::default()
1312 },
1313 ]);
1314
1315 Ok(hint_to_resolve)
1316 }
1317 },
1318 )
1319 .next()
1320 .await;
1321 cx.background_executor.run_until_parked();
1322
1323 cx.update_editor(|editor, cx| {
1324 update_inlay_link_and_hover_points(
1325 &editor.snapshot(cx),
1326 new_type_hint_part_hover_position,
1327 editor,
1328 true,
1329 false,
1330 cx,
1331 );
1332 });
1333 cx.background_executor
1334 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1335 cx.background_executor.run_until_parked();
1336 cx.update_editor(|editor, cx| {
1337 let hover_state = &editor.hover_state;
1338 assert!(
1339 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1340 );
1341 let popover = hover_state.info_popovers.first().cloned().unwrap();
1342 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1343 assert_eq!(
1344 popover.symbol_range,
1345 RangeInEditor::Inlay(InlayHighlight {
1346 inlay: InlayId::Hint(0),
1347 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1348 range: ": ".len()..": ".len() + new_type_label.len(),
1349 }),
1350 "Popover range should match the new type label part"
1351 );
1352 assert_eq!(
1353 popover.parsed_content.text,
1354 format!("A tooltip for `{new_type_label}`"),
1355 "Rendered text should not anyhow alter backticks"
1356 );
1357 });
1358
1359 let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1360 let snapshot = editor.snapshot(cx);
1361 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1362 let next_valid = inlay_range.end.to_display_point(&snapshot);
1363 assert_eq!(previous_valid.row(), next_valid.row());
1364 assert!(previous_valid.column() < next_valid.column());
1365 let exact_unclipped = DisplayPoint::new(
1366 previous_valid.row(),
1367 previous_valid.column()
1368 + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1369 as u32,
1370 );
1371 PointForPosition {
1372 previous_valid,
1373 next_valid,
1374 exact_unclipped,
1375 column_overshoot_after_line_end: 0,
1376 }
1377 });
1378 cx.update_editor(|editor, cx| {
1379 update_inlay_link_and_hover_points(
1380 &editor.snapshot(cx),
1381 struct_hint_part_hover_position,
1382 editor,
1383 true,
1384 false,
1385 cx,
1386 );
1387 });
1388 cx.background_executor
1389 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1390 cx.background_executor.run_until_parked();
1391 cx.update_editor(|editor, cx| {
1392 let hover_state = &editor.hover_state;
1393 assert!(
1394 hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
1395 );
1396 let popover = hover_state.info_popovers.first().cloned().unwrap();
1397 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1398 assert_eq!(
1399 popover.symbol_range,
1400 RangeInEditor::Inlay(InlayHighlight {
1401 inlay: InlayId::Hint(0),
1402 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1403 range: ": ".len() + new_type_label.len() + "<".len()
1404 ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1405 }),
1406 "Popover range should match the struct label part"
1407 );
1408 assert_eq!(
1409 popover.parsed_content.text,
1410 format!("A tooltip for {struct_label}"),
1411 "Rendered markdown element should remove backticks from text"
1412 );
1413 });
1414 }
1415}