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.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 // query the LSP for hover info
119 let hover_request = project.update(cx, |project, cx| {
120 project.hover(&buffer, buffer_position.clone(), cx)
121 });
122
123 // We should only delay if the hover popover isn't visible, it wasn't recently hidden, and
124 // the hover wasn't triggered from the keyboard
125 let should_delay = editor.hover_state.popover.is_none() // Hover not visible currently
126 && editor
127 .hover_state
128 .hidden_at
129 .map(|hidden| hidden.elapsed().as_millis() > HOVER_GRACE_MILLIS as u128)
130 .unwrap_or(true) // Hover wasn't recently visible
131 && !ignore_timeout; // Hover was not triggered from keyboard
132
133 if should_delay {
134 if let Some(range) = &editor.hover_state.symbol_range {
135 if range
136 .to_offset(&snapshot.buffer_snapshot)
137 .contains(&multibuffer_offset)
138 {
139 // Hover triggered from same location as last time. Don't show again.
140 return;
141 }
142 }
143 }
144
145 // Get input anchor
146 let anchor = snapshot
147 .buffer_snapshot
148 .anchor_at(multibuffer_offset, Bias::Left);
149
150 let task = cx.spawn_weak(|this, mut cx| {
151 async move {
152 // If we need to delay, delay a set amount initially before making the lsp request
153 let delay = if should_delay {
154 // Construct delay task to wait for later
155 let total_delay = Some(
156 cx.background()
157 .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
158 );
159
160 cx.background()
161 .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
162 .await;
163 total_delay
164 } else {
165 None
166 };
167
168 // Construct new hover popover from hover request
169 let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
170 if hover_result.contents.is_empty() {
171 return None;
172 }
173
174 // Create symbol range of anchors for highlighting and filtering
175 // of future requests.
176 let range = if let Some(range) = hover_result.range {
177 let start = snapshot
178 .buffer_snapshot
179 .anchor_in_excerpt(excerpt_id.clone(), range.start);
180 let end = snapshot
181 .buffer_snapshot
182 .anchor_in_excerpt(excerpt_id.clone(), range.end);
183
184 start..end
185 } else {
186 anchor.clone()..anchor.clone()
187 };
188
189 if let Some(this) = this.upgrade(&cx) {
190 this.update(&mut cx, |this, _| {
191 this.hover_state.symbol_range = Some(range.clone());
192 });
193 }
194
195 Some(HoverPopover {
196 project: project.clone(),
197 anchor: range.start.clone(),
198 contents: hover_result.contents,
199 })
200 });
201
202 if let Some(delay) = delay {
203 delay.await;
204 }
205
206 if let Some(this) = this.upgrade(&cx) {
207 this.update(&mut cx, |this, cx| {
208 if hover_popover.is_some() {
209 // Highlight the selected symbol using a background highlight
210 if let Some(range) = this.hover_state.symbol_range.clone() {
211 this.highlight_background::<HoverState>(
212 vec![range],
213 |theme| theme.editor.hover_popover.highlight,
214 cx,
215 );
216 }
217 this.hover_state.popover = hover_popover;
218 cx.notify();
219 } else {
220 if this.hover_state.popover.is_some() {
221 // Popover was visible, but now is hidden. Dismiss it
222 hide_hover(this, cx);
223 } else {
224 // Clear selected symbol range for future requests
225 this.hover_state.symbol_range = None;
226 }
227 }
228 });
229 }
230 Ok::<_, anyhow::Error>(())
231 }
232 .log_err()
233 });
234
235 editor.hover_state.task = Some(task);
236}
237
238#[derive(Default)]
239pub struct HoverState {
240 pub popover: Option<HoverPopover>,
241 pub hidden_at: Option<Instant>,
242 pub symbol_range: Option<Range<Anchor>>,
243 pub task: Option<Task<Option<()>>>,
244}
245
246impl HoverState {
247 pub fn visible(&self) -> bool {
248 self.popover.is_some()
249 }
250}
251
252#[derive(Debug, Clone)]
253pub struct HoverPopover {
254 pub project: ModelHandle<Project>,
255 pub anchor: Anchor,
256 pub contents: Vec<HoverBlock>,
257}
258
259impl HoverPopover {
260 pub fn render(
261 &self,
262 snapshot: &EditorSnapshot,
263 style: EditorStyle,
264 cx: &mut RenderContext<Editor>,
265 ) -> (DisplayPoint, ElementBox) {
266 let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
267 let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
268 flex.extend(self.contents.iter().map(|content| {
269 let project = self.project.read(cx);
270 if let Some(language) = content
271 .language
272 .clone()
273 .and_then(|language| project.languages().get_language(&language))
274 {
275 let runs = language
276 .highlight_text(&content.text.as_str().into(), 0..content.text.len());
277
278 Text::new(content.text.clone(), style.text.clone())
279 .with_soft_wrap(true)
280 .with_highlights(
281 runs.iter()
282 .filter_map(|(range, id)| {
283 id.style(style.theme.syntax.as_ref())
284 .map(|style| (range.clone(), style))
285 })
286 .collect(),
287 )
288 .boxed()
289 } else {
290 let mut text_style = style.hover_popover.prose.clone();
291 text_style.font_size = style.text.font_size;
292
293 Text::new(content.text.clone(), text_style)
294 .with_soft_wrap(true)
295 .contained()
296 .with_style(style.hover_popover.block_style)
297 .boxed()
298 }
299 }));
300 flex.contained()
301 .with_style(style.hover_popover.container)
302 .boxed()
303 })
304 .with_cursor_style(CursorStyle::Arrow)
305 .with_padding(Padding {
306 bottom: 5.,
307 top: 5.,
308 ..Default::default()
309 })
310 .boxed();
311
312 let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
313 (display_point, element)
314 }
315}