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