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}