]>
Commit | Line | Data |
---|---|---|
064997fb FG |
1 | mod render; |
2 | ||
3 | #[cfg(test)] | |
4 | mod tests; | |
5 | ||
6 | use std::iter; | |
7 | ||
8 | use either::Either; | |
9 | use hir::{HasSource, Semantics}; | |
10 | use ide_db::{ | |
11 | base_db::FileRange, | |
f2b60f7d | 12 | defs::{Definition, IdentClass, OperatorClass}, |
064997fb FG |
13 | famous_defs::FamousDefs, |
14 | helpers::pick_best_token, | |
15 | FxIndexSet, RootDatabase, | |
16 | }; | |
17 | use itertools::Itertools; | |
18 | use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, T}; | |
19 | ||
20 | use crate::{ | |
21 | doc_links::token_as_doc_comment, | |
22 | markup::Markup, | |
23 | runnables::{runnable_fn, runnable_mod}, | |
24 | FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav, | |
25 | }; | |
26 | #[derive(Clone, Debug, PartialEq, Eq)] | |
27 | pub struct HoverConfig { | |
28 | pub links_in_hover: bool, | |
29 | pub documentation: Option<HoverDocFormat>, | |
f2b60f7d | 30 | pub keywords: bool, |
064997fb FG |
31 | } |
32 | ||
33 | impl HoverConfig { | |
34 | fn markdown(&self) -> bool { | |
35 | matches!(self.documentation, Some(HoverDocFormat::Markdown)) | |
36 | } | |
37 | } | |
38 | ||
39 | #[derive(Clone, Debug, PartialEq, Eq)] | |
40 | pub enum HoverDocFormat { | |
41 | Markdown, | |
42 | PlainText, | |
43 | } | |
44 | ||
45 | #[derive(Debug, Clone)] | |
46 | pub enum HoverAction { | |
47 | Runnable(Runnable), | |
48 | Implementation(FilePosition), | |
49 | Reference(FilePosition), | |
50 | GoToType(Vec<HoverGotoTypeData>), | |
51 | } | |
52 | ||
53 | impl HoverAction { | |
54 | fn goto_type_from_targets(db: &RootDatabase, targets: Vec<hir::ModuleDef>) -> Self { | |
55 | let targets = targets | |
56 | .into_iter() | |
57 | .filter_map(|it| { | |
58 | Some(HoverGotoTypeData { | |
59 | mod_path: render::path( | |
60 | db, | |
61 | it.module(db)?, | |
62 | it.name(db).map(|name| name.to_string()), | |
63 | ), | |
64 | nav: it.try_to_nav(db)?, | |
65 | }) | |
66 | }) | |
67 | .collect(); | |
68 | HoverAction::GoToType(targets) | |
69 | } | |
70 | } | |
71 | ||
72 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] | |
73 | pub struct HoverGotoTypeData { | |
74 | pub mod_path: String, | |
75 | pub nav: NavigationTarget, | |
76 | } | |
77 | ||
78 | /// Contains the results when hovering over an item | |
79 | #[derive(Debug, Default)] | |
80 | pub struct HoverResult { | |
81 | pub markup: Markup, | |
82 | pub actions: Vec<HoverAction>, | |
83 | } | |
84 | ||
85 | // Feature: Hover | |
86 | // | |
87 | // Shows additional information, like the type of an expression or the documentation for a definition when "focusing" code. | |
88 | // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. | |
89 | // | |
90 | // image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[] | |
91 | pub(crate) fn hover( | |
92 | db: &RootDatabase, | |
93 | FileRange { file_id, range }: FileRange, | |
94 | config: &HoverConfig, | |
95 | ) -> Option<RangeInfo<HoverResult>> { | |
96 | let sema = &hir::Semantics::new(db); | |
97 | let file = sema.parse(file_id).syntax().clone(); | |
98 | ||
99 | if !range.is_empty() { | |
100 | return hover_ranged(&file, range, sema, config); | |
101 | } | |
102 | let offset = range.start(); | |
103 | ||
104 | let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind { | |
f2b60f7d FG |
105 | IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] | T![Self] => 4, |
106 | // index and prefix ops | |
107 | T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] => 3, | |
108 | kind if kind.is_keyword() => 2, | |
064997fb FG |
109 | T!['('] | T![')'] => 2, |
110 | kind if kind.is_trivia() => 0, | |
111 | _ => 1, | |
112 | })?; | |
113 | ||
114 | if let Some(doc_comment) = token_as_doc_comment(&original_token) { | |
115 | cov_mark::hit!(no_highlight_on_comment_hover); | |
116 | return doc_comment.get_definition_with_descend_at(sema, offset, |def, node, range| { | |
117 | let res = hover_for_definition(sema, file_id, def, &node, config)?; | |
118 | Some(RangeInfo::new(range, res)) | |
119 | }); | |
120 | } | |
121 | ||
487cf647 FG |
122 | let in_attr = original_token |
123 | .parent_ancestors() | |
124 | .filter_map(ast::Item::cast) | |
125 | .any(|item| sema.is_attr_macro_call(&item)) | |
126 | && !matches!( | |
127 | original_token.parent().and_then(ast::TokenTree::cast), | |
128 | Some(tt) if tt.syntax().ancestors().any(|it| ast::Meta::can_cast(it.kind())) | |
129 | ); | |
f2b60f7d FG |
130 | // prefer descending the same token kind in attribute expansions, in normal macros text |
131 | // equivalency is more important | |
064997fb FG |
132 | let descended = if in_attr { |
133 | [sema.descend_into_macros_with_kind_preference(original_token.clone())].into() | |
134 | } else { | |
135 | sema.descend_into_macros_with_same_text(original_token.clone()) | |
136 | }; | |
137 | ||
138 | // FIXME: Definition should include known lints and the like instead of having this special case here | |
139 | let hovered_lint = descended.iter().find_map(|token| { | |
140 | let attr = token.parent_ancestors().find_map(ast::Attr::cast)?; | |
141 | render::try_for_lint(&attr, token) | |
142 | }); | |
143 | if let Some(res) = hovered_lint { | |
144 | return Some(RangeInfo::new(original_token.text_range(), res)); | |
145 | } | |
146 | ||
147 | let result = descended | |
148 | .iter() | |
149 | .filter_map(|token| { | |
150 | let node = token.parent()?; | |
151 | let class = IdentClass::classify_token(sema, token)?; | |
f2b60f7d FG |
152 | if let IdentClass::Operator(OperatorClass::Await(_)) = class { |
153 | // It's better for us to fall back to the keyword hover here, | |
154 | // rendering poll is very confusing | |
155 | return None; | |
156 | } | |
064997fb FG |
157 | Some(class.definitions().into_iter().zip(iter::once(node).cycle())) |
158 | }) | |
159 | .flatten() | |
160 | .unique_by(|&(def, _)| def) | |
161 | .filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config)) | |
162 | .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| { | |
163 | acc.actions.extend(actions); | |
164 | acc.markup = Markup::from(format!("{}\n---\n{}", acc.markup, markup)); | |
165 | acc | |
166 | }); | |
167 | ||
168 | if result.is_none() { | |
169 | // fallbacks, show keywords or types | |
170 | ||
171 | let res = descended.iter().find_map(|token| render::keyword(sema, config, token)); | |
172 | if let Some(res) = res { | |
173 | return Some(RangeInfo::new(original_token.text_range(), res)); | |
174 | } | |
175 | let res = descended | |
176 | .iter() | |
177 | .find_map(|token| hover_type_fallback(sema, config, token, &original_token)); | |
178 | if let Some(_) = res { | |
179 | return res; | |
180 | } | |
181 | } | |
182 | result.map(|mut res: HoverResult| { | |
183 | res.actions = dedupe_or_merge_hover_actions(res.actions); | |
184 | RangeInfo::new(original_token.text_range(), res) | |
185 | }) | |
186 | } | |
187 | ||
188 | pub(crate) fn hover_for_definition( | |
189 | sema: &Semantics<'_, RootDatabase>, | |
190 | file_id: FileId, | |
191 | definition: Definition, | |
192 | node: &SyntaxNode, | |
193 | config: &HoverConfig, | |
194 | ) -> Option<HoverResult> { | |
195 | let famous_defs = match &definition { | |
196 | Definition::BuiltinType(_) => Some(FamousDefs(sema, sema.scope(node)?.krate())), | |
197 | _ => None, | |
198 | }; | |
199 | render::definition(sema.db, definition, famous_defs.as_ref(), config).map(|markup| { | |
200 | HoverResult { | |
201 | markup: render::process_markup(sema.db, definition, &markup, config), | |
202 | actions: show_implementations_action(sema.db, definition) | |
203 | .into_iter() | |
204 | .chain(show_fn_references_action(sema.db, definition)) | |
205 | .chain(runnable_action(sema, definition, file_id)) | |
206 | .chain(goto_type_action_for_def(sema.db, definition)) | |
207 | .collect(), | |
208 | } | |
209 | }) | |
210 | } | |
211 | ||
212 | fn hover_ranged( | |
213 | file: &SyntaxNode, | |
214 | range: syntax::TextRange, | |
215 | sema: &Semantics<'_, RootDatabase>, | |
216 | config: &HoverConfig, | |
217 | ) -> Option<RangeInfo<HoverResult>> { | |
218 | // FIXME: make this work in attributes | |
219 | let expr_or_pat = file.covering_element(range).ancestors().find_map(|it| { | |
220 | match_ast! { | |
221 | match it { | |
222 | ast::Expr(expr) => Some(Either::Left(expr)), | |
223 | ast::Pat(pat) => Some(Either::Right(pat)), | |
224 | _ => None, | |
225 | } | |
226 | } | |
227 | })?; | |
228 | let res = match &expr_or_pat { | |
229 | Either::Left(ast::Expr::TryExpr(try_expr)) => render::try_expr(sema, config, try_expr), | |
230 | Either::Left(ast::Expr::PrefixExpr(prefix_expr)) | |
231 | if prefix_expr.op_kind() == Some(ast::UnaryOp::Deref) => | |
232 | { | |
233 | render::deref_expr(sema, config, prefix_expr) | |
234 | } | |
235 | _ => None, | |
236 | }; | |
237 | let res = res.or_else(|| render::type_info(sema, config, &expr_or_pat)); | |
238 | res.map(|it| { | |
239 | let range = match expr_or_pat { | |
240 | Either::Left(it) => it.syntax().text_range(), | |
241 | Either::Right(it) => it.syntax().text_range(), | |
242 | }; | |
243 | RangeInfo::new(range, it) | |
244 | }) | |
245 | } | |
246 | ||
247 | fn hover_type_fallback( | |
248 | sema: &Semantics<'_, RootDatabase>, | |
249 | config: &HoverConfig, | |
250 | token: &SyntaxToken, | |
251 | original_token: &SyntaxToken, | |
252 | ) -> Option<RangeInfo<HoverResult>> { | |
f2b60f7d FG |
253 | let node = |
254 | token.parent_ancestors().take_while(|it| !ast::Item::can_cast(it.kind())).find(|n| { | |
255 | ast::Expr::can_cast(n.kind()) | |
256 | || ast::Pat::can_cast(n.kind()) | |
257 | || ast::Type::can_cast(n.kind()) | |
258 | })?; | |
064997fb FG |
259 | |
260 | let expr_or_pat = match_ast! { | |
261 | match node { | |
262 | ast::Expr(it) => Either::Left(it), | |
263 | ast::Pat(it) => Either::Right(it), | |
264 | // If this node is a MACRO_CALL, it means that `descend_into_macros_many` failed to resolve. | |
265 | // (e.g expanding a builtin macro). So we give up here. | |
266 | ast::MacroCall(_it) => return None, | |
267 | _ => return None, | |
268 | } | |
269 | }; | |
270 | ||
271 | let res = render::type_info(sema, config, &expr_or_pat)?; | |
272 | let range = sema | |
273 | .original_range_opt(&node) | |
274 | .map(|frange| frange.range) | |
275 | .unwrap_or_else(|| original_token.text_range()); | |
276 | Some(RangeInfo::new(range, res)) | |
277 | } | |
278 | ||
279 | fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { | |
280 | fn to_action(nav_target: NavigationTarget) -> HoverAction { | |
281 | HoverAction::Implementation(FilePosition { | |
282 | file_id: nav_target.file_id, | |
283 | offset: nav_target.focus_or_full_range().start(), | |
284 | }) | |
285 | } | |
286 | ||
287 | let adt = match def { | |
288 | Definition::Trait(it) => return it.try_to_nav(db).map(to_action), | |
289 | Definition::Adt(it) => Some(it), | |
290 | Definition::SelfType(it) => it.self_ty(db).as_adt(), | |
291 | _ => None, | |
292 | }?; | |
293 | adt.try_to_nav(db).map(to_action) | |
294 | } | |
295 | ||
296 | fn show_fn_references_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { | |
297 | match def { | |
298 | Definition::Function(it) => it.try_to_nav(db).map(|nav_target| { | |
299 | HoverAction::Reference(FilePosition { | |
300 | file_id: nav_target.file_id, | |
301 | offset: nav_target.focus_or_full_range().start(), | |
302 | }) | |
303 | }), | |
304 | _ => None, | |
305 | } | |
306 | } | |
307 | ||
308 | fn runnable_action( | |
309 | sema: &hir::Semantics<'_, RootDatabase>, | |
310 | def: Definition, | |
311 | file_id: FileId, | |
312 | ) -> Option<HoverAction> { | |
313 | match def { | |
314 | Definition::Module(it) => runnable_mod(sema, it).map(HoverAction::Runnable), | |
315 | Definition::Function(func) => { | |
316 | let src = func.source(sema.db)?; | |
317 | if src.file_id != file_id.into() { | |
318 | cov_mark::hit!(hover_macro_generated_struct_fn_doc_comment); | |
319 | cov_mark::hit!(hover_macro_generated_struct_fn_doc_attr); | |
320 | return None; | |
321 | } | |
322 | ||
323 | runnable_fn(sema, func).map(HoverAction::Runnable) | |
324 | } | |
325 | _ => None, | |
326 | } | |
327 | } | |
328 | ||
329 | fn goto_type_action_for_def(db: &RootDatabase, def: Definition) -> Option<HoverAction> { | |
330 | let mut targets: Vec<hir::ModuleDef> = Vec::new(); | |
331 | let mut push_new_def = |item: hir::ModuleDef| { | |
332 | if !targets.contains(&item) { | |
333 | targets.push(item); | |
334 | } | |
335 | }; | |
336 | ||
337 | if let Definition::GenericParam(hir::GenericParam::TypeParam(it)) = def { | |
338 | it.trait_bounds(db).into_iter().for_each(|it| push_new_def(it.into())); | |
339 | } else { | |
340 | let ty = match def { | |
341 | Definition::Local(it) => it.ty(db), | |
342 | Definition::GenericParam(hir::GenericParam::ConstParam(it)) => it.ty(db), | |
343 | Definition::Field(field) => field.ty(db), | |
344 | Definition::Function(function) => function.ret_type(db), | |
345 | _ => return None, | |
346 | }; | |
347 | ||
348 | walk_and_push_ty(db, &ty, &mut push_new_def); | |
349 | } | |
350 | ||
351 | Some(HoverAction::goto_type_from_targets(db, targets)) | |
352 | } | |
353 | ||
354 | fn walk_and_push_ty( | |
355 | db: &RootDatabase, | |
356 | ty: &hir::Type, | |
357 | push_new_def: &mut dyn FnMut(hir::ModuleDef), | |
358 | ) { | |
359 | ty.walk(db, |t| { | |
360 | if let Some(adt) = t.as_adt() { | |
361 | push_new_def(adt.into()); | |
362 | } else if let Some(trait_) = t.as_dyn_trait() { | |
363 | push_new_def(trait_.into()); | |
364 | } else if let Some(traits) = t.as_impl_traits(db) { | |
365 | traits.for_each(|it| push_new_def(it.into())); | |
366 | } else if let Some(trait_) = t.as_associated_type_parent_trait(db) { | |
367 | push_new_def(trait_.into()); | |
368 | } | |
369 | }); | |
370 | } | |
371 | ||
372 | fn dedupe_or_merge_hover_actions(actions: Vec<HoverAction>) -> Vec<HoverAction> { | |
373 | let mut deduped_actions = Vec::with_capacity(actions.len()); | |
374 | let mut go_to_type_targets = FxIndexSet::default(); | |
375 | ||
376 | let mut seen_implementation = false; | |
377 | let mut seen_reference = false; | |
378 | let mut seen_runnable = false; | |
379 | for action in actions { | |
380 | match action { | |
381 | HoverAction::GoToType(targets) => { | |
382 | go_to_type_targets.extend(targets); | |
383 | } | |
384 | HoverAction::Implementation(..) => { | |
385 | if !seen_implementation { | |
386 | seen_implementation = true; | |
387 | deduped_actions.push(action); | |
388 | } | |
389 | } | |
390 | HoverAction::Reference(..) => { | |
391 | if !seen_reference { | |
392 | seen_reference = true; | |
393 | deduped_actions.push(action); | |
394 | } | |
395 | } | |
396 | HoverAction::Runnable(..) => { | |
397 | if !seen_runnable { | |
398 | seen_runnable = true; | |
399 | deduped_actions.push(action); | |
400 | } | |
401 | } | |
402 | }; | |
403 | } | |
404 | ||
405 | if !go_to_type_targets.is_empty() { | |
406 | deduped_actions.push(HoverAction::GoToType(go_to_type_targets.into_iter().collect())); | |
407 | } | |
408 | ||
409 | deduped_actions | |
410 | } |