]> git.proxmox.com Git - rustc.git/blame - src/tools/rust-analyzer/crates/ide/src/hover.rs
New upstream version 1.67.1+dfsg1
[rustc.git] / src / tools / rust-analyzer / crates / ide / src / hover.rs
CommitLineData
064997fb
FG
1mod render;
2
3#[cfg(test)]
4mod tests;
5
6use std::iter;
7
8use either::Either;
9use hir::{HasSource, Semantics};
10use 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};
17use itertools::Itertools;
18use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, T};
19
20use 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)]
27pub struct HoverConfig {
28 pub links_in_hover: bool,
29 pub documentation: Option<HoverDocFormat>,
f2b60f7d 30 pub keywords: bool,
064997fb
FG
31}
32
33impl HoverConfig {
34 fn markdown(&self) -> bool {
35 matches!(self.documentation, Some(HoverDocFormat::Markdown))
36 }
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub enum HoverDocFormat {
41 Markdown,
42 PlainText,
43}
44
45#[derive(Debug, Clone)]
46pub enum HoverAction {
47 Runnable(Runnable),
48 Implementation(FilePosition),
49 Reference(FilePosition),
50 GoToType(Vec<HoverGotoTypeData>),
51}
52
53impl 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)]
73pub 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)]
80pub 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[]
91pub(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
188pub(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
212fn 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
247fn 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
279fn 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
296fn 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
308fn 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
329fn 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
354fn 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
372fn 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}