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.symbol_range = None;
 70
 71    editor.clear_background_highlights::<HoverState>(cx);
 72
 73    did_hide
 74}
 75
 76/// Queries the LSP and shows type info and documentation
 77/// about the symbol the mouse is currently hovering over.
 78/// Triggered by the `Hover` action when the cursor may be over a symbol.
 79fn show_hover(
 80    editor: &mut Editor,
 81    point: DisplayPoint,
 82    ignore_timeout: bool,
 83    cx: &mut ViewContext<Editor>,
 84) {
 85    if editor.pending_rename.is_some() {
 86        return;
 87    }
 88
 89    let snapshot = editor.snapshot(cx);
 90    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
 91
 92    let (buffer, buffer_position) = if let Some(output) = editor
 93        .buffer
 94        .read(cx)
 95        .text_anchor_for_position(multibuffer_offset, cx)
 96    {
 97        output
 98    } else {
 99        return;
100    };
101
102    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
103        .buffer()
104        .read(cx)
105        .excerpt_containing(multibuffer_offset, cx)
106    {
107        excerpt_id
108    } else {
109        return;
110    };
111
112    let project = if let Some(project) = editor.project.clone() {
113        project
114    } else {
115        return;
116    };
117
118    // query the LSP for hover info
119    let hover_request = project.update(cx, |project, cx| {
120        project.hover(&buffer, buffer_position.clone(), cx)
121    });
122
123    // We should only delay if the hover popover isn't visible, it wasn't recently hidden, and
124    // the hover wasn't triggered from the keyboard
125    let should_delay = editor.hover_state.popover.is_none() // Hover not visible currently
126        && editor
127            .hover_state
128            .hidden_at
129            .map(|hidden| hidden.elapsed().as_millis() > HOVER_GRACE_MILLIS as u128)
130            .unwrap_or(true) // Hover wasn't recently visible
131        && !ignore_timeout; // Hover was not triggered from keyboard
132
133    if should_delay {
134        if let Some(range) = &editor.hover_state.symbol_range {
135            if range
136                .to_offset(&snapshot.buffer_snapshot)
137                .contains(&multibuffer_offset)
138            {
139                // Hover triggered from same location as last time. Don't show again.
140                return;
141            }
142        }
143    }
144
145    // Get input anchor
146    let anchor = snapshot
147        .buffer_snapshot
148        .anchor_at(multibuffer_offset, Bias::Left);
149
150    let task = cx.spawn_weak(|this, mut cx| {
151        async move {
152            // If we need to delay, delay a set amount initially before making the lsp request
153            let delay = if should_delay {
154                // Construct delay task to wait for later
155                let total_delay = Some(
156                    cx.background()
157                        .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
158                );
159
160                cx.background()
161                    .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
162                    .await;
163                total_delay
164            } else {
165                None
166            };
167
168            // Construct new hover popover from hover request
169            let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
170                if hover_result.contents.is_empty() {
171                    return None;
172                }
173
174                // Create symbol range of anchors for highlighting and filtering
175                // of future requests.
176                let range = if let Some(range) = hover_result.range {
177                    let start = snapshot
178                        .buffer_snapshot
179                        .anchor_in_excerpt(excerpt_id.clone(), range.start);
180                    let end = snapshot
181                        .buffer_snapshot
182                        .anchor_in_excerpt(excerpt_id.clone(), range.end);
183
184                    start..end
185                } else {
186                    anchor.clone()..anchor.clone()
187                };
188
189                if let Some(this) = this.upgrade(&cx) {
190                    this.update(&mut cx, |this, _| {
191                        this.hover_state.symbol_range = Some(range.clone());
192                    });
193                }
194
195                Some(HoverPopover {
196                    project: project.clone(),
197                    anchor: range.start.clone(),
198                    contents: hover_result.contents,
199                })
200            });
201
202            if let Some(delay) = delay {
203                delay.await;
204            }
205
206            if let Some(this) = this.upgrade(&cx) {
207                this.update(&mut cx, |this, cx| {
208                    if hover_popover.is_some() {
209                        // Highlight the selected symbol using a background highlight
210                        if let Some(range) = this.hover_state.symbol_range.clone() {
211                            this.highlight_background::<HoverState>(
212                                vec![range],
213                                |theme| theme.editor.hover_popover.highlight,
214                                cx,
215                            );
216                        }
217                        this.hover_state.popover = hover_popover;
218                        cx.notify();
219                    } else {
220                        if this.hover_state.popover.is_some() {
221                            // Popover was visible, but now is hidden. Dismiss it
222                            hide_hover(this, cx);
223                        } else {
224                            // Clear selected symbol range for future requests
225                            this.hover_state.symbol_range = None;
226                        }
227                    }
228                });
229            }
230            Ok::<_, anyhow::Error>(())
231        }
232        .log_err()
233    });
234
235    editor.hover_state.task = Some(task);
236}
237
238#[derive(Default)]
239pub struct HoverState {
240    pub popover: Option<HoverPopover>,
241    pub hidden_at: Option<Instant>,
242    pub symbol_range: Option<Range<Anchor>>,
243    pub task: Option<Task<Option<()>>>,
244}
245
246impl HoverState {
247    pub fn visible(&self) -> bool {
248        self.popover.is_some()
249    }
250}
251
252#[derive(Debug, Clone)]
253pub struct HoverPopover {
254    pub project: ModelHandle<Project>,
255    pub anchor: Anchor,
256    pub contents: Vec<HoverBlock>,
257}
258
259impl HoverPopover {
260    pub fn render(
261        &self,
262        snapshot: &EditorSnapshot,
263        style: EditorStyle,
264        cx: &mut RenderContext<Editor>,
265    ) -> (DisplayPoint, ElementBox) {
266        let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
267            let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
268            flex.extend(self.contents.iter().map(|content| {
269                let project = self.project.read(cx);
270                if let Some(language) = content
271                    .language
272                    .clone()
273                    .and_then(|language| project.languages().get_language(&language))
274                {
275                    let runs = language
276                        .highlight_text(&content.text.as_str().into(), 0..content.text.len());
277
278                    Text::new(content.text.clone(), style.text.clone())
279                        .with_soft_wrap(true)
280                        .with_highlights(
281                            runs.iter()
282                                .filter_map(|(range, id)| {
283                                    id.style(style.theme.syntax.as_ref())
284                                        .map(|style| (range.clone(), style))
285                                })
286                                .collect(),
287                        )
288                        .boxed()
289                } else {
290                    let mut text_style = style.hover_popover.prose.clone();
291                    text_style.font_size = style.text.font_size;
292
293                    Text::new(content.text.clone(), text_style)
294                        .with_soft_wrap(true)
295                        .contained()
296                        .with_style(style.hover_popover.block_style)
297                        .boxed()
298                }
299            }));
300            flex.contained()
301                .with_style(style.hover_popover.container)
302                .boxed()
303        })
304        .with_cursor_style(CursorStyle::Arrow)
305        .with_padding(Padding {
306            bottom: 5.,
307            top: 5.,
308            ..Default::default()
309        })
310        .boxed();
311
312        let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
313        (display_point, element)
314    }
315}