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