hover_popover.rs

  1use std::{
  2    ops::Range,
  3    time::{Duration, Instant},
  4};
  5
  6use gpui::{
  7    actions,
  8    elements::{Flex, MouseEventHandler, Padding, Text},
  9    impl_internal_actions,
 10    platform::CursorStyle,
 11    Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
 12};
 13use language::Bias;
 14use project::{HoverBlock, Project};
 15use util::TryFutureExt;
 16
 17use crate::{
 18    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
 19    EditorStyle,
 20};
 21
 22pub const HOVER_DELAY_MILLIS: u64 = 500;
 23pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 250;
 24pub const HOVER_GRACE_MILLIS: u64 = 250;
 25
 26#[derive(Clone, PartialEq)]
 27pub struct HoverAt {
 28    pub point: Option<DisplayPoint>,
 29}
 30
 31actions!(editor, [Hover]);
 32impl_internal_actions!(editor, [HoverAt]);
 33
 34pub fn init(cx: &mut MutableAppContext) {
 35    cx.add_action(hover);
 36    cx.add_action(hover_at);
 37}
 38
 39/// Bindable action which uses the most recent selection head to trigger a hover
 40pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
 41    let head = editor.selections.newest_display(cx).head();
 42    show_hover(editor, head, true, cx);
 43}
 44
 45/// The internal hover action dispatches between `show_hover` or `hide_hover`
 46/// depending on whether a point to hover over is provided.
 47pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
 48    if let Some(point) = action.point {
 49        show_hover(editor, point, false, cx);
 50    } else {
 51        hide_hover(editor, cx);
 52    }
 53}
 54
 55/// Hides the type information popup.
 56/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 57/// selections changed.
 58pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
 59    let mut did_hide = false;
 60
 61    // only notify the context once
 62    if editor.hover_state.popover.is_some() {
 63        editor.hover_state.popover = None;
 64        editor.hover_state.hidden_at = Some(cx.background().now());
 65        did_hide = true;
 66        cx.notify();
 67    }
 68    editor.hover_state.task = None;
 69    editor.hover_state.triggered_from = None;
 70    editor.hover_state.symbol_range = None;
 71
 72    editor.clear_background_highlights::<HoverState>(cx);
 73
 74    did_hide
 75}
 76
 77/// Queries the LSP and shows type info and documentation
 78/// about the symbol the mouse is currently hovering over.
 79/// Triggered by the `Hover` action when the cursor may be over a symbol.
 80fn show_hover(
 81    editor: &mut Editor,
 82    point: DisplayPoint,
 83    ignore_timeout: bool,
 84    cx: &mut ViewContext<Editor>,
 85) {
 86    if editor.pending_rename.is_some() {
 87        return;
 88    }
 89
 90    let snapshot = editor.snapshot(cx);
 91    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
 92
 93    let (buffer, buffer_position) = if let Some(output) = editor
 94        .buffer
 95        .read(cx)
 96        .text_anchor_for_position(multibuffer_offset, cx)
 97    {
 98        output
 99    } else {
100        return;
101    };
102
103    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
104        .buffer()
105        .read(cx)
106        .excerpt_containing(multibuffer_offset, cx)
107    {
108        excerpt_id
109    } else {
110        return;
111    };
112
113    let project = if let Some(project) = editor.project.clone() {
114        project
115    } else {
116        return;
117    };
118
119    // We should only delay if the hover popover isn't visible, it wasn't recently hidden, and
120    // the hover wasn't triggered from the keyboard
121    let should_delay = editor.hover_state.popover.is_none() // Hover not visible currently
122    && editor
123            .hover_state
124            .hidden_at
125            .map(|hidden| hidden.elapsed().as_millis() > HOVER_GRACE_MILLIS as u128)
126            .unwrap_or(true) // Hover wasn't recently visible
127        && !ignore_timeout; // Hover was not triggered from keyboard
128
129    if should_delay {
130        if let Some(range) = &editor.hover_state.symbol_range {
131            if range
132                .to_offset(&snapshot.buffer_snapshot)
133                .contains(&multibuffer_offset)
134            {
135                // Hover triggered from same location as last time. Don't show again.
136                return;
137            }
138        }
139    }
140
141    // Get input anchor
142    let anchor = snapshot
143        .buffer_snapshot
144        .anchor_at(multibuffer_offset, Bias::Left);
145
146    // Don't request again if the location is the same as the previous request
147    if let Some(triggered_from) = &editor.hover_state.triggered_from {
148        if triggered_from
149            .cmp(&anchor, &snapshot.buffer_snapshot)
150            .is_eq()
151        {
152            return;
153        }
154    }
155
156    let task = cx.spawn_weak(|this, mut cx| {
157        async move {
158            // If we need to delay, delay a set amount initially before making the lsp request
159            let delay = if should_delay {
160                // Construct delay task to wait for later
161                let total_delay = Some(
162                    cx.background()
163                        .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
164                );
165
166                cx.background()
167                    .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
168                    .await;
169                total_delay
170            } else {
171                None
172            };
173
174            // query the LSP for hover info
175            let hover_request = cx.update(|cx| {
176                project.update(cx, |project, cx| {
177                    project.hover(&buffer, buffer_position.clone(), cx)
178                })
179            });
180
181            // Construct new hover popover from hover request
182            let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
183                if hover_result.contents.is_empty() {
184                    return None;
185                }
186
187                // Create symbol range of anchors for highlighting and filtering
188                // of future requests.
189                let range = if let Some(range) = hover_result.range {
190                    let start = snapshot
191                        .buffer_snapshot
192                        .anchor_in_excerpt(excerpt_id.clone(), range.start);
193                    let end = snapshot
194                        .buffer_snapshot
195                        .anchor_in_excerpt(excerpt_id.clone(), range.end);
196
197                    start..end
198                } else {
199                    anchor.clone()..anchor.clone()
200                };
201
202                if let Some(this) = this.upgrade(&cx) {
203                    this.update(&mut cx, |this, _| {
204                        this.hover_state.symbol_range = Some(range.clone());
205                    });
206                }
207
208                Some(HoverPopover {
209                    project: project.clone(),
210                    anchor: range.start.clone(),
211                    contents: hover_result.contents,
212                })
213            });
214
215            if let Some(delay) = delay {
216                delay.await;
217            }
218
219            if let Some(this) = this.upgrade(&cx) {
220                this.update(&mut cx, |this, cx| {
221                    if hover_popover.is_some() {
222                        // Highlight the selected symbol using a background highlight
223                        if let Some(range) = this.hover_state.symbol_range.clone() {
224                            this.highlight_background::<HoverState>(
225                                vec![range],
226                                |theme| theme.editor.hover_popover.highlight,
227                                cx,
228                            );
229                        }
230                        this.hover_state.popover = hover_popover;
231                        cx.notify();
232                    } else {
233                        if this.hover_state.visible() {
234                            // Popover was visible, but now is hidden. Dismiss it
235                            hide_hover(this, cx);
236                        } else {
237                            // Clear selected symbol range for future requests
238                            this.hover_state.symbol_range = None;
239                        }
240                    }
241                });
242            }
243            Ok::<_, anyhow::Error>(())
244        }
245        .log_err()
246    });
247
248    editor.hover_state.task = Some(task);
249}
250
251#[derive(Default)]
252pub struct HoverState {
253    pub popover: Option<HoverPopover>,
254    pub hidden_at: Option<Instant>,
255    pub triggered_from: Option<Anchor>,
256    pub symbol_range: Option<Range<Anchor>>,
257    pub task: Option<Task<Option<()>>>,
258}
259
260impl HoverState {
261    pub fn visible(&self) -> bool {
262        self.popover.is_some()
263    }
264}
265
266#[derive(Debug, Clone)]
267pub struct HoverPopover {
268    pub project: ModelHandle<Project>,
269    pub anchor: Anchor,
270    pub contents: Vec<HoverBlock>,
271}
272
273impl HoverPopover {
274    pub fn render(
275        &self,
276        snapshot: &EditorSnapshot,
277        style: EditorStyle,
278        cx: &mut RenderContext<Editor>,
279    ) -> (DisplayPoint, ElementBox) {
280        let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
281            let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
282            flex.extend(self.contents.iter().map(|content| {
283                let project = self.project.read(cx);
284                if let Some(language) = content
285                    .language
286                    .clone()
287                    .and_then(|language| project.languages().get_language(&language))
288                {
289                    let runs = language
290                        .highlight_text(&content.text.as_str().into(), 0..content.text.len());
291
292                    Text::new(content.text.clone(), style.text.clone())
293                        .with_soft_wrap(true)
294                        .with_highlights(
295                            runs.iter()
296                                .filter_map(|(range, id)| {
297                                    id.style(style.theme.syntax.as_ref())
298                                        .map(|style| (range.clone(), style))
299                                })
300                                .collect(),
301                        )
302                        .boxed()
303                } else {
304                    let mut text_style = style.hover_popover.prose.clone();
305                    text_style.font_size = style.text.font_size;
306
307                    Text::new(content.text.clone(), text_style)
308                        .with_soft_wrap(true)
309                        .contained()
310                        .with_style(style.hover_popover.block_style)
311                        .boxed()
312                }
313            }));
314            flex.contained()
315                .with_style(style.hover_popover.container)
316                .boxed()
317        })
318        .with_cursor_style(CursorStyle::Arrow)
319        .with_padding(Padding {
320            bottom: 5.,
321            top: 5.,
322            ..Default::default()
323        })
324        .boxed();
325
326        let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
327        (display_point, element)
328    }
329}