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