Skip to main content

gpui_base/input/editor/lsp/
hover.rs

1use anyhow::Result;
2use gpui::{App, Context, MouseMoveEvent, Task, Window};
3use instant::Duration;
4use ropey::Rope;
5
6use crate::input::{EditorMode, HoverPopoverState, InputBaseState, RopeExt};
7
8/// Hover provider
9///
10/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
11pub trait HoverProvider {
12    /// textDocument/hover
13    ///
14    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
15    fn hover(
16        &self,
17        _text: &Rope,
18        _offset: usize,
19        _window: &mut Window,
20        _cx: &mut App,
21    ) -> Task<Result<Option<lsp_types::Hover>>>;
22}
23
24impl InputBaseState<EditorMode> {
25    /// Handle hover trigger LSP request.
26    pub(super) fn handle_hover_popover(
27        &mut self,
28        offset: usize,
29        window: &mut Window,
30        cx: &mut Context<InputBaseState<EditorMode>>,
31    ) {
32        if self.selecting {
33            return;
34        }
35
36        let Some(provider) = self.extras.lsp.hover_provider.clone() else {
37            return;
38        };
39
40        if let Some(hover_popover) = self.extras.hover_popover.as_ref() {
41            if hover_popover.symbol_range.contains(&offset) {
42                return;
43            }
44        }
45
46        let text = self.text.clone();
47        let mut symbol_range = self.text.word_range(offset).unwrap_or(offset..offset);
48        let editor = cx.entity();
49        let should_delay = self.extras.hover_popover.is_none();
50        // Ask the provider only after the delay: every mouse move replaces this
51        // task, so positions the pointer merely passes over never reach it.
52        self.extras.lsp._hover_task = cx.spawn_in(window, async move |this, cx| {
53            if should_delay {
54                cx.background_executor()
55                    .timer(Duration::from_millis(150))
56                    .await;
57            }
58
59            let task = this.update_in(cx, |_, window, cx| {
60                provider.hover(&text, offset, window, cx)
61            })?;
62            let result = task.await?;
63
64            _ = editor.update(cx, |editor, cx| {
65                match result {
66                    Some(hover) => {
67                        if let Some(range) = hover.range {
68                            let start = editor.text.position_to_offset(&range.start);
69                            let end = editor.text.position_to_offset(&range.end);
70                            symbol_range = start..end;
71                        }
72                        editor.extras.hover_popover = Some(HoverPopoverState {
73                            symbol_range,
74                            hover,
75                        });
76                    }
77                    None => {
78                        editor.extras.hover_popover = None;
79                    }
80                }
81                cx.notify();
82            });
83
84            Ok(())
85        });
86    }
87
88    pub(crate) fn handle_mouse_move(
89        &mut self,
90        offset: usize,
91        event: &MouseMoveEvent,
92        window: &mut Window,
93        cx: &mut Context<Self>,
94    ) {
95        if event.modifiers.alt {
96            self.clear_hover_state(cx);
97            return;
98        }
99        if event.modifiers.secondary() {
100            self.handle_hover_definition(offset, window, cx);
101        } else {
102            // Repaint only when the definition underline goes away; the hover
103            // and definition results notify when they arrive.
104            if !self.extras.hover_definition.is_empty() {
105                self.extras.hover_definition.clear();
106                cx.notify();
107            }
108            self.handle_hover_popover(offset, window, cx);
109        }
110    }
111
112    pub fn clear_hover_state(&mut self, cx: &mut Context<Self>) {
113        let changed =
114            !self.extras.hover_definition.is_empty() || self.extras.hover_popover.is_some();
115        self.extras.hover_definition.clear();
116        self.extras.hover_popover = None;
117        self.extras.lsp._hover_task = Task::ready(Ok(()));
118        if changed {
119            cx.notify();
120        }
121    }
122}