Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:33:36.202Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

hover_popover.rs

2782 lines · 104.9 KB · rust
1use crate::{
2    Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
3    GlobalDiagnosticRenderer, HighlightKey, Hover,
4    display_map::{InlayOffset, ToDisplayPoint, is_invisible},
5    editor_settings::EditorSettingsScrollbarProxy,
6    hover_links::{InlayHighlight, RangeInEditor},
7    movement::TextLayoutDetails,
8    scroll::ScrollAmount,
9};
10use anyhow::Context as _;
11use gpui::{
12    AnyElement, App, AsyncWindowContext, Bounds, Context, Entity, Focusable as _, FontWeight, Hsla,
13    InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
14    StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TaskExt,
15    TextStyleRefinement, Window, canvas, div, px,
16};
17use itertools::Itertools;
18use language::{DiagnosticEntry, Language, LanguageRegistry};
19use lsp::DiagnosticSeverity;
20use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
21use multi_buffer::{MultiBufferOffset, ToOffset, ToPoint};
22use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
23use settings::Settings;
24use std::{
25    borrow::Cow,
26    cell::{Cell, RefCell},
27};
28use std::{ops::Range, sync::Arc, time::Duration};
29use std::{path::PathBuf, rc::Rc};
30use theme_settings::ThemeSettings;
31use ui::{CopyButton, Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
32use url::Url;
33use util::TryFutureExt;
34use workspace::{OpenOptions, OpenVisible, Workspace};
35
36pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
37pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
38pub const POPOVER_RIGHT_OFFSET: Pixels = px(8.0);
39pub const HOVER_POPOVER_GAP: Pixels = px(10.);
40
41/// Bindable action which uses the most recent selection head to trigger a hover
42pub fn hover(editor: &mut Editor, _: &Hover, window: &mut Window, cx: &mut Context<Editor>) {
43    let head = editor.selections.newest_anchor().head();
44    show_hover(editor, head, true, window, cx);
45}
46
47/// The internal hover action dispatches between `show_hover` or `hide_hover`
48/// depending on whether a point to hover over is provided.
49pub fn hover_at(
50    editor: &mut Editor,
51    anchor: Option<Anchor>,
52    mouse_position: Option<gpui::Point<Pixels>>,
53    window: &mut Window,
54    cx: &mut Context<Editor>,
55) {
56    if EditorSettings::get_global(cx).hover_popover_enabled {
57        if show_keyboard_hover(editor, window, cx) {
58            return;
59        }
60
61        if let Some(anchor) = anchor {
62            editor.hover_state.hiding_delay_task = None;
63            editor.hover_state.closest_mouse_distance = None;
64            show_hover(editor, anchor, false, window, cx);
65        } else if !editor.hover_state.visible() {
66            editor.hover_state.info_task = None;
67        } else {
68            let settings = EditorSettings::get_global(cx);
69            if !settings.hover_popover_sticky {
70                hide_hover(editor, cx);
71                return;
72            }
73
74            let mut getting_closer = false;
75            if let Some(mouse_position) = mouse_position {
76                getting_closer = editor.hover_state.is_mouse_getting_closer(mouse_position);
77            }
78
79            // If we are moving away and a timer is already running, just let it count down.
80            if !getting_closer && editor.hover_state.hiding_delay_task.is_some() {
81                return;
82            }
83
84            // If we are moving closer, or if no timer is running at all, start/restart the timer.
85            let delay = Duration::from_millis(settings.hover_popover_hiding_delay.0);
86            let task = cx.spawn(async move |this, cx| {
87                cx.background_executor().timer(delay).await;
88                this.update(cx, |editor, cx| {
89                    hide_hover(editor, cx);
90                })
91                .ok();
92            });
93            editor.hover_state.hiding_delay_task = Some(task);
94        }
95    }
96}
97
98pub fn show_keyboard_hover(
99    editor: &mut Editor,
100    window: &mut Window,
101    cx: &mut Context<Editor>,
102) -> bool {
103    if let Some(anchor) = editor.hover_state.info_popovers.iter().find_map(|p| {
104        if *p.keyboard_grace.borrow() {
105            p.anchor
106        } else {
107            None
108        }
109    }) {
110        show_hover(editor, anchor, false, window, cx);
111        return true;
112    }
113
114    if let Some(anchor) = editor
115        .hover_state
116        .diagnostic_popover
117        .as_ref()
118        .and_then(|d| {
119            if *d.keyboard_grace.borrow() {
120                Some(d.anchor)
121            } else {
122                None
123            }
124        })
125    {
126        show_hover(editor, anchor, false, window, cx);
127        return true;
128    }
129
130    false
131}
132
133pub struct InlayHover {
134    pub(crate) range: InlayHighlight,
135    pub tooltip: HoverBlock,
136}
137
138pub fn find_hovered_hint_part(
139    label_parts: Vec<InlayHintLabelPart>,
140    hint_start: InlayOffset,
141    hovered_offset: InlayOffset,
142) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
143    if hovered_offset >= hint_start {
144        let mut offset_in_hint = hovered_offset - hint_start;
145        let mut part_start = hint_start;
146        for part in label_parts {
147            let part_len = part.value.len();
148            if offset_in_hint >= part_len {
149                offset_in_hint -= part_len;
150                part_start.0 += part_len;
151            } else {
152                let part_end = InlayOffset(part_start.0 + part_len);
153                return Some((part, part_start..part_end));
154            }
155        }
156    }
157    None
158}
159
160pub fn hover_at_inlay(
161    editor: &mut Editor,
162    inlay_hover: InlayHover,
163    window: &mut Window,
164    cx: &mut Context<Editor>,
165) {
166    if EditorSettings::get_global(cx).hover_popover_enabled {
167        if editor.pending_rename.is_some() {
168            return;
169        }
170
171        let Some(project) = editor.project.clone() else {
172            return;
173        };
174
175        if editor
176            .hover_state
177            .info_popovers
178            .iter()
179            .any(|InfoPopover { symbol_range, .. }| {
180                if let RangeInEditor::Inlay(range) = symbol_range
181                    && range == &inlay_hover.range
182                {
183                    // Hover triggered from same location as last time. Don't show again.
184                    return true;
185                }
186                false
187            })
188        {
189            return;
190        }
191
192        let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
193
194        editor.hover_state.hiding_delay_task = None;
195        editor.hover_state.closest_mouse_distance = None;
196
197        let task = cx.spawn_in(window, async move |this, cx| {
198            async move {
199                cx.background_executor()
200                    .timer(Duration::from_millis(hover_popover_delay))
201                    .await;
202                this.update(cx, |this, _| {
203                    this.hover_state.diagnostic_popover = None;
204                })?;
205
206                let language_registry = project.read_with(cx, |p, _| p.languages().clone());
207                let blocks = vec![inlay_hover.tooltip];
208                let parsed_content = parse_blocks(&blocks, Some(&language_registry), None, cx);
209
210                let scroll_handle = ScrollHandle::new();
211
212                let subscription = this
213                    .update(cx, |_, cx| {
214                        parsed_content.as_ref().map(|parsed_content| {
215                            cx.observe(parsed_content, |_, _, cx| cx.notify())
216                        })
217                    })
218                    .ok()
219                    .flatten();
220
221                let hover_popover = InfoPopover {
222                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
223                    parsed_content,
224                    scroll_handle,
225                    keyboard_grace: Rc::new(RefCell::new(false)),
226                    anchor: None,
227                    last_bounds: Rc::new(Cell::new(None)),
228                    _subscription: subscription,
229                };
230
231                this.update(cx, |this, cx| {
232                    // TODO: no background highlights happen for inlays currently
233                    this.hover_state.info_popovers = vec![hover_popover];
234                    cx.notify();
235                })?;
236
237                anyhow::Ok(())
238            }
239            .log_err()
240            .await
241        });
242
243        editor.hover_state.info_task = Some(task);
244    }
245}
246
247/// Hides the type information popup.
248/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
249/// selections changed.
250pub fn hide_hover(editor: &mut Editor, cx: &mut Context<Editor>) -> bool {
251    let info_popovers = editor.hover_state.info_popovers.drain(..);
252    let diagnostics_popover = editor.hover_state.diagnostic_popover.take();
253    let did_hide = info_popovers.count() > 0 || diagnostics_popover.is_some();
254
255    editor.hover_state.info_task = None;
256    editor.hover_state.hiding_delay_task = None;
257    editor.hover_state.closest_mouse_distance = None;
258
259    editor.clear_background_highlights(HighlightKey::HoverState, cx);
260
261    if did_hide {
262        cx.notify();
263    }
264
265    did_hide
266}
267
268/// Queries the LSP and shows type info and documentation
269/// about the symbol the mouse is currently hovering over.
270/// Triggered by the `Hover` action when the cursor may be over a symbol.
271fn show_hover(
272    editor: &mut Editor,
273    anchor: Anchor,
274    ignore_timeout: bool,
275    window: &mut Window,
276    cx: &mut Context<Editor>,
277) -> Option<()> {
278    if editor.pending_rename.is_some() {
279        return None;
280    }
281
282    let snapshot = editor.snapshot(window, cx);
283
284    let (buffer_position, _) = editor
285        .buffer
286        .read(cx)
287        .snapshot(cx)
288        .anchor_to_buffer_anchor(anchor)?;
289    let buffer = editor.buffer.read(cx).buffer(buffer_position.buffer_id)?;
290
291    let language_registry = editor
292        .project()
293        .map(|project| project.read(cx).languages().clone());
294    let provider = editor.semantics_provider.clone()?;
295
296    editor.hover_state.hiding_delay_task = None;
297    editor.hover_state.closest_mouse_distance = None;
298
299    if !ignore_timeout {
300        if same_info_hover(editor, &snapshot, anchor)
301            || same_diagnostic_hover(editor, &snapshot, anchor)
302            || editor.hover_state.diagnostic_popover.is_some()
303        {
304            // Hover triggered from same location as last time. Don't show again.
305            return None;
306        } else {
307            hide_hover(editor, cx);
308        }
309    }
310
311    let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
312    let all_diagnostics_active = editor.all_diagnostics_active();
313    let active_group_id = editor.active_diagnostic_group_id();
314
315    let renderer = GlobalDiagnosticRenderer::global(cx);
316    let task = cx.spawn_in(window, async move |this, cx| {
317        async move {
318            // If we need to delay, delay a set amount initially before making the lsp request
319            let delay = if ignore_timeout {
320                None
321            } else {
322                let lsp_request_early = hover_popover_delay / 2;
323                cx.background_executor()
324                    .timer(Duration::from_millis(
325                        hover_popover_delay - lsp_request_early,
326                    ))
327                    .await;
328
329                // Construct delay task to wait for later
330                let total_delay = Some(
331                    cx.background_executor()
332                        .timer(Duration::from_millis(lsp_request_early)),
333                );
334                total_delay
335            };
336
337            let hover_request = cx.update(|_, cx| provider.hover(&buffer, buffer_position, cx))?;
338
339            if let Some(delay) = delay {
340                delay.await;
341            }
342            let offset = anchor.to_offset(&snapshot.buffer_snapshot());
343            let local_diagnostic = if all_diagnostics_active {
344                None
345            } else {
346                snapshot
347                    .buffer_snapshot()
348                    .diagnostics_with_buffer_ids_in_range::<MultiBufferOffset>(offset..offset)
349                    .filter(|(_, diagnostic)| {
350                        Some(diagnostic.diagnostic.group_id) != active_group_id
351                    })
352                    // Find the entry with the most specific range
353                    .min_by_key(|(_, entry)| entry.range.end - entry.range.start)
354            };
355
356            let diagnostic_popover = if let Some((buffer_id, local_diagnostic)) = local_diagnostic {
357                let group = snapshot
358                    .buffer_snapshot()
359                    .diagnostic_group(buffer_id, local_diagnostic.diagnostic.group_id)
360                    .collect::<Vec<_>>();
361                let point_range = local_diagnostic
362                    .range
363                    .start
364                    .to_point(&snapshot.buffer_snapshot())
365                    ..local_diagnostic
366                        .range
367                        .end
368                        .to_point(&snapshot.buffer_snapshot());
369                let markdown = cx.update(|_, cx| {
370                    renderer
371                        .as_ref()
372                        .and_then(|renderer| {
373                            renderer.render_hover(
374                                group,
375                                point_range,
376                                buffer_id,
377                                language_registry.clone(),
378                                cx,
379                            )
380                        })
381                        .context("no rendered diagnostic")
382                })??;
383
384                let (background_color, border_color) = cx.update(|_, cx| {
385                    let status_colors = cx.theme().status();
386                    match local_diagnostic.diagnostic.severity {
387                        DiagnosticSeverity::ERROR => {
388                            (status_colors.error_background, status_colors.error_border)
389                        }
390                        DiagnosticSeverity::WARNING => (
391                            status_colors.warning_background,
392                            status_colors.warning_border,
393                        ),
394                        DiagnosticSeverity::INFORMATION => {
395                            (status_colors.info_background, status_colors.info_border)
396                        }
397                        DiagnosticSeverity::HINT => {
398                            (status_colors.hint_background, status_colors.hint_border)
399                        }
400                        _ => (
401                            status_colors.ignored_background,
402                            status_colors.ignored_border,
403                        ),
404                    }
405                })?;
406
407                let subscription =
408                    this.update(cx, |_, cx| cx.observe(&markdown, |_, _, cx| cx.notify()))?;
409
410                let local_diagnostic = DiagnosticEntry {
411                    diagnostic: local_diagnostic.diagnostic.to_owned(),
412                    range: snapshot
413                        .buffer_snapshot()
414                        .anchor_before(local_diagnostic.range.start)
415                        ..snapshot
416                            .buffer_snapshot()
417                            .anchor_after(local_diagnostic.range.end),
418                };
419
420                let scroll_handle = ScrollHandle::new();
421
422                Some(DiagnosticPopover {
423                    local_diagnostic,
424                    markdown,
425                    border_color,
426                    scroll_handle,
427                    background_color,
428                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
429                    anchor,
430                    last_bounds: Rc::new(Cell::new(None)),
431                    _subscription: subscription,
432                })
433            } else {
434                None
435            };
436
437            this.update(cx, |this, _| {
438                this.hover_state.diagnostic_popover = diagnostic_popover;
439            })?;
440
441            let invisible_char = if let Some(invisible) = snapshot
442                .buffer_snapshot()
443                .chars_at(anchor)
444                .next()
445                .filter(|&c| is_invisible(c))
446            {
447                let after = snapshot.buffer_snapshot().anchor_after(
448                    anchor.to_offset(&snapshot.buffer_snapshot()) + invisible.len_utf8(),
449                );
450                Some((invisible, anchor..after))
451            } else if let Some(invisible) = snapshot
452                .buffer_snapshot()
453                .reversed_chars_at(anchor)
454                .next()
455                .filter(|&c| is_invisible(c))
456            {
457                let before = snapshot.buffer_snapshot().anchor_before(
458                    anchor.to_offset(&snapshot.buffer_snapshot()) - invisible.len_utf8(),
459                );
460
461                Some((invisible, before..anchor))
462            } else {
463                None
464            };
465
466            let hovers_response = if let Some(hover_request) = hover_request {
467                hover_request.await.unwrap_or_default()
468            } else {
469                Vec::new()
470            };
471            let snapshot = this.update_in(cx, |this, window, cx| this.snapshot(window, cx))?;
472            let mut hover_highlights = Vec::with_capacity(hovers_response.len());
473            let mut info_popovers = Vec::with_capacity(
474                hovers_response.len() + if invisible_char.is_some() { 1 } else { 0 },
475            );
476
477            if let Some((invisible, range)) = invisible_char {
478                let blocks = vec![HoverBlock {
479                    text: format!("Unicode character U+{:02X}", invisible as u32),
480                    kind: HoverBlockKind::PlainText,
481                }];
482                let parsed_content = parse_blocks(&blocks, language_registry.as_ref(), None, cx);
483                let scroll_handle = ScrollHandle::new();
484                let subscription = this
485                    .update(cx, |_, cx| {
486                        parsed_content.as_ref().map(|parsed_content| {
487                            cx.observe(parsed_content, |_, _, cx| cx.notify())
488                        })
489                    })
490                    .ok()
491                    .flatten();
492                info_popovers.push(InfoPopover {
493                    symbol_range: RangeInEditor::Text(range),
494                    parsed_content,
495                    scroll_handle,
496                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
497                    anchor: Some(anchor),
498                    last_bounds: Rc::new(Cell::new(None)),
499                    _subscription: subscription,
500                })
501            }
502
503            let doc_link_task = this
504                .update(cx, |editor, cx| {
505                    editor.document_links_at(buffer.clone(), buffer_position, cx)
506                })
507                .ok()
508                .flatten();
509            let doc_link_tooltips = match doc_link_task {
510                Some(task) => task
511                    .await
512                    .into_iter()
513                    .filter_map(|(_, link)| {
514                        let multi_buffer_range = snapshot
515                            .buffer_snapshot()
516                            .buffer_anchor_range_to_anchor_range(link.range.clone())?;
517                        let tooltip = link.tooltip?;
518                        Some((multi_buffer_range, tooltip))
519                    })
520                    .collect::<Vec<_>>(),
521                None => Vec::new(),
522            };
523
524            for hover_result in hovers_response {
525                // Create symbol range of anchors for highlighting and filtering of future requests.
526                let range = hover_result
527                    .range
528                    .and_then(|range| {
529                        let range = snapshot
530                            .buffer_snapshot()
531                            .buffer_anchor_range_to_anchor_range(range)?;
532                        Some(range)
533                    })
534                    .or_else(|| {
535                        let snapshot = &snapshot.buffer_snapshot();
536                        let range = snapshot.syntax_ancestor(anchor..anchor)?.1;
537                        Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
538                    })
539                    .unwrap_or_else(|| anchor..anchor);
540
541                let blocks = hover_result.contents;
542                let language = hover_result.language;
543                let parsed_content =
544                    parse_blocks(&blocks, language_registry.as_ref(), language, cx);
545                let scroll_handle = ScrollHandle::new();
546                hover_highlights.push(range.clone());
547                let subscription = this
548                    .update(cx, |_, cx| {
549                        parsed_content.as_ref().map(|parsed_content| {
550                            cx.observe(parsed_content, |_, _, cx| cx.notify())
551                        })
552                    })
553                    .ok()
554                    .flatten();
555                info_popovers.push(InfoPopover {
556                    symbol_range: RangeInEditor::Text(range),
557                    parsed_content,
558                    scroll_handle,
559                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
560                    anchor: Some(anchor),
561                    last_bounds: Rc::new(Cell::new(None)),
562                    _subscription: subscription,
563                });
564            }
565
566            for (multi_buffer_range, tooltip) in doc_link_tooltips {
567                let blocks = vec![HoverBlock {
568                    text: tooltip.to_string(),
569                    kind: HoverBlockKind::Markdown,
570                }];
571                let parsed_content = parse_blocks(&blocks, language_registry.as_ref(), None, cx);
572                let scroll_handle = ScrollHandle::new();
573                let subscription = this
574                    .update(cx, |_, cx| {
575                        parsed_content.as_ref().map(|parsed_content| {
576                            cx.observe(parsed_content, |_, _, cx| cx.notify())
577                        })
578                    })
579                    .ok()
580                    .flatten();
581                info_popovers.push(InfoPopover {
582                    symbol_range: RangeInEditor::Text(multi_buffer_range),
583                    parsed_content,
584                    scroll_handle,
585                    keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
586                    anchor: Some(anchor),
587                    last_bounds: Rc::new(Cell::new(None)),
588                    _subscription: subscription,
589                });
590            }
591
592            this.update_in(cx, |editor, window, cx| {
593                if hover_highlights.is_empty() {
594                    editor.clear_background_highlights(HighlightKey::HoverState, cx);
595                } else {
596                    // Highlight the selected symbol using a background highlight
597                    editor.highlight_background(
598                        HighlightKey::HoverState,
599                        &hover_highlights,
600                        |_, theme| theme.colors().element_hover, // todo update theme
601                        cx,
602                    );
603                }
604
605                editor.hover_state.info_popovers = info_popovers;
606                cx.notify();
607                window.refresh();
608            })?;
609
610            anyhow::Ok(())
611        }
612        .log_err()
613        .await
614    });
615
616    editor.hover_state.info_task = Some(task);
617    None
618}
619
620fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
621    editor
622        .hover_state
623        .info_popovers
624        .iter()
625        .any(|InfoPopover { symbol_range, .. }| {
626            symbol_range
627                .as_text_range()
628                .map(|range| {
629                    let hover_range = range.to_offset(&snapshot.buffer_snapshot());
630                    let offset = anchor.to_offset(&snapshot.buffer_snapshot());
631                    // LSP returns a hover result for the end index of ranges that should be hovered, so we need to
632                    // use an inclusive range here to check if we should dismiss the popover
633                    (hover_range.start..=hover_range.end).contains(&offset)
634                })
635                .unwrap_or(false)
636        })
637}
638
639fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
640    editor
641        .hover_state
642        .diagnostic_popover
643        .as_ref()
644        .map(|diagnostic| {
645            let hover_range = diagnostic
646                .local_diagnostic
647                .range
648                .to_offset(&snapshot.buffer_snapshot());
649            let offset = anchor.to_offset(&snapshot.buffer_snapshot());
650
651            // Here we do basically the same as in `same_info_hover`, see comment there for an explanation
652            (hover_range.start..=hover_range.end).contains(&offset)
653        })
654        .unwrap_or(false)
655}
656
657fn parse_blocks(
658    blocks: &[HoverBlock],
659    language_registry: Option<&Arc<LanguageRegistry>>,
660    language: Option<Arc<Language>>,
661    cx: &mut AsyncWindowContext,
662) -> Option<Entity<Markdown>> {
663    let combined_text = blocks
664        .iter()
665        .map(|block| match &block.kind {
666            project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
667                Cow::Borrowed(block.text.trim())
668            }
669            project::HoverBlockKind::Code { language } => {
670                Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
671            }
672        })
673        .join("\n\n");
674
675    cx.new_window_entity(|_window, cx| {
676        Markdown::new(
677            combined_text.into(),
678            language_registry.cloned(),
679            language.map(|language| language.name()),
680            cx,
681        )
682    })
683    .ok()
684}
685
686pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
687    let settings = ThemeSettings::get_global(cx);
688    let ui_font_family = settings.ui_font.family.clone();
689    let ui_font_features = settings.ui_font.features.clone();
690    let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
691    let buffer_font_family = settings.buffer_font.family.clone();
692    let buffer_font_features = settings.buffer_font.features.clone();
693    let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
694    let buffer_font_weight = settings.buffer_font.weight;
695
696    let mut base_text_style = window.text_style();
697    base_text_style.refine(&TextStyleRefinement {
698        font_family: Some(ui_font_family),
699        font_features: Some(ui_font_features),
700        font_fallbacks: ui_font_fallbacks,
701        color: Some(cx.theme().colors().editor_foreground),
702        ..Default::default()
703    });
704    MarkdownStyle {
705        base_text_style,
706        code_block: StyleRefinement::default()
707            .my(rems(1.))
708            .font_buffer(cx)
709            .font_features(buffer_font_features.clone())
710            .font_weight(buffer_font_weight),
711        inline_code: TextStyleRefinement {
712            background_color: Some(cx.theme().colors().background),
713            font_family: Some(buffer_font_family),
714            font_features: Some(buffer_font_features),
715            font_fallbacks: buffer_font_fallbacks,
716            font_weight: Some(buffer_font_weight),
717            ..Default::default()
718        },
719        rule_color: cx.theme().colors().border,
720        block_quote_border_color: Color::Muted.color(cx),
721        block_quote: TextStyleRefinement {
722            color: Some(Color::Muted.color(cx)),
723            ..Default::default()
724        },
725        link: TextStyleRefinement {
726            color: Some(cx.theme().colors().editor_foreground),
727            underline: Some(gpui::UnderlineStyle {
728                thickness: px(1.),
729                color: Some(cx.theme().colors().editor_foreground),
730                wavy: false,
731            }),
732            ..Default::default()
733        },
734        syntax: cx.theme().syntax().clone(),
735        selection_background_color: cx.theme().colors().element_selection_background,
736        heading: StyleRefinement::default()
737            .font_weight(FontWeight::BOLD)
738            .text_base()
739            .mt(rems(1.))
740            .mb_0(),
741        table_columns_min_size: true,
742        soft_break_as_hard_break: true,
743        ..Default::default()
744    }
745}
746
747pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
748    let settings = ThemeSettings::get_global(cx);
749    let ui_font_family = settings.ui_font.family.clone();
750    let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
751    let ui_font_features = settings.ui_font.features.clone();
752    let buffer_font_family = settings.buffer_font.family.clone();
753    let buffer_font_features = settings.buffer_font.features.clone();
754    let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
755
756    let mut base_text_style = window.text_style();
757    base_text_style.refine(&TextStyleRefinement {
758        font_family: Some(ui_font_family),
759        font_features: Some(ui_font_features),
760        font_fallbacks: ui_font_fallbacks,
761        color: Some(cx.theme().colors().editor_foreground),
762        ..Default::default()
763    });
764    MarkdownStyle {
765        base_text_style,
766        code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
767        inline_code: TextStyleRefinement {
768            background_color: Some(cx.theme().colors().editor_background.opacity(0.5)),
769            font_family: Some(buffer_font_family),
770            font_features: Some(buffer_font_features),
771            font_fallbacks: buffer_font_fallbacks,
772            ..Default::default()
773        },
774        rule_color: cx.theme().colors().border,
775        block_quote_border_color: Color::Muted.color(cx),
776        block_quote: TextStyleRefinement {
777            color: Some(Color::Muted.color(cx)),
778            ..Default::default()
779        },
780        link: TextStyleRefinement {
781            color: Some(cx.theme().colors().editor_foreground),
782            underline: Some(gpui::UnderlineStyle {
783                thickness: px(1.),
784                color: Some(cx.theme().colors().editor_foreground),
785                wavy: false,
786            }),
787            ..Default::default()
788        },
789        syntax: cx.theme().syntax().clone(),
790        selection_background_color: cx.theme().colors().element_selection_background,
791        height_is_multiple_of_line_height: true,
792        heading: StyleRefinement::default()
793            .font_weight(FontWeight::BOLD)
794            .text_base()
795            .mb_0(),
796        table_columns_min_size: true,
797        ..Default::default()
798    }
799}
800
801fn parse_file_link(link: &str) -> Option<(PathBuf, Option<String>)> {
802    let uri = Url::parse(link).ok().filter(|uri| uri.scheme() == "file")?;
803    let fragment = uri.fragment().map(ToOwned::to_owned);
804    let path = uri.to_file_path().unwrap_or_else(|_| {
805        let encoded = uri.path();
806
807        urlencoding::decode(encoded)
808            .map(Cow::into_owned)
809            .map(PathBuf::from)
810            .unwrap_or_else(|_| PathBuf::from(encoded))
811    });
812
813    Some((path, fragment))
814}
815
816pub fn open_markdown_url(
817    workspace: Option<Entity<Workspace>>,
818    link: SharedString,
819    window: &mut Window,
820    cx: &mut App,
821) {
822    if let Some((path, fragment)) = parse_file_link(&link)
823        && let Some(workspace) = workspace
824    {
825        workspace.update(cx, |workspace, cx| {
826            let task = workspace.open_abs_path(
827                path,
828                OpenOptions {
829                    visible: Some(OpenVisible::None),
830                    ..Default::default()
831                },
832                window,
833                cx,
834            );
835
836            cx.spawn_in(window, async move |_, cx| {
837                let item = task.await?;
838                // Ruby LSP uses URLs with #L1,1-4,4
839                // we'll just take the first number and assume it's a line number
840                let Some(fragment) = fragment else {
841                    return anyhow::Ok(());
842                };
843                let mut accum = 0u32;
844                for c in fragment.chars() {
845                    if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
846                        accum *= 10;
847                        accum += c as u32 - '0' as u32;
848                    } else if accum > 0 {
849                        break;
850                    }
851                }
852                if accum == 0 {
853                    return Ok(());
854                }
855                let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
856                    return Ok(());
857                };
858                editor.update_in(cx, |editor, window, cx| {
859                    editor.change_selections(Default::default(), window, cx, |selections| {
860                        selections.select_ranges([
861                            text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0)
862                        ]);
863                    });
864                })
865            })
866            .detach_and_log_err(cx);
867        });
868        return;
869    }
870
871    if let Some(workspace) = workspace {
872        workspace.update(cx, |workspace, cx| {
873            workspace.open_url_or_file(&link, None, window, cx);
874        });
875    } else {
876        cx.open_url(&link);
877    }
878}
879
880#[derive(Default)]
881pub struct HoverState {
882    pub info_popovers: Vec<InfoPopover>,
883    pub diagnostic_popover: Option<DiagnosticPopover>,
884    pub info_task: Option<Task<Option<()>>>,
885    pub closest_mouse_distance: Option<Pixels>,
886    pub hiding_delay_task: Option<Task<()>>,
887}
888
889impl HoverState {
890    pub fn visible(&self) -> bool {
891        !self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
892    }
893
894    pub fn is_mouse_getting_closer(&mut self, mouse_position: gpui::Point<Pixels>) -> bool {
895        if !self.visible() {
896            return false;
897        }
898
899        let mut popover_bounds = Vec::new();
900        for info_popover in &self.info_popovers {
901            if let Some(bounds) = info_popover.last_bounds.get() {
902                popover_bounds.push(bounds);
903            }
904        }
905        if let Some(diagnostic_popover) = &self.diagnostic_popover {
906            if let Some(bounds) = diagnostic_popover.last_bounds.get() {
907                popover_bounds.push(bounds);
908            }
909        }
910
911        if popover_bounds.is_empty() {
912            return false;
913        }
914
915        let distance = popover_bounds
916            .iter()
917            .map(|bounds| self.distance_from_point_to_bounds(mouse_position, *bounds))
918            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
919            .unwrap_or(px(f32::MAX));
920
921        if let Some(closest_distance) = self.closest_mouse_distance {
922            if distance > closest_distance + px(4.0) {
923                return false;
924            }
925        }
926
927        self.closest_mouse_distance =
928            Some(distance.min(self.closest_mouse_distance.unwrap_or(distance)));
929        true
930    }
931
932    fn distance_from_point_to_bounds(
933        &self,
934        point: gpui::Point<Pixels>,
935        bounds: Bounds<Pixels>,
936    ) -> Pixels {
937        let center_x = bounds.origin.x + bounds.size.width / 2.;
938        let center_y = bounds.origin.y + bounds.size.height / 2.;
939        let dx: f32 = ((point.x - center_x).abs() - bounds.size.width / 2.)
940            .max(px(0.0))
941            .into();
942        let dy: f32 = ((point.y - center_y).abs() - bounds.size.height / 2.)
943            .max(px(0.0))
944            .into();
945        px((dx.powi(2) + dy.powi(2)).sqrt())
946    }
947
948    pub(crate) fn render(
949        &mut self,
950        snapshot: &EditorSnapshot,
951        visible_rows: Range<DisplayRow>,
952        max_size: Size<Pixels>,
953        text_layout_details: &TextLayoutDetails,
954        window: &mut Window,
955        cx: &mut Context<Editor>,
956    ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
957        if !self.visible() {
958            return None;
959        }
960        // If there is a diagnostic, position the popovers based on that.
961        // Otherwise use the start of the hover range
962        let anchor = self
963            .diagnostic_popover
964            .as_ref()
965            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
966            .or_else(|| {
967                self.info_popovers.iter().find_map(|info_popover| {
968                    match &info_popover.symbol_range {
969                        RangeInEditor::Text(range) => Some(&range.start),
970                        RangeInEditor::Inlay(_) => None,
971                    }
972                })
973            })
974            .or_else(|| {
975                self.info_popovers.iter().find_map(|info_popover| {
976                    match &info_popover.symbol_range {
977                        RangeInEditor::Text(_) => None,
978                        RangeInEditor::Inlay(range) => Some(&range.inlay_position),
979                    }
980                })
981            })?;
982        let mut point = anchor.to_display_point(&snapshot.display_snapshot);
983        // Clamp the point within the visible rows in case the popup source spans multiple lines
984        if visible_rows.end <= point.row() {
985            point = crate::movement::up_by_rows(
986                &snapshot.display_snapshot,
987                point,
988                1 + (point.row() - visible_rows.end).0,
989                text::SelectionGoal::None,
990                true,
991                text_layout_details,
992            )
993            .0;
994        } else if point.row() < visible_rows.start {
995            point = crate::movement::down_by_rows(
996                &snapshot.display_snapshot,
997                point,
998                (visible_rows.start - point.row()).0,
999                text::SelectionGoal::None,
1000                true,
1001                text_layout_details,
1002            )
1003            .0;
1004        }
1005
1006        if !visible_rows.contains(&point.row()) {
1007            log::error!("Hover popover point out of bounds after moving");
1008            return None;
1009        }
1010
1011        let mut elements = Vec::new();
1012
1013        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
1014            elements.push(diagnostic_popover.render(max_size, window, cx));
1015        }
1016        for info_popover in &mut self.info_popovers {
1017            elements.push(info_popover.render(max_size, window, cx));
1018        }
1019
1020        Some((point, elements))
1021    }
1022
1023    pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
1024        let mut hover_popover_is_focused = false;
1025        for info_popover in &self.info_popovers {
1026            if let Some(markdown_view) = &info_popover.parsed_content
1027                && markdown_view.focus_handle(cx).is_focused(window)
1028            {
1029                hover_popover_is_focused = true;
1030            }
1031        }
1032        if let Some(diagnostic_popover) = &self.diagnostic_popover
1033            && diagnostic_popover
1034                .markdown
1035                .focus_handle(cx)
1036                .is_focused(window)
1037        {
1038            hover_popover_is_focused = true;
1039        }
1040        hover_popover_is_focused
1041    }
1042}
1043
1044pub struct InfoPopover {
1045    pub symbol_range: RangeInEditor,
1046    pub parsed_content: Option<Entity<Markdown>>,
1047    pub scroll_handle: ScrollHandle,
1048    pub keyboard_grace: Rc<RefCell<bool>>,
1049    pub anchor: Option<Anchor>,
1050    pub last_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
1051    _subscription: Option<Subscription>,
1052}
1053
1054impl InfoPopover {
1055    pub(crate) fn render(
1056        &mut self,
1057        max_size: Size<Pixels>,
1058        window: &mut Window,
1059        cx: &mut Context<Editor>,
1060    ) -> AnyElement {
1061        let keyboard_grace = Rc::clone(&self.keyboard_grace);
1062        let this = cx.entity().downgrade();
1063        let this2 = this.clone();
1064        let bounds_cell = self.last_bounds.clone();
1065        div()
1066            .id("info_popover")
1067            .occlude()
1068            .elevation_2(cx)
1069            .child(
1070                canvas(
1071                    {
1072                        move |bounds, _window, _cx| {
1073                            bounds_cell.set(Some(bounds));
1074                        }
1075                    },
1076                    |_, _, _, _| {},
1077                )
1078                .absolute()
1079                .size_full(),
1080            )
1081            // Prevent a mouse down/move on the popover from being propagated to the editor,
1082            // because that would dismiss the popover.
1083            .on_mouse_move({
1084                move |_, _, cx: &mut App| {
1085                    this.update(cx, |editor, _| {
1086                        editor.hover_state.closest_mouse_distance = Some(px(0.0));
1087                        editor.hover_state.hiding_delay_task = None;
1088                    })
1089                    .ok();
1090                    cx.stop_propagation()
1091                }
1092            })
1093            .on_mouse_down(MouseButton::Left, move |_, _, cx| {
1094                let mut keyboard_grace = keyboard_grace.borrow_mut();
1095                *keyboard_grace = false;
1096                cx.stop_propagation();
1097            })
1098            .when_some(self.parsed_content.clone(), |this, markdown| {
1099                this.child(
1100                    div()
1101                        .id("info-md-container")
1102                        .overflow_y_scroll()
1103                        .max_w(max_size.width)
1104                        .max_h(max_size.height)
1105                        .track_scroll(&self.scroll_handle)
1106                        .child(
1107                            MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1108                                .scroll_handle(self.scroll_handle.clone())
1109                                .code_block_renderer(markdown::CodeBlockRenderer::Default {
1110                                    copy_button_visibility: CopyButtonVisibility::Hidden,
1111                                    wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
1112                                    border: false,
1113                                })
1114                                .on_url_click(move |link, window, cx| {
1115                                    open_markdown_url(
1116                                        this2
1117                                            .read_with(cx, |editor, _| editor.workspace())
1118                                            .ok()
1119                                            .flatten(),
1120                                        link,
1121                                        window,
1122                                        cx,
1123                                    )
1124                                })
1125                                .p_2(),
1126                        ),
1127                )
1128                .custom_scrollbars(
1129                    Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1130                        .tracked_scroll_handle(&self.scroll_handle),
1131                    window,
1132                    cx,
1133                )
1134            })
1135            .into_any_element()
1136    }
1137
1138    pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
1139        let mut current = self.scroll_handle.offset();
1140        current.y -= amount.pixels(
1141            window.line_height(),
1142            self.scroll_handle.bounds().size.height - px(16.),
1143        ) / 2.0;
1144        cx.notify();
1145        self.scroll_handle.set_offset(current);
1146    }
1147}
1148
1149pub struct DiagnosticPopover {
1150    pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
1151    markdown: Entity<Markdown>,
1152    border_color: Hsla,
1153    background_color: Hsla,
1154    pub keyboard_grace: Rc<RefCell<bool>>,
1155    pub anchor: Anchor,
1156    pub last_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
1157    _subscription: Subscription,
1158    pub scroll_handle: ScrollHandle,
1159}
1160
1161impl DiagnosticPopover {
1162    pub fn render(
1163        &self,
1164        max_size: Size<Pixels>,
1165        window: &mut Window,
1166        cx: &mut Context<Editor>,
1167    ) -> AnyElement {
1168        let keyboard_grace = Rc::clone(&self.keyboard_grace);
1169        let this = cx.entity().downgrade();
1170        let bounds_cell = self.last_bounds.clone();
1171        div()
1172            .id("diagnostic")
1173            .occlude()
1174            .elevation_2_borderless(cx)
1175            .child(
1176                canvas(
1177                    {
1178                        move |bounds, _window, _cx| {
1179                            bounds_cell.set(Some(bounds));
1180                        }
1181                    },
1182                    |_, _, _, _| {},
1183                )
1184                .absolute()
1185                .size_full(),
1186            )
1187            // Don't draw the background color if the theme
1188            // allows transparent surfaces.
1189            .when(theme_is_transparent(cx), |this| {
1190                this.bg(gpui::transparent_black())
1191            })
1192            // Prevent a mouse move on the popover from being propagated to the editor,
1193            // because that would dismiss the popover.
1194            .on_mouse_move({
1195                let this = this.clone();
1196                move |_, _, cx: &mut App| {
1197                    this.update(cx, |editor, _| {
1198                        editor.hover_state.closest_mouse_distance = Some(px(0.0));
1199                        editor.hover_state.hiding_delay_task = None;
1200                    })
1201                    .ok();
1202                    cx.stop_propagation()
1203                }
1204            })
1205            // Prevent a mouse down on the popover from being propagated to the editor,
1206            // because that would move the cursor.
1207            .on_mouse_down(MouseButton::Left, move |_, _, cx| {
1208                let mut keyboard_grace = keyboard_grace.borrow_mut();
1209                *keyboard_grace = false;
1210                cx.stop_propagation();
1211            })
1212            .child(
1213                div()
1214                    .relative()
1215                    .py_1()
1216                    .pl_2()
1217                    .pr_8()
1218                    .bg(self.background_color)
1219                    .border_1()
1220                    .border_color(self.border_color)
1221                    .rounded_lg()
1222                    .child(
1223                        div()
1224                            .id("diagnostic-content-container")
1225                            .max_w(max_size.width)
1226                            .max_h(max_size.height)
1227                            .overflow_y_scroll()
1228                            .track_scroll(&self.scroll_handle)
1229                            .child(
1230                                MarkdownElement::new(
1231                                    self.markdown.clone(),
1232                                    diagnostics_markdown_style(window, cx),
1233                                )
1234                                .code_block_renderer(markdown::CodeBlockRenderer::Default {
1235                                    copy_button_visibility: CopyButtonVisibility::Hidden,
1236                                    wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
1237                                    border: false,
1238                                })
1239                                .on_url_click(
1240                                    move |link, window, cx| {
1241                                        if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
1242                                        {
1243                                            this.update(cx, |this, cx| {
1244                                                renderer.as_ref().open_link(this, link, window, cx);
1245                                            })
1246                                            .ok();
1247                                        }
1248                                    },
1249                                ),
1250                            ),
1251                    )
1252                    .child(div().absolute().top_1().right_1().child({
1253                        let message = self.local_diagnostic.diagnostic.message.clone();
1254                        CopyButton::new("copy-diagnostic", message).tooltip_label("Copy Diagnostic")
1255                    }))
1256                    .custom_scrollbars(
1257                        Scrollbars::for_settings::<EditorSettingsScrollbarProxy>()
1258                            .tracked_scroll_handle(&self.scroll_handle),
1259                        window,
1260                        cx,
1261                    ),
1262            )
1263            .into_any_element()
1264    }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::*;
1270    use crate::{
1271        PointForPosition,
1272        actions::ConfirmCompletion,
1273        editor_tests::{handle_completion_request, init_test},
1274        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
1275        test::editor_lsp_test_context::EditorLspTestContext,
1276    };
1277    use collections::BTreeSet;
1278    use futures::stream::StreamExt;
1279    use gpui::App;
1280    use indoc::indoc;
1281    use markdown::parser::MarkdownEvent;
1282    use project::InlayId;
1283    use settings::InlayHintSettingsContent;
1284    use settings::{DelayMs, SettingsStore};
1285    use std::sync::atomic;
1286    use std::sync::atomic::AtomicUsize;
1287    use text::Bias;
1288
1289    fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
1290        cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 })
1291    }
1292
1293    #[gpui::test]
1294    fn test_hover_markdown_preserves_soft_breaks(cx: &mut gpui::TestAppContext) {
1295        init_test(cx, |_| {});
1296
1297        let cx = cx.add_empty_window();
1298        let text = concat!(
1299            "class super(object)\n",
1300            "|  super(type) -> unbound super object\n",
1301            "|  super(type, obj) -> bound super object"
1302        );
1303        let markdown = cx.new(|cx| Markdown::new(text.into(), None, None, cx));
1304        cx.run_until_parked();
1305
1306        let rendered = MarkdownElement::rendered_text(markdown, cx, hover_markdown_style);
1307
1308        // The two soft breaks must render as real newline characters rather
1309        // than being collapsed into spaces.
1310        assert_eq!(
1311            rendered.matches('\n').count(),
1312            2,
1313            "expected two hard line breaks, got {rendered:?}"
1314        );
1315        let lines: Vec<&str> = rendered.split('\n').collect();
1316        assert_eq!(
1317            lines,
1318            [
1319                "class super(object)",
1320                "|  super(type) -> unbound super object",
1321                "|  super(type, obj) -> bound super object",
1322            ]
1323        );
1324        // The two spaces after each `|` continuation marker are preserved verbatim.
1325        assert!(lines[1].starts_with("|  super"));
1326        assert!(lines[2].starts_with("|  super"));
1327        // No tabs are introduced anywhere in the rendered output.
1328        assert!(!rendered.contains('\t'));
1329        // And the full rendering matches the source exactly.
1330        assert_eq!(rendered, text);
1331    }
1332
1333    impl InfoPopover {
1334        fn get_rendered_text(&self, cx: &gpui::App) -> String {
1335            let mut rendered_text = String::new();
1336            if let Some(parsed_content) = self.parsed_content.clone() {
1337                let markdown = parsed_content.read(cx);
1338                let text = markdown.parsed_markdown().source().to_string();
1339                let data = markdown.parsed_markdown().events();
1340                let slice = data;
1341
1342                for (range, event) in slice.iter() {
1343                    match event {
1344                        MarkdownEvent::SubstitutedText(parsed) => {
1345                            rendered_text.push_str(parsed.as_str())
1346                        }
1347                        MarkdownEvent::Text | MarkdownEvent::Code => {
1348                            rendered_text.push_str(&text[range.clone()])
1349                        }
1350                        _ => {}
1351                    }
1352                }
1353            }
1354            rendered_text
1355        }
1356    }
1357
1358    #[gpui::test]
1359    async fn test_mouse_hover_info_popover_with_autocomplete_popover(
1360        cx: &mut gpui::TestAppContext,
1361    ) {
1362        init_test(cx, |_| {});
1363
1364        let mut cx = EditorLspTestContext::new_rust(
1365            lsp::ServerCapabilities {
1366                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1367                completion_provider: Some(lsp::CompletionOptions {
1368                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
1369                    resolve_provider: Some(true),
1370                    ..Default::default()
1371                }),
1372                ..Default::default()
1373            },
1374            cx,
1375        )
1376        .await;
1377        let counter = Arc::new(AtomicUsize::new(0));
1378        // Basic hover delays and then pops without moving the mouse
1379        cx.set_state(indoc! {"
1380                oneˇ
1381                two
1382                three
1383                fn test() { println!(); }
1384            "});
1385
1386        //prompt autocompletion menu
1387        cx.simulate_keystroke(".");
1388        handle_completion_request(
1389            indoc! {"
1390                        one.|<>
1391                        two
1392                        three
1393                    "},
1394            vec!["first_completion", "second_completion"],
1395            true,
1396            counter.clone(),
1397            &mut cx,
1398        )
1399        .await;
1400        cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
1401            .await;
1402        assert_eq!(counter.load(atomic::Ordering::Acquire), 1); // 1 completion request
1403
1404        let hover_point = cx.display_point(indoc! {"
1405                one.
1406                two
1407                three
1408                fn test() { printˇln!(); }
1409            "});
1410        cx.update_editor(|editor, window, cx| {
1411            let snapshot = editor.snapshot(window, cx);
1412            let anchor = snapshot
1413                .buffer_snapshot()
1414                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1415            hover_at(editor, Some(anchor), None, window, cx)
1416        });
1417        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1418
1419        // After delay, hover should be visible.
1420        let symbol_range = cx.lsp_range(indoc! {"
1421                one.
1422                two
1423                three
1424                fn test() { «println!»(); }
1425            "});
1426        let mut requests =
1427            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1428                Ok(Some(lsp::Hover {
1429                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1430                        kind: lsp::MarkupKind::Markdown,
1431                        value: "some basic docs".to_string(),
1432                    }),
1433                    range: Some(symbol_range),
1434                }))
1435            });
1436        cx.background_executor
1437            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1438        requests.next().await;
1439
1440        cx.editor(|editor, _window, cx| {
1441            assert!(editor.hover_state.visible());
1442            assert_eq!(
1443                editor.hover_state.info_popovers.len(),
1444                1,
1445                "Expected exactly one hover but got: {:?}",
1446                editor.hover_state.info_popovers.len()
1447            );
1448            let rendered_text = editor
1449                .hover_state
1450                .info_popovers
1451                .first()
1452                .unwrap()
1453                .get_rendered_text(cx);
1454            assert_eq!(rendered_text, "some basic docs".to_string())
1455        });
1456
1457        // check that the completion menu is still visible and that there still has only been 1 completion request
1458        cx.editor(|editor, _, _| assert!(editor.context_menu_visible()));
1459        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1460
1461        //apply a completion and check it was successfully applied
1462        let _apply_additional_edits = cx.update_editor(|editor, window, cx| {
1463            editor.context_menu_next(&Default::default(), window, cx);
1464            editor
1465                .confirm_completion(&ConfirmCompletion::default(), window, cx)
1466                .unwrap()
1467        });
1468        cx.assert_editor_state(indoc! {"
1469            one.second_completionˇ
1470            two
1471            three
1472            fn test() { println!(); }
1473        "});
1474
1475        // check that the completion menu is no longer visible and that there still has only been 1 completion request
1476        cx.editor(|editor, _, _| assert!(!editor.context_menu_visible()));
1477        assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
1478
1479        //verify the information popover is still visible and unchanged
1480        cx.editor(|editor, _, cx| {
1481            assert!(editor.hover_state.visible());
1482            assert_eq!(
1483                editor.hover_state.info_popovers.len(),
1484                1,
1485                "Expected exactly one hover but got: {:?}",
1486                editor.hover_state.info_popovers.len()
1487            );
1488            let rendered_text = editor
1489                .hover_state
1490                .info_popovers
1491                .first()
1492                .unwrap()
1493                .get_rendered_text(cx);
1494
1495            assert_eq!(rendered_text, "some basic docs".to_string())
1496        });
1497
1498        // Mouse moved with no hover response dismisses
1499        let hover_point = cx.display_point(indoc! {"
1500                one.second_completionˇ
1501                two
1502                three
1503                fn teˇst() { println!(); }
1504            "});
1505        let mut request = cx
1506            .lsp
1507            .set_request_handler::<lsp::request::HoverRequest, _, _>(
1508                |_, _| async move { Ok(None) },
1509            );
1510        cx.update_editor(|editor, window, cx| {
1511            let snapshot = editor.snapshot(window, cx);
1512            let anchor = snapshot
1513                .buffer_snapshot()
1514                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1515            hover_at(editor, Some(anchor), None, window, cx)
1516        });
1517        cx.background_executor
1518            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1519        request.next().await;
1520
1521        // verify that the information popover is no longer visible
1522        cx.editor(|editor, _, _| {
1523            assert!(!editor.hover_state.visible());
1524        });
1525    }
1526
1527    #[gpui::test]
1528    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
1529        init_test(cx, |_| {});
1530
1531        let mut cx = EditorLspTestContext::new_rust(
1532            lsp::ServerCapabilities {
1533                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1534                ..Default::default()
1535            },
1536            cx,
1537        )
1538        .await;
1539
1540        // Basic hover delays and then pops without moving the mouse
1541        cx.set_state(indoc! {"
1542            fn ˇtest() { println!(); }
1543        "});
1544        let hover_point = cx.display_point(indoc! {"
1545            fn test() { printˇln!(); }
1546        "});
1547
1548        cx.update_editor(|editor, window, cx| {
1549            let snapshot = editor.snapshot(window, cx);
1550            let anchor = snapshot
1551                .buffer_snapshot()
1552                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1553            hover_at(editor, Some(anchor), None, window, cx)
1554        });
1555        assert!(!cx.editor(|editor, _window, _cx| editor.hover_state.visible()));
1556
1557        // After delay, hover should be visible.
1558        let symbol_range = cx.lsp_range(indoc! {"
1559            fn test() { «println!»(); }
1560        "});
1561        let mut requests =
1562            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1563                Ok(Some(lsp::Hover {
1564                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1565                        kind: lsp::MarkupKind::Markdown,
1566                        value: "some basic docs".to_string(),
1567                    }),
1568                    range: Some(symbol_range),
1569                }))
1570            });
1571        cx.background_executor
1572            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1573        requests.next().await;
1574
1575        cx.editor(|editor, _, cx| {
1576            assert!(editor.hover_state.visible());
1577            assert_eq!(
1578                editor.hover_state.info_popovers.len(),
1579                1,
1580                "Expected exactly one hover but got: {:?}",
1581                editor.hover_state.info_popovers.len()
1582            );
1583            let rendered_text = editor
1584                .hover_state
1585                .info_popovers
1586                .first()
1587                .unwrap()
1588                .get_rendered_text(cx);
1589
1590            assert_eq!(rendered_text, "some basic docs".to_string())
1591        });
1592
1593        // Mouse moved with no hover response dismisses
1594        let hover_point = cx.display_point(indoc! {"
1595            fn teˇst() { println!(); }
1596        "});
1597        let mut request = cx
1598            .lsp
1599            .set_request_handler::<lsp::request::HoverRequest, _, _>(
1600                |_, _| async move { Ok(None) },
1601            );
1602        cx.update_editor(|editor, window, cx| {
1603            let snapshot = editor.snapshot(window, cx);
1604            let anchor = snapshot
1605                .buffer_snapshot()
1606                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1607            hover_at(editor, Some(anchor), None, window, cx)
1608        });
1609        cx.background_executor
1610            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1611        request.next().await;
1612        cx.editor(|editor, _, _| {
1613            assert!(!editor.hover_state.visible());
1614        });
1615    }
1616
1617    #[gpui::test]
1618    async fn test_mouse_hover_cancelled_before_delay(cx: &mut gpui::TestAppContext) {
1619        init_test(cx, |_| {});
1620
1621        let mut cx = EditorLspTestContext::new_rust(
1622            lsp::ServerCapabilities {
1623                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1624                ..Default::default()
1625            },
1626            cx,
1627        )
1628        .await;
1629
1630        cx.set_state(indoc! {"
1631            fn ˇtest() { println!(); }
1632        "});
1633        let hover_point = cx.display_point(indoc! {"
1634            fn test() { printˇln!(); }
1635        "});
1636
1637        cx.update_editor(|editor, window, cx| {
1638            let snapshot = editor.snapshot(window, cx);
1639            let anchor = snapshot
1640                .buffer_snapshot()
1641                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
1642            hover_at(editor, Some(anchor), None, window, cx);
1643            hover_at(editor, None, None, window, cx);
1644        });
1645
1646        let request_count = Arc::new(AtomicUsize::new(0));
1647        cx.set_request_handler::<lsp::request::HoverRequest, _, _>({
1648            let request_count = request_count.clone();
1649            move |_, _, _| {
1650                let request_count = request_count.clone();
1651                async move {
1652                    request_count.fetch_add(1, atomic::Ordering::Release);
1653                    Ok(Some(lsp::Hover {
1654                        contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1655                            kind: lsp::MarkupKind::Markdown,
1656                            value: "some basic docs".to_string(),
1657                        }),
1658                        range: None,
1659                    }))
1660                }
1661            }
1662        });
1663
1664        cx.background_executor
1665            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
1666        cx.background_executor.run_until_parked();
1667        cx.run_until_parked();
1668
1669        assert_eq!(request_count.load(atomic::Ordering::Acquire), 0);
1670        cx.editor(|editor, _, _| {
1671            assert!(!editor.hover_state.visible());
1672        });
1673    }
1674
1675    #[gpui::test]
1676    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
1677        init_test(cx, |_| {});
1678
1679        let mut cx = EditorLspTestContext::new_rust(
1680            lsp::ServerCapabilities {
1681                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1682                ..Default::default()
1683            },
1684            cx,
1685        )
1686        .await;
1687
1688        // Hover with keyboard has no delay
1689        cx.set_state(indoc! {"
1690            fˇn test() { println!(); }
1691        "});
1692        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1693        let symbol_range = cx.lsp_range(indoc! {"
1694            «fn» test() { println!(); }
1695        "});
1696
1697        cx.editor(|editor, _window, _cx| {
1698            assert!(!editor.hover_state.visible());
1699
1700            assert_eq!(
1701                editor.hover_state.info_popovers.len(),
1702                0,
1703                "Expected no hovers but got but got: {:?}",
1704                editor.hover_state.info_popovers.len()
1705            );
1706        });
1707
1708        let mut requests =
1709            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1710                Ok(Some(lsp::Hover {
1711                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1712                        kind: lsp::MarkupKind::Markdown,
1713                        value: "some other basic docs".to_string(),
1714                    }),
1715                    range: Some(symbol_range),
1716                }))
1717            });
1718
1719        requests.next().await;
1720        cx.dispatch_action(Hover);
1721
1722        cx.condition(|editor, _| editor.hover_state.visible()).await;
1723        cx.editor(|editor, _, cx| {
1724            assert_eq!(
1725                editor.hover_state.info_popovers.len(),
1726                1,
1727                "Expected exactly one hover but got: {:?}",
1728                editor.hover_state.info_popovers.len()
1729            );
1730
1731            let rendered_text = editor
1732                .hover_state
1733                .info_popovers
1734                .first()
1735                .unwrap()
1736                .get_rendered_text(cx);
1737
1738            assert_eq!(rendered_text, "some other basic docs".to_string())
1739        });
1740    }
1741
1742    #[gpui::test]
1743    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
1744        init_test(cx, |_| {});
1745
1746        let mut cx = EditorLspTestContext::new_rust(
1747            lsp::ServerCapabilities {
1748                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1749                ..Default::default()
1750            },
1751            cx,
1752        )
1753        .await;
1754
1755        // Hover with keyboard has no delay
1756        cx.set_state(indoc! {"
1757            fˇn test() { println!(); }
1758        "});
1759        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1760        let symbol_range = cx.lsp_range(indoc! {"
1761            «fn» test() { println!(); }
1762        "});
1763        cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1764            Ok(Some(lsp::Hover {
1765                contents: lsp::HoverContents::Array(vec![
1766                    lsp::MarkedString::String("regular text for hover to show".to_string()),
1767                    lsp::MarkedString::String("".to_string()),
1768                    lsp::MarkedString::LanguageString(lsp::LanguageString {
1769                        language: "Rust".to_string(),
1770                        value: "".to_string(),
1771                    }),
1772                ]),
1773                range: Some(symbol_range),
1774            }))
1775        })
1776        .next()
1777        .await;
1778        cx.dispatch_action(Hover);
1779
1780        cx.condition(|editor, _| editor.hover_state.visible()).await;
1781        cx.editor(|editor, _, cx| {
1782            assert_eq!(
1783                editor.hover_state.info_popovers.len(),
1784                1,
1785                "Expected exactly one hover but got: {:?}",
1786                editor.hover_state.info_popovers.len()
1787            );
1788            let rendered_text = editor
1789                .hover_state
1790                .info_popovers
1791                .first()
1792                .unwrap()
1793                .get_rendered_text(cx);
1794
1795            assert_eq!(
1796                rendered_text,
1797                "regular text for hover to show".to_string(),
1798                "No empty string hovers should be shown"
1799            );
1800        });
1801    }
1802
1803    #[gpui::test]
1804    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
1805        init_test(cx, |_| {});
1806
1807        let mut cx = EditorLspTestContext::new_rust(
1808            lsp::ServerCapabilities {
1809                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1810                ..Default::default()
1811            },
1812            cx,
1813        )
1814        .await;
1815
1816        // Hover with keyboard has no delay
1817        cx.set_state(indoc! {"
1818            fˇn test() { println!(); }
1819        "});
1820        cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx));
1821        let symbol_range = cx.lsp_range(indoc! {"
1822            «fn» test() { println!(); }
1823        "});
1824
1825        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
1826        let markdown_string = format!("\n```rust\n{code_str}```");
1827
1828        let closure_markdown_string = markdown_string.clone();
1829        cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
1830            let future_markdown_string = closure_markdown_string.clone();
1831            async move {
1832                Ok(Some(lsp::Hover {
1833                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1834                        kind: lsp::MarkupKind::Markdown,
1835                        value: future_markdown_string,
1836                    }),
1837                    range: Some(symbol_range),
1838                }))
1839            }
1840        })
1841        .next()
1842        .await;
1843
1844        cx.dispatch_action(Hover);
1845
1846        cx.condition(|editor, _| editor.hover_state.visible()).await;
1847        cx.editor(|editor, _, cx| {
1848            assert_eq!(
1849                editor.hover_state.info_popovers.len(),
1850                1,
1851                "Expected exactly one hover but got: {:?}",
1852                editor.hover_state.info_popovers.len()
1853            );
1854            let rendered_text = editor
1855                .hover_state
1856                .info_popovers
1857                .first()
1858                .unwrap()
1859                .get_rendered_text(cx);
1860
1861            assert_eq!(
1862                rendered_text, code_str,
1863                "Should not have extra line breaks at end of rendered hover"
1864            );
1865        });
1866    }
1867
1868    #[gpui::test]
1869    // https://github.com/zed-industries/zed/issues/15498
1870    async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
1871        init_test(cx, |_| {});
1872
1873        let mut cx = EditorLspTestContext::new_rust(
1874            lsp::ServerCapabilities {
1875                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1876                ..Default::default()
1877            },
1878            cx,
1879        )
1880        .await;
1881
1882        cx.set_state(indoc! {"
1883            fn fuˇnc(abc def: i32) -> u32 {
1884            }
1885        "});
1886
1887        cx.lsp
1888            .set_request_handler::<lsp::request::HoverRequest, _, _>({
1889                |_, _| async move {
1890                    Ok(Some(lsp::Hover {
1891                        contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1892                            kind: lsp::MarkupKind::Markdown,
1893                            value: indoc!(
1894                                r#"
1895                    ### function `errands_data_read`
1896
1897                    ---
1898                    → `char *`
1899                    Function to read a file into a string
1900
1901                    ---
1902                    ```cpp
1903                    static char *errands_data_read()
1904                    ```
1905                    "#
1906                            )
1907                            .to_string(),
1908                        }),
1909                        range: None,
1910                    }))
1911                }
1912            });
1913        cx.update_editor(|editor, window, cx| hover(editor, &Default::default(), window, cx));
1914        cx.run_until_parked();
1915
1916        cx.update_editor(|editor, _, cx| {
1917            let popover = editor.hover_state.info_popovers.first().unwrap();
1918            let content = popover.get_rendered_text(cx);
1919
1920            assert!(content.contains("Function to read a file"));
1921        });
1922    }
1923
1924    #[gpui::test]
1925    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1926        init_test(cx, |settings| {
1927            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1928                show_value_hints: Some(true),
1929                enabled: Some(true),
1930                edit_debounce_ms: Some(0),
1931                scroll_debounce_ms: Some(0),
1932                show_type_hints: Some(true),
1933                show_parameter_hints: Some(true),
1934                show_other_hints: Some(true),
1935                show_background: Some(false),
1936                toggle_on_modifiers_press: None,
1937            })
1938        });
1939
1940        let mut cx = EditorLspTestContext::new_rust(
1941            lsp::ServerCapabilities {
1942                inlay_hint_provider: Some(lsp::OneOf::Right(
1943                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1944                        resolve_provider: Some(true),
1945                        ..Default::default()
1946                    }),
1947                )),
1948                ..Default::default()
1949            },
1950            cx,
1951        )
1952        .await;
1953
1954        cx.set_state(indoc! {"
1955            struct TestStruct;
1956
1957            // ==================
1958
1959            struct TestNewType<T>(T);
1960
1961            fn main() {
1962                let variableˇ = TestNewType(TestStruct);
1963            }
1964        "});
1965
1966        let hint_start_offset = cx.ranges(indoc! {"
1967            struct TestStruct;
1968
1969            // ==================
1970
1971            struct TestNewType<T>(T);
1972
1973            fn main() {
1974                let variableˇ = TestNewType(TestStruct);
1975            }
1976        "})[0]
1977            .start;
1978        let hint_position = cx.to_lsp(MultiBufferOffset(hint_start_offset));
1979        let new_type_target_range = cx.lsp_range(indoc! {"
1980            struct TestStruct;
1981
1982            // ==================
1983
1984            struct «TestNewType»<T>(T);
1985
1986            fn main() {
1987                let variable = TestNewType(TestStruct);
1988            }
1989        "});
1990        let struct_target_range = cx.lsp_range(indoc! {"
1991            struct «TestStruct»;
1992
1993            // ==================
1994
1995            struct TestNewType<T>(T);
1996
1997            fn main() {
1998                let variable = TestNewType(TestStruct);
1999            }
2000        "});
2001
2002        let uri = cx.buffer_lsp_url.clone();
2003        let new_type_label = "TestNewType";
2004        let struct_label = "TestStruct";
2005        let entire_hint_label = ": TestNewType<TestStruct>";
2006        let closure_uri = uri.clone();
2007        cx.lsp
2008            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2009                let task_uri = closure_uri.clone();
2010                async move {
2011                    assert_eq!(params.text_document.uri, task_uri);
2012                    Ok(Some(vec![lsp::InlayHint {
2013                        position: hint_position,
2014                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
2015                            value: entire_hint_label.to_string(),
2016                            ..Default::default()
2017                        }]),
2018                        kind: Some(lsp::InlayHintKind::TYPE),
2019                        text_edits: None,
2020                        tooltip: None,
2021                        padding_left: Some(false),
2022                        padding_right: Some(false),
2023                        data: None,
2024                    }]))
2025                }
2026            })
2027            .next()
2028            .await;
2029        cx.background_executor.run_until_parked();
2030        cx.update_editor(|editor, _, cx| {
2031            let expected_layers = vec![entire_hint_label.to_string()];
2032            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
2033            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2034        });
2035
2036        let inlay_range = cx
2037            .ranges(indoc! {"
2038                struct TestStruct;
2039
2040                // ==================
2041
2042                struct TestNewType<T>(T);
2043
2044                fn main() {
2045                    let variable« »= TestNewType(TestStruct);
2046                }
2047        "})
2048            .first()
2049            .cloned()
2050            .unwrap();
2051        let new_type_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
2052            let snapshot = editor.snapshot(window, cx);
2053            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
2054            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
2055            assert_eq!(previous_valid.row(), next_valid.row());
2056            assert!(previous_valid.column() < next_valid.column());
2057            let exact_unclipped = DisplayPoint::new(
2058                previous_valid.row(),
2059                previous_valid.column()
2060                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
2061                        as u32,
2062            );
2063            PointForPosition {
2064                previous_valid,
2065                next_valid,
2066                nearest_valid: previous_valid,
2067                exact_unclipped,
2068                column_overshoot_after_line_end: 0,
2069            }
2070        });
2071        cx.update_editor(|editor, window, cx| {
2072            editor.update_inlay_link_and_hover_points(
2073                &editor.snapshot(window, cx),
2074                new_type_hint_part_hover_position,
2075                None,
2076                true,
2077                false,
2078                window,
2079                cx,
2080            );
2081        });
2082
2083        let resolve_closure_uri = uri.clone();
2084        cx.lsp
2085            .set_request_handler::<lsp::request::InlayHintResolveRequest, _, _>(
2086                move |mut hint_to_resolve, _| {
2087                    let mut resolved_hint_positions = BTreeSet::new();
2088                    let task_uri = resolve_closure_uri.clone();
2089                    async move {
2090                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
2091                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
2092
2093                        // `: TestNewType<TestStruct>`
2094                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
2095                            lsp::InlayHintLabelPart {
2096                                value: ": ".to_string(),
2097                                ..Default::default()
2098                            },
2099                            lsp::InlayHintLabelPart {
2100                                value: new_type_label.to_string(),
2101                                location: Some(lsp::Location {
2102                                    uri: task_uri.clone(),
2103                                    range: new_type_target_range,
2104                                }),
2105                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
2106                                    "A tooltip for `{new_type_label}`"
2107                                ))),
2108                                ..Default::default()
2109                            },
2110                            lsp::InlayHintLabelPart {
2111                                value: "<".to_string(),
2112                                ..Default::default()
2113                            },
2114                            lsp::InlayHintLabelPart {
2115                                value: struct_label.to_string(),
2116                                location: Some(lsp::Location {
2117                                    uri: task_uri,
2118                                    range: struct_target_range,
2119                                }),
2120                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
2121                                    lsp::MarkupContent {
2122                                        kind: lsp::MarkupKind::Markdown,
2123                                        value: format!("A tooltip for `{struct_label}`"),
2124                                    },
2125                                )),
2126                                ..Default::default()
2127                            },
2128                            lsp::InlayHintLabelPart {
2129                                value: ">".to_string(),
2130                                ..Default::default()
2131                            },
2132                        ]);
2133
2134                        Ok(hint_to_resolve)
2135                    }
2136                },
2137            )
2138            .next()
2139            .await;
2140        cx.background_executor.run_until_parked();
2141
2142        cx.update_editor(|editor, window, cx| {
2143            editor.update_inlay_link_and_hover_points(
2144                &editor.snapshot(window, cx),
2145                new_type_hint_part_hover_position,
2146                None,
2147                true,
2148                false,
2149                window,
2150                cx,
2151            );
2152        });
2153        cx.background_executor
2154            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2155        cx.background_executor.run_until_parked();
2156        cx.update_editor(|editor, _, cx| {
2157            let hover_state = &editor.hover_state;
2158            assert!(
2159                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2160            );
2161            let popover = hover_state.info_popovers.first().unwrap();
2162            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2163            assert_eq!(
2164                popover.symbol_range,
2165                RangeInEditor::Inlay(InlayHighlight {
2166                    inlay: InlayId::Hint(0),
2167                    inlay_position: buffer_snapshot
2168                        .anchor_after(MultiBufferOffset(inlay_range.start)),
2169                    range: ": ".len()..": ".len() + new_type_label.len(),
2170                }),
2171                "Popover range should match the new type label part"
2172            );
2173            assert_eq!(
2174                popover.get_rendered_text(cx),
2175                format!("A tooltip for {new_type_label}"),
2176            );
2177        });
2178
2179        let struct_hint_part_hover_position = cx.update_editor(|editor, window, cx| {
2180            let snapshot = editor.snapshot(window, cx);
2181            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
2182            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
2183            assert_eq!(previous_valid.row(), next_valid.row());
2184            assert!(previous_valid.column() < next_valid.column());
2185            let exact_unclipped = DisplayPoint::new(
2186                previous_valid.row(),
2187                previous_valid.column()
2188                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
2189                        as u32,
2190            );
2191            PointForPosition {
2192                previous_valid,
2193                next_valid,
2194                nearest_valid: previous_valid,
2195                exact_unclipped,
2196                column_overshoot_after_line_end: 0,
2197            }
2198        });
2199        cx.update_editor(|editor, window, cx| {
2200            editor.update_inlay_link_and_hover_points(
2201                &editor.snapshot(window, cx),
2202                struct_hint_part_hover_position,
2203                None,
2204                true,
2205                false,
2206                window,
2207                cx,
2208            );
2209        });
2210        cx.background_executor
2211            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2212        cx.background_executor.run_until_parked();
2213        cx.update_editor(|editor, _, cx| {
2214            let hover_state = &editor.hover_state;
2215            assert!(
2216                hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
2217            );
2218            let popover = hover_state.info_popovers.first().unwrap();
2219            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
2220            assert_eq!(
2221                popover.symbol_range,
2222                RangeInEditor::Inlay(InlayHighlight {
2223                    inlay: InlayId::Hint(0),
2224                    inlay_position: buffer_snapshot
2225                        .anchor_after(MultiBufferOffset(inlay_range.start)),
2226                    range: ": ".len() + new_type_label.len() + "<".len()
2227                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
2228                }),
2229                "Popover range should match the struct label part"
2230            );
2231            assert_eq!(
2232                popover.get_rendered_text(cx),
2233                format!("A tooltip for {struct_label}"),
2234                "Rendered markdown element should remove backticks from text"
2235            );
2236        });
2237    }
2238
2239    #[test]
2240    fn test_find_hovered_hint_part_with_multibyte_characters() {
2241        use crate::display_map::InlayOffset;
2242        use multi_buffer::MultiBufferOffset;
2243        use project::InlayHintLabelPart;
2244
2245        // Test with multi-byte UTF-8 character "→" (3 bytes, 1 character)
2246        let label = "→ app/Livewire/UserProfile.php";
2247        let label_parts = vec![InlayHintLabelPart {
2248            value: label.to_string(),
2249            tooltip: None,
2250            location: None,
2251        }];
2252
2253        let hint_start = InlayOffset(MultiBufferOffset(100));
2254
2255        // Verify the label has more bytes than characters (due to "→")
2256        assert_eq!(label.len(), 32); // bytes
2257        assert_eq!(label.chars().count(), 30); // characters
2258
2259        // Test hovering at the last byte (should find the part)
2260        let last_byte_offset = InlayOffset(MultiBufferOffset(100 + label.len() - 1));
2261        let result = find_hovered_hint_part(label_parts.clone(), hint_start, last_byte_offset);
2262        assert!(
2263            result.is_some(),
2264            "Should find part when hovering at last byte"
2265        );
2266        let (part, range) = result.unwrap();
2267        assert_eq!(part.value, label);
2268        assert_eq!(range.start, hint_start);
2269        assert_eq!(range.end, InlayOffset(MultiBufferOffset(100 + label.len())));
2270
2271        // Test hovering at the first byte of "→" (byte 0)
2272        let first_byte_offset = InlayOffset(MultiBufferOffset(100));
2273        let result = find_hovered_hint_part(label_parts.clone(), hint_start, first_byte_offset);
2274        assert!(
2275            result.is_some(),
2276            "Should find part when hovering at first byte"
2277        );
2278
2279        // Test hovering in the middle of "→" (byte 1, still part of the arrow character)
2280        let mid_arrow_offset = InlayOffset(MultiBufferOffset(101));
2281        let result = find_hovered_hint_part(label_parts, hint_start, mid_arrow_offset);
2282        assert!(
2283            result.is_some(),
2284            "Should find part when hovering in middle of multi-byte char"
2285        );
2286
2287        // Test with multiple parts containing multi-byte characters
2288        // Part ranges are [start, end) - start inclusive, end exclusive
2289        // "→ " occupies bytes [0, 4), "path" occupies bytes [4, 8)
2290        let parts = vec![
2291            InlayHintLabelPart {
2292                value: "→ ".to_string(), // 4 bytes (3 + 1)
2293                tooltip: None,
2294                location: None,
2295            },
2296            InlayHintLabelPart {
2297                value: "path".to_string(), // 4 bytes
2298                tooltip: None,
2299                location: None,
2300            },
2301        ];
2302
2303        // Hover at byte 3 (last byte of "→ ", the space character)
2304        let arrow_last_byte = InlayOffset(MultiBufferOffset(100 + 3));
2305        let result = find_hovered_hint_part(parts.clone(), hint_start, arrow_last_byte);
2306        assert!(result.is_some(), "Should find first part at its last byte");
2307        let (part, range) = result.unwrap();
2308        assert_eq!(part.value, "→ ");
2309        assert_eq!(
2310            range,
2311            InlayOffset(MultiBufferOffset(100))..InlayOffset(MultiBufferOffset(104))
2312        );
2313
2314        // Hover at byte 4 (first byte of "path", at the boundary)
2315        let path_start_offset = InlayOffset(MultiBufferOffset(100 + 4));
2316        let result = find_hovered_hint_part(parts.clone(), hint_start, path_start_offset);
2317        assert!(result.is_some(), "Should find second part at boundary");
2318        let (part, _) = result.unwrap();
2319        assert_eq!(part.value, "path");
2320
2321        // Hover at byte 7 (last byte of "path")
2322        let path_end_offset = InlayOffset(MultiBufferOffset(100 + 7));
2323        let result = find_hovered_hint_part(parts, hint_start, path_end_offset);
2324        assert!(result.is_some(), "Should find second part at last byte");
2325        let (part, range) = result.unwrap();
2326        assert_eq!(part.value, "path");
2327        assert_eq!(
2328            range,
2329            InlayOffset(MultiBufferOffset(104))..InlayOffset(MultiBufferOffset(108))
2330        );
2331    }
2332
2333    #[gpui::test]
2334    async fn test_hover_popover_hiding_delay(cx: &mut gpui::TestAppContext) {
2335        init_test(cx, |_| {});
2336
2337        let custom_delay_ms = 500u64;
2338        cx.update(|cx| {
2339            cx.update_global::<SettingsStore, _>(|settings, cx| {
2340                settings.update_user_settings(cx, |settings| {
2341                    settings.editor.hover_popover_sticky = Some(true);
2342                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2343                });
2344            });
2345        });
2346
2347        let mut cx = EditorLspTestContext::new_rust(
2348            lsp::ServerCapabilities {
2349                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2350                ..Default::default()
2351            },
2352            cx,
2353        )
2354        .await;
2355
2356        cx.set_state(indoc! {"
2357            fn ˇtest() { println!(); }
2358        "});
2359
2360        // Trigger hover on a symbol
2361        let hover_point = cx.display_point(indoc! {"
2362            fn test() { printˇln!(); }
2363        "});
2364        let symbol_range = cx.lsp_range(indoc! {"
2365            fn test() { «println!»(); }
2366        "});
2367        let mut requests =
2368            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2369                Ok(Some(lsp::Hover {
2370                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2371                        kind: lsp::MarkupKind::Markdown,
2372                        value: "some basic docs".to_string(),
2373                    }),
2374                    range: Some(symbol_range),
2375                }))
2376            });
2377        cx.update_editor(|editor, window, cx| {
2378            let snapshot = editor.snapshot(window, cx);
2379            let anchor = snapshot
2380                .buffer_snapshot()
2381                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2382            hover_at(editor, Some(anchor), None, window, cx)
2383        });
2384        cx.background_executor
2385            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2386        requests.next().await;
2387
2388        // Hover should be visible
2389        cx.editor(|editor, _, _| {
2390            assert!(editor.hover_state.visible());
2391        });
2392
2393        // Move mouse away (hover_at with None anchor triggers the hiding delay)
2394        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2395
2396        // Popover should still be visible before the custom hiding delay expires
2397        cx.background_executor
2398            .advance_clock(Duration::from_millis(custom_delay_ms - 100));
2399        cx.editor(|editor, _, _| {
2400            assert!(
2401                editor.hover_state.visible(),
2402                "Popover should remain visible before the hiding delay expires"
2403            );
2404        });
2405
2406        // After the full custom delay, the popover should be hidden
2407        cx.background_executor
2408            .advance_clock(Duration::from_millis(200));
2409        cx.editor(|editor, _, _| {
2410            assert!(
2411                !editor.hover_state.visible(),
2412                "Popover should be hidden after the hiding delay expires"
2413            );
2414        });
2415    }
2416
2417    #[gpui::test]
2418    async fn test_hover_popover_sticky_disabled(cx: &mut gpui::TestAppContext) {
2419        init_test(cx, |_| {});
2420
2421        cx.update(|cx| {
2422            cx.update_global::<SettingsStore, _>(|settings, cx| {
2423                settings.update_user_settings(cx, |settings| {
2424                    settings.editor.hover_popover_sticky = Some(false);
2425                });
2426            });
2427        });
2428
2429        let mut cx = EditorLspTestContext::new_rust(
2430            lsp::ServerCapabilities {
2431                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2432                ..Default::default()
2433            },
2434            cx,
2435        )
2436        .await;
2437
2438        cx.set_state(indoc! {"
2439            fn ˇtest() { println!(); }
2440        "});
2441
2442        // Trigger hover on a symbol
2443        let hover_point = cx.display_point(indoc! {"
2444            fn test() { printˇln!(); }
2445        "});
2446        let symbol_range = cx.lsp_range(indoc! {"
2447            fn test() { «println!»(); }
2448        "});
2449        let mut requests =
2450            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2451                Ok(Some(lsp::Hover {
2452                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2453                        kind: lsp::MarkupKind::Markdown,
2454                        value: "some basic docs".to_string(),
2455                    }),
2456                    range: Some(symbol_range),
2457                }))
2458            });
2459        cx.update_editor(|editor, window, cx| {
2460            let snapshot = editor.snapshot(window, cx);
2461            let anchor = snapshot
2462                .buffer_snapshot()
2463                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2464            hover_at(editor, Some(anchor), None, window, cx)
2465        });
2466        cx.background_executor
2467            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2468        requests.next().await;
2469
2470        // Hover should be visible
2471        cx.editor(|editor, _, _| {
2472            assert!(editor.hover_state.visible());
2473        });
2474
2475        // Move mouse away — with sticky disabled, hide immediately
2476        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2477
2478        // Popover should be hidden immediately without any delay
2479        cx.editor(|editor, _, _| {
2480            assert!(
2481                !editor.hover_state.visible(),
2482                "Popover should be hidden immediately when sticky is disabled"
2483            );
2484        });
2485    }
2486
2487    #[gpui::test]
2488    async fn test_hover_popover_hiding_delay_restarts_when_mouse_gets_closer(
2489        cx: &mut gpui::TestAppContext,
2490    ) {
2491        init_test(cx, |_| {});
2492
2493        let custom_delay_ms = 600u64;
2494        cx.update(|cx| {
2495            cx.update_global::<SettingsStore, _>(|settings, cx| {
2496                settings.update_user_settings(cx, |settings| {
2497                    settings.editor.hover_popover_sticky = Some(true);
2498                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2499                });
2500            });
2501        });
2502
2503        let mut cx = EditorLspTestContext::new_rust(
2504            lsp::ServerCapabilities {
2505                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2506                ..Default::default()
2507            },
2508            cx,
2509        )
2510        .await;
2511
2512        cx.set_state(indoc! {"
2513            fn ˇtest() { println!(); }
2514        "});
2515
2516        let hover_point = cx.display_point(indoc! {"
2517            fn test() { printˇln!(); }
2518        "});
2519        let symbol_range = cx.lsp_range(indoc! {"
2520            fn test() { «println!»(); }
2521        "});
2522        let mut requests =
2523            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2524                Ok(Some(lsp::Hover {
2525                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2526                        kind: lsp::MarkupKind::Markdown,
2527                        value: "some basic docs".to_string(),
2528                    }),
2529                    range: Some(symbol_range),
2530                }))
2531            });
2532        cx.update_editor(|editor, window, cx| {
2533            let snapshot = editor.snapshot(window, cx);
2534            let anchor = snapshot
2535                .buffer_snapshot()
2536                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2537            hover_at(editor, Some(anchor), None, window, cx)
2538        });
2539        cx.background_executor
2540            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2541        requests.next().await;
2542
2543        cx.editor(|editor, _, _| {
2544            assert!(editor.hover_state.visible());
2545        });
2546
2547        cx.update_editor(|editor, _, _| {
2548            let popover = editor.hover_state.info_popovers.first().unwrap();
2549            popover.last_bounds.set(Some(Bounds {
2550                origin: gpui::Point {
2551                    x: px(100.0),
2552                    y: px(100.0),
2553                },
2554                size: Size {
2555                    width: px(100.0),
2556                    height: px(60.0),
2557                },
2558            }));
2559        });
2560
2561        let far_point = gpui::Point {
2562            x: px(260.0),
2563            y: px(130.0),
2564        };
2565        cx.update_editor(|editor, window, cx| hover_at(editor, None, Some(far_point), window, cx));
2566
2567        cx.background_executor
2568            .advance_clock(Duration::from_millis(400));
2569        cx.background_executor.run_until_parked();
2570
2571        let closer_point = gpui::Point {
2572            x: px(220.0),
2573            y: px(130.0),
2574        };
2575        cx.update_editor(|editor, window, cx| {
2576            hover_at(editor, None, Some(closer_point), window, cx)
2577        });
2578
2579        cx.background_executor
2580            .advance_clock(Duration::from_millis(250));
2581        cx.background_executor.run_until_parked();
2582
2583        cx.editor(|editor, _, _| {
2584            assert!(
2585                editor.hover_state.visible(),
2586                "Popover should remain visible because moving closer restarts the hiding timer"
2587            );
2588        });
2589
2590        cx.background_executor
2591            .advance_clock(Duration::from_millis(350));
2592        cx.background_executor.run_until_parked();
2593
2594        cx.editor(|editor, _, _| {
2595            assert!(
2596                !editor.hover_state.visible(),
2597                "Popover should hide after the restarted hiding timer expires"
2598            );
2599        });
2600    }
2601
2602    #[gpui::test]
2603    async fn test_hover_popover_cancel_hide_on_rehover(cx: &mut gpui::TestAppContext) {
2604        init_test(cx, |_| {});
2605
2606        let custom_delay_ms = 500u64;
2607        cx.update(|cx| {
2608            cx.update_global::<SettingsStore, _>(|settings, cx| {
2609                settings.update_user_settings(cx, |settings| {
2610                    settings.editor.hover_popover_sticky = Some(true);
2611                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(custom_delay_ms));
2612                });
2613            });
2614        });
2615
2616        let mut cx = EditorLspTestContext::new_rust(
2617            lsp::ServerCapabilities {
2618                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2619                ..Default::default()
2620            },
2621            cx,
2622        )
2623        .await;
2624
2625        cx.set_state(indoc! {"
2626            fn ˇtest() { println!(); }
2627        "});
2628
2629        let hover_point = cx.display_point(indoc! {"
2630            fn test() { printˇln!(); }
2631        "});
2632        let symbol_range = cx.lsp_range(indoc! {"
2633            fn test() { «println!»(); }
2634        "});
2635        let mut requests =
2636            cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
2637                Ok(Some(lsp::Hover {
2638                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
2639                        kind: lsp::MarkupKind::Markdown,
2640                        value: "some basic docs".to_string(),
2641                    }),
2642                    range: Some(symbol_range),
2643                }))
2644            });
2645        cx.update_editor(|editor, window, cx| {
2646            let snapshot = editor.snapshot(window, cx);
2647            let anchor = snapshot
2648                .buffer_snapshot()
2649                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2650            hover_at(editor, Some(anchor), None, window, cx)
2651        });
2652        cx.background_executor
2653            .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
2654        requests.next().await;
2655
2656        cx.editor(|editor, _, _| {
2657            assert!(editor.hover_state.visible());
2658        });
2659
2660        // Move mouse away — starts the 500ms hide timer
2661        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2662
2663        cx.background_executor
2664            .advance_clock(Duration::from_millis(300));
2665        cx.background_executor.run_until_parked();
2666        cx.editor(|editor, _, _| {
2667            assert!(
2668                editor.hover_state.visible(),
2669                "Popover should still be visible before hiding delay expires"
2670            );
2671        });
2672
2673        // Move back to the symbol — should cancel the hiding timer
2674        cx.update_editor(|editor, window, cx| {
2675            let snapshot = editor.snapshot(window, cx);
2676            let anchor = snapshot
2677                .buffer_snapshot()
2678                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2679            hover_at(editor, Some(anchor), None, window, cx)
2680        });
2681
2682        // Advance past the original deadline — popover should still be visible
2683        // because re-hovering cleared the hiding_delay_task
2684        cx.background_executor
2685            .advance_clock(Duration::from_millis(300));
2686        cx.background_executor.run_until_parked();
2687        cx.editor(|editor, _, _| {
2688            assert!(
2689                editor.hover_state.visible(),
2690                "Popover should remain visible after re-hovering the symbol"
2691            );
2692            assert!(
2693                editor.hover_state.hiding_delay_task.is_none(),
2694                "Hiding delay task should have been cleared by re-hover"
2695            );
2696        });
2697
2698        // Move away again — starts a fresh 500ms timer
2699        cx.update_editor(|editor, window, cx| hover_at(editor, None, None, window, cx));
2700
2701        cx.background_executor
2702            .advance_clock(Duration::from_millis(custom_delay_ms + 100));
2703        cx.background_executor.run_until_parked();
2704        cx.editor(|editor, _, _| {
2705            assert!(
2706                !editor.hover_state.visible(),
2707                "Popover should hide after the new hiding timer expires"
2708            );
2709        });
2710    }
2711
2712    #[gpui::test]
2713    async fn test_hover_popover_enabled_false_ignores_sticky(cx: &mut gpui::TestAppContext) {
2714        init_test(cx, |_| {});
2715
2716        cx.update(|cx| {
2717            cx.update_global::<SettingsStore, _>(|settings, cx| {
2718                settings.update_user_settings(cx, |settings| {
2719                    settings.editor.hover_popover_enabled = Some(false);
2720                    settings.editor.hover_popover_sticky = Some(true);
2721                    settings.editor.hover_popover_hiding_delay = Some(DelayMs(500));
2722                });
2723            });
2724        });
2725
2726        let mut cx = EditorLspTestContext::new_rust(
2727            lsp::ServerCapabilities {
2728                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
2729                ..Default::default()
2730            },
2731            cx,
2732        )
2733        .await;
2734
2735        cx.set_state(indoc! {"
2736            fn ˇtest() { println!(); }
2737        "});
2738
2739        let hover_point = cx.display_point(indoc! {"
2740            fn test() { printˇln!(); }
2741        "});
2742
2743        // Trigger hover_at — should be gated by hover_popover_enabled=false
2744        cx.update_editor(|editor, window, cx| {
2745            let snapshot = editor.snapshot(window, cx);
2746            let anchor = snapshot
2747                .buffer_snapshot()
2748                .anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
2749            hover_at(editor, Some(anchor), None, window, cx)
2750        });
2751
2752        // No need to advance clock or wait for LSP — the gate should prevent any work
2753        cx.editor(|editor, _, _| {
2754            assert!(
2755                !editor.hover_state.visible(),
2756                "Popover should not appear when hover_popover_enabled is false"
2757            );
2758            assert!(
2759                editor.hover_state.info_task.is_none(),
2760                "No hover info task should be scheduled when hover is disabled"
2761            );
2762        });
2763    }
2764
2765    #[test]
2766    fn test_parse_file_links() {
2767        assert_eq!(
2768            parse_file_link("file:///path/to/file"),
2769            Some((PathBuf::from("/path/to/file"), None))
2770        );
2771        assert_eq!(
2772            parse_file_link("file:///path/to/file%20with%20spaces"),
2773            Some((PathBuf::from("/path/to/file with spaces"), None))
2774        );
2775        assert_eq!(
2776            parse_file_link("file:///path/to/file#123"),
2777            Some((PathBuf::from("/path/to/file"), Some("123".to_string())))
2778        );
2779        assert_eq!(parse_file_link("http://example.com/"), None,);
2780    }
2781}
2782
Served at tenant.openagents/omega Member data and write actions are omitted.