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