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 = 350;
23pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
24
25#[derive(Clone, PartialEq)]
26pub struct HoverAt {
27 pub point: Option<DisplayPoint>,
28}
29
30actions!(editor, [Hover]);
31impl_internal_actions!(editor, [HoverAt]);
32
33pub fn init(cx: &mut MutableAppContext) {
34 cx.add_action(hover);
35 cx.add_action(hover_at);
36}
37
38/// Bindable action which uses the most recent selection head to trigger a hover
39pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
40 let head = editor.selections.newest_display(cx).head();
41 show_hover(editor, head, true, cx);
42}
43
44/// The internal hover action dispatches between `show_hover` or `hide_hover`
45/// depending on whether a point to hover over is provided.
46pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
47 if let Some(point) = action.point {
48 show_hover(editor, point, false, cx);
49 } else {
50 hide_hover(editor, cx);
51 }
52}
53
54/// Hides the type information popup.
55/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
56/// selections changed.
57pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
58 let mut did_hide = false;
59
60 // only notify the context once
61 if editor.hover_state.popover.is_some() {
62 editor.hover_state.popover = None;
63 editor.hover_state.hidden_at = Some(cx.background().now());
64 did_hide = true;
65 cx.notify();
66 }
67 editor.hover_state.task = None;
68 editor.hover_state.triggered_from = 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 if !ignore_timeout {
119 if let Some(range) = &editor.hover_state.symbol_range {
120 if range
121 .to_offset(&snapshot.buffer_snapshot)
122 .contains(&multibuffer_offset)
123 {
124 // Hover triggered from same location as last time. Don't show again.
125 return;
126 } else {
127 hide_hover(editor, cx);
128 }
129 }
130 }
131
132 // Get input anchor
133 let anchor = snapshot
134 .buffer_snapshot
135 .anchor_at(multibuffer_offset, Bias::Left);
136
137 // Don't request again if the location is the same as the previous request
138 if let Some(triggered_from) = &editor.hover_state.triggered_from {
139 if triggered_from
140 .cmp(&anchor, &snapshot.buffer_snapshot)
141 .is_eq()
142 {
143 return;
144 }
145 }
146
147 let task = cx.spawn_weak(|this, mut cx| {
148 async move {
149 // If we need to delay, delay a set amount initially before making the lsp request
150 let delay = if !ignore_timeout {
151 // Construct delay task to wait for later
152 let total_delay = Some(
153 cx.background()
154 .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
155 );
156
157 cx.background()
158 .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
159 .await;
160 total_delay
161 } else {
162 None
163 };
164
165 // query the LSP for hover info
166 let hover_request = cx.update(|cx| {
167 project.update(cx, |project, cx| {
168 project.hover(&buffer, buffer_position.clone(), cx)
169 })
170 });
171
172 // Construct new hover popover from hover request
173 let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
174 if hover_result.contents.is_empty() {
175 return None;
176 }
177
178 // Create symbol range of anchors for highlighting and filtering
179 // of future requests.
180 let range = if let Some(range) = hover_result.range {
181 let start = snapshot
182 .buffer_snapshot
183 .anchor_in_excerpt(excerpt_id.clone(), range.start);
184 let end = snapshot
185 .buffer_snapshot
186 .anchor_in_excerpt(excerpt_id.clone(), range.end);
187
188 start..end
189 } else {
190 anchor.clone()..anchor.clone()
191 };
192
193 if let Some(this) = this.upgrade(&cx) {
194 this.update(&mut cx, |this, _| {
195 this.hover_state.symbol_range = Some(range.clone());
196 });
197 }
198
199 Some(HoverPopover {
200 project: project.clone(),
201 anchor: range.start.clone(),
202 contents: hover_result.contents,
203 })
204 });
205
206 if let Some(delay) = delay {
207 delay.await;
208 }
209
210 if let Some(this) = this.upgrade(&cx) {
211 this.update(&mut cx, |this, cx| {
212 if hover_popover.is_some() {
213 // Highlight the selected symbol using a background highlight
214 if let Some(range) = this.hover_state.symbol_range.clone() {
215 this.highlight_background::<HoverState>(
216 vec![range],
217 |theme| theme.editor.hover_popover.highlight,
218 cx,
219 );
220 }
221 this.hover_state.popover = hover_popover;
222 cx.notify();
223 } else {
224 if this.hover_state.visible() {
225 // Popover was visible, but now is hidden. Dismiss it
226 hide_hover(this, cx);
227 } else {
228 // Clear selected symbol range for future requests
229 this.hover_state.symbol_range = None;
230 }
231 }
232 });
233 }
234 Ok::<_, anyhow::Error>(())
235 }
236 .log_err()
237 });
238
239 editor.hover_state.task = Some(task);
240}
241
242#[derive(Default)]
243pub struct HoverState {
244 pub popover: Option<HoverPopover>,
245 pub hidden_at: Option<Instant>,
246 pub triggered_from: Option<Anchor>,
247 pub symbol_range: Option<Range<Anchor>>,
248 pub task: Option<Task<Option<()>>>,
249}
250
251impl HoverState {
252 pub fn visible(&self) -> bool {
253 self.popover.is_some()
254 }
255}
256
257#[derive(Debug, Clone)]
258pub struct HoverPopover {
259 pub project: ModelHandle<Project>,
260 pub anchor: Anchor,
261 pub contents: Vec<HoverBlock>,
262}
263
264impl HoverPopover {
265 pub fn render(
266 &self,
267 snapshot: &EditorSnapshot,
268 style: EditorStyle,
269 cx: &mut RenderContext<Editor>,
270 ) -> (DisplayPoint, ElementBox) {
271 let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
272 let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
273 flex.extend(self.contents.iter().map(|content| {
274 let project = self.project.read(cx);
275 if let Some(language) = content
276 .language
277 .clone()
278 .and_then(|language| project.languages().get_language(&language))
279 {
280 let runs = language
281 .highlight_text(&content.text.as_str().into(), 0..content.text.len());
282
283 Text::new(content.text.clone(), style.text.clone())
284 .with_soft_wrap(true)
285 .with_highlights(
286 runs.iter()
287 .filter_map(|(range, id)| {
288 id.style(style.theme.syntax.as_ref())
289 .map(|style| (range.clone(), style))
290 })
291 .collect(),
292 )
293 .boxed()
294 } else {
295 let mut text_style = style.hover_popover.prose.clone();
296 text_style.font_size = style.text.font_size;
297
298 Text::new(content.text.clone(), text_style)
299 .with_soft_wrap(true)
300 .contained()
301 .with_style(style.hover_popover.block_style)
302 .boxed()
303 }
304 }));
305 flex.contained()
306 .with_style(style.hover_popover.container)
307 .boxed()
308 })
309 .with_cursor_style(CursorStyle::Arrow)
310 .with_padding(Padding {
311 bottom: 5.,
312 top: 5.,
313 ..Default::default()
314 })
315 .boxed();
316
317 let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
318 (display_point, element)
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use futures::StreamExt;
325 use indoc::indoc;
326
327 use project::HoverBlock;
328
329 use crate::test::EditorLspTestContext;
330
331 use super::*;
332
333 #[gpui::test]
334 async fn test_hover_popover(cx: &mut gpui::TestAppContext) {
335 let mut cx = EditorLspTestContext::new_rust(
336 lsp::ServerCapabilities {
337 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
338 ..Default::default()
339 },
340 cx,
341 )
342 .await;
343
344 // Basic hover delays and then pops without moving the mouse
345 cx.set_state(indoc! {"
346 fn |test()
347 println!();"});
348 let hover_point = cx.display_point(indoc! {"
349 fn test()
350 print|ln!();"});
351
352 cx.update_editor(|editor, cx| {
353 hover_at(
354 editor,
355 &HoverAt {
356 point: Some(hover_point),
357 },
358 cx,
359 )
360 });
361 assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
362
363 // After delay, hover should be visible.
364 let symbol_range = cx.lsp_range(indoc! {"
365 fn test()
366 [println!]();"});
367 let mut requests =
368 cx.lsp
369 .handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
370 Ok(Some(lsp::Hover {
371 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
372 kind: lsp::MarkupKind::Markdown,
373 value: indoc! {"
374 # Some basic docs
375 Some test documentation"}
376 .to_string(),
377 }),
378 range: Some(symbol_range),
379 }))
380 });
381 cx.foreground()
382 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
383 requests.next().await;
384
385 cx.editor(|editor, _| {
386 assert!(editor.hover_state.visible());
387 assert_eq!(
388 editor.hover_state.popover.clone().unwrap().contents,
389 vec![
390 HoverBlock {
391 text: "Some basic docs".to_string(),
392 language: None
393 },
394 HoverBlock {
395 text: "Some test documentation".to_string(),
396 language: None
397 }
398 ]
399 )
400 });
401
402 // Mouse moved with no hover response dismisses
403 let hover_point = cx.display_point(indoc! {"
404 fn te|st()
405 println!();"});
406 cx.update_editor(|editor, cx| {
407 hover_at(
408 editor,
409 &HoverAt {
410 point: Some(hover_point),
411 },
412 cx,
413 )
414 });
415 let mut request = cx
416 .lsp
417 .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
418 cx.foreground()
419 .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
420 request.next().await;
421 cx.editor(|editor, _| {
422 assert!(!editor.hover_state.visible());
423 });
424
425 // Hover with keyboard has no delay
426 cx.set_state(indoc! {"
427 f|n test()
428 println!();"});
429 cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
430 let symbol_range = cx.lsp_range(indoc! {"
431 [fn] test()
432 println!();"});
433 cx.lsp
434 .handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
435 Ok(Some(lsp::Hover {
436 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
437 kind: lsp::MarkupKind::Markdown,
438 value: indoc! {"
439 # Some other basic docs
440 Some other test documentation"}
441 .to_string(),
442 }),
443 range: Some(symbol_range),
444 }))
445 })
446 .next()
447 .await;
448 cx.foreground().run_until_parked();
449 cx.editor(|editor, _| {
450 assert!(editor.hover_state.visible());
451 assert_eq!(
452 editor.hover_state.popover.clone().unwrap().contents,
453 vec![
454 HoverBlock {
455 text: "Some other basic docs".to_string(),
456 language: None
457 },
458 HoverBlock {
459 text: "Some other test documentation".to_string(),
460 language: None
461 }
462 ]
463 )
464 });
465 }
466}