]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide/src/hover/render.rs
Rollup merge of #101759 - lnicola:rust-analyzer-2022-09-13, r=lnicola
[rust.git] / src / tools / rust-analyzer / crates / ide / src / hover / render.rs
1 //! Logic for rendering the different hover messages
2 use std::fmt::Display;
3
4 use either::Either;
5 use hir::{AsAssocItem, AttributeTemplate, HasAttrs, HasSource, HirDisplay, Semantics, TypeInfo};
6 use ide_db::{
7     base_db::SourceDatabase,
8     defs::Definition,
9     famous_defs::FamousDefs,
10     generated::lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES},
11     syntax_helpers::insert_whitespace_into_node,
12     RootDatabase,
13 };
14 use itertools::Itertools;
15 use stdx::format_to;
16 use syntax::{
17     algo, ast, match_ast, AstNode, Direction,
18     SyntaxKind::{LET_EXPR, LET_STMT},
19     SyntaxToken, T,
20 };
21
22 use crate::{
23     doc_links::{remove_links, rewrite_links},
24     hover::walk_and_push_ty,
25     markdown_remove::remove_markdown,
26     HoverAction, HoverConfig, HoverResult, Markup,
27 };
28
29 pub(super) fn type_info(
30     sema: &Semantics<'_, RootDatabase>,
31     config: &HoverConfig,
32     expr_or_pat: &Either<ast::Expr, ast::Pat>,
33 ) -> Option<HoverResult> {
34     let TypeInfo { original, adjusted } = match expr_or_pat {
35         Either::Left(expr) => sema.type_of_expr(expr)?,
36         Either::Right(pat) => sema.type_of_pat(pat)?,
37     };
38
39     let mut res = HoverResult::default();
40     let mut targets: Vec<hir::ModuleDef> = Vec::new();
41     let mut push_new_def = |item: hir::ModuleDef| {
42         if !targets.contains(&item) {
43             targets.push(item);
44         }
45     };
46     walk_and_push_ty(sema.db, &original, &mut push_new_def);
47
48     res.markup = if let Some(adjusted_ty) = adjusted {
49         walk_and_push_ty(sema.db, &adjusted_ty, &mut push_new_def);
50         let original = original.display(sema.db).to_string();
51         let adjusted = adjusted_ty.display(sema.db).to_string();
52         let static_text_diff_len = "Coerced to: ".len() - "Type: ".len();
53         format!(
54             "{bt_start}Type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}",
55             original,
56             adjusted,
57             apad = static_text_diff_len + adjusted.len().max(original.len()),
58             opad = original.len(),
59             bt_start = if config.markdown() { "```text\n" } else { "" },
60             bt_end = if config.markdown() { "```\n" } else { "" }
61         )
62         .into()
63     } else {
64         if config.markdown() {
65             Markup::fenced_block(&original.display(sema.db))
66         } else {
67             original.display(sema.db).to_string().into()
68         }
69     };
70     res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
71     Some(res)
72 }
73
74 pub(super) fn try_expr(
75     sema: &Semantics<'_, RootDatabase>,
76     config: &HoverConfig,
77     try_expr: &ast::TryExpr,
78 ) -> Option<HoverResult> {
79     let inner_ty = sema.type_of_expr(&try_expr.expr()?)?.original;
80     let mut ancestors = try_expr.syntax().ancestors();
81     let mut body_ty = loop {
82         let next = ancestors.next()?;
83         break match_ast! {
84             match next {
85                 ast::Fn(fn_) => sema.to_def(&fn_)?.ret_type(sema.db),
86                 ast::Item(__) => return None,
87                 ast::ClosureExpr(closure) => sema.type_of_expr(&closure.body()?)?.original,
88                 ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) {
89                     sema.type_of_expr(&block_expr.into())?.original
90                 } else {
91                     continue;
92                 },
93                 _ => continue,
94             }
95         };
96     };
97
98     if inner_ty == body_ty {
99         return None;
100     }
101
102     let mut inner_ty = inner_ty;
103     let mut s = "Try Target".to_owned();
104
105     let adts = inner_ty.as_adt().zip(body_ty.as_adt());
106     if let Some((hir::Adt::Enum(inner), hir::Adt::Enum(body))) = adts {
107         let famous_defs = FamousDefs(sema, sema.scope(try_expr.syntax())?.krate());
108         // special case for two options, there is no value in showing them
109         if let Some(option_enum) = famous_defs.core_option_Option() {
110             if inner == option_enum && body == option_enum {
111                 cov_mark::hit!(hover_try_expr_opt_opt);
112                 return None;
113             }
114         }
115
116         // special case two results to show the error variants only
117         if let Some(result_enum) = famous_defs.core_result_Result() {
118             if inner == result_enum && body == result_enum {
119                 let error_type_args =
120                     inner_ty.type_arguments().nth(1).zip(body_ty.type_arguments().nth(1));
121                 if let Some((inner, body)) = error_type_args {
122                     inner_ty = inner;
123                     body_ty = body;
124                     s = "Try Error".to_owned();
125                 }
126             }
127         }
128     }
129
130     let mut res = HoverResult::default();
131
132     let mut targets: Vec<hir::ModuleDef> = Vec::new();
133     let mut push_new_def = |item: hir::ModuleDef| {
134         if !targets.contains(&item) {
135             targets.push(item);
136         }
137     };
138     walk_and_push_ty(sema.db, &inner_ty, &mut push_new_def);
139     walk_and_push_ty(sema.db, &body_ty, &mut push_new_def);
140     res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
141
142     let inner_ty = inner_ty.display(sema.db).to_string();
143     let body_ty = body_ty.display(sema.db).to_string();
144     let ty_len_max = inner_ty.len().max(body_ty.len());
145
146     let l = "Propagated as: ".len() - " Type: ".len();
147     let static_text_len_diff = l as isize - s.len() as isize;
148     let tpad = static_text_len_diff.max(0) as usize;
149     let ppad = static_text_len_diff.min(0).abs() as usize;
150
151     res.markup = format!(
152         "{bt_start}{} Type: {:>pad0$}\nPropagated as: {:>pad1$}\n{bt_end}",
153         s,
154         inner_ty,
155         body_ty,
156         pad0 = ty_len_max + tpad,
157         pad1 = ty_len_max + ppad,
158         bt_start = if config.markdown() { "```text\n" } else { "" },
159         bt_end = if config.markdown() { "```\n" } else { "" }
160     )
161     .into();
162     Some(res)
163 }
164
165 pub(super) fn deref_expr(
166     sema: &Semantics<'_, RootDatabase>,
167     config: &HoverConfig,
168     deref_expr: &ast::PrefixExpr,
169 ) -> Option<HoverResult> {
170     let inner_ty = sema.type_of_expr(&deref_expr.expr()?)?.original;
171     let TypeInfo { original, adjusted } =
172         sema.type_of_expr(&ast::Expr::from(deref_expr.clone()))?;
173
174     let mut res = HoverResult::default();
175     let mut targets: Vec<hir::ModuleDef> = Vec::new();
176     let mut push_new_def = |item: hir::ModuleDef| {
177         if !targets.contains(&item) {
178             targets.push(item);
179         }
180     };
181     walk_and_push_ty(sema.db, &inner_ty, &mut push_new_def);
182     walk_and_push_ty(sema.db, &original, &mut push_new_def);
183
184     res.markup = if let Some(adjusted_ty) = adjusted {
185         walk_and_push_ty(sema.db, &adjusted_ty, &mut push_new_def);
186         let original = original.display(sema.db).to_string();
187         let adjusted = adjusted_ty.display(sema.db).to_string();
188         let inner = inner_ty.display(sema.db).to_string();
189         let type_len = "To type: ".len();
190         let coerced_len = "Coerced to: ".len();
191         let deref_len = "Dereferenced from: ".len();
192         let max_len = (original.len() + type_len)
193             .max(adjusted.len() + coerced_len)
194             .max(inner.len() + deref_len);
195         format!(
196             "{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}",
197             inner,
198             original,
199             adjusted,
200             ipad = max_len - deref_len,
201             apad = max_len - type_len,
202             opad = max_len - coerced_len,
203             bt_start = if config.markdown() { "```text\n" } else { "" },
204             bt_end = if config.markdown() { "```\n" } else { "" }
205         )
206         .into()
207     } else {
208         let original = original.display(sema.db).to_string();
209         let inner = inner_ty.display(sema.db).to_string();
210         let type_len = "To type: ".len();
211         let deref_len = "Dereferenced from: ".len();
212         let max_len = (original.len() + type_len).max(inner.len() + deref_len);
213         format!(
214             "{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\n{bt_end}",
215             inner,
216             original,
217             ipad = max_len - deref_len,
218             apad = max_len - type_len,
219             bt_start = if config.markdown() { "```text\n" } else { "" },
220             bt_end = if config.markdown() { "```\n" } else { "" }
221         )
222         .into()
223     };
224     res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
225
226     Some(res)
227 }
228
229 pub(super) fn keyword(
230     sema: &Semantics<'_, RootDatabase>,
231     config: &HoverConfig,
232     token: &SyntaxToken,
233 ) -> Option<HoverResult> {
234     if !token.kind().is_keyword() || !config.documentation.is_some() || !config.keywords {
235         return None;
236     }
237     let parent = token.parent()?;
238     let famous_defs = FamousDefs(sema, sema.scope(&parent)?.krate());
239
240     let KeywordHint { description, keyword_mod, actions } = keyword_hints(sema, token, parent);
241
242     let doc_owner = find_std_module(&famous_defs, &keyword_mod)?;
243     let docs = doc_owner.attrs(sema.db).docs()?;
244     let markup = process_markup(
245         sema.db,
246         Definition::Module(doc_owner),
247         &markup(Some(docs.into()), description, None)?,
248         config,
249     );
250     Some(HoverResult { markup, actions })
251 }
252
253 pub(super) fn try_for_lint(attr: &ast::Attr, token: &SyntaxToken) -> Option<HoverResult> {
254     let (path, tt) = attr.as_simple_call()?;
255     if !tt.syntax().text_range().contains(token.text_range().start()) {
256         return None;
257     }
258     let (is_clippy, lints) = match &*path {
259         "feature" => (false, FEATURES),
260         "allow" | "deny" | "forbid" | "warn" => {
261             let is_clippy = algo::non_trivia_sibling(token.clone().into(), Direction::Prev)
262                 .filter(|t| t.kind() == T![:])
263                 .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev))
264                 .filter(|t| t.kind() == T![:])
265                 .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev))
266                 .map_or(false, |t| {
267                     t.kind() == T![ident] && t.into_token().map_or(false, |t| t.text() == "clippy")
268                 });
269             if is_clippy {
270                 (true, CLIPPY_LINTS)
271             } else {
272                 (false, DEFAULT_LINTS)
273             }
274         }
275         _ => return None,
276     };
277
278     let tmp;
279     let needle = if is_clippy {
280         tmp = format!("clippy::{}", token.text());
281         &tmp
282     } else {
283         &*token.text()
284     };
285
286     let lint =
287         lints.binary_search_by_key(&needle, |lint| lint.label).ok().map(|idx| &lints[idx])?;
288     Some(HoverResult {
289         markup: Markup::from(format!("```\n{}\n```\n___\n\n{}", lint.label, lint.description)),
290         ..Default::default()
291     })
292 }
293
294 pub(super) fn process_markup(
295     db: &RootDatabase,
296     def: Definition,
297     markup: &Markup,
298     config: &HoverConfig,
299 ) -> Markup {
300     let markup = markup.as_str();
301     let markup = if !config.markdown() {
302         remove_markdown(markup)
303     } else if config.links_in_hover {
304         rewrite_links(db, markup, def)
305     } else {
306         remove_links(markup)
307     };
308     Markup::from(markup)
309 }
310
311 fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> {
312     match def {
313         Definition::Field(f) => Some(f.parent_def(db).name(db)),
314         Definition::Local(l) => l.parent(db).name(db),
315         Definition::Function(f) => match f.as_assoc_item(db)?.container(db) {
316             hir::AssocItemContainer::Trait(t) => Some(t.name(db)),
317             hir::AssocItemContainer::Impl(i) => i.self_ty(db).as_adt().map(|adt| adt.name(db)),
318         },
319         Definition::Variant(e) => Some(e.parent_enum(db).name(db)),
320         _ => None,
321     }
322     .map(|name| name.to_string())
323 }
324
325 pub(super) fn path(db: &RootDatabase, module: hir::Module, item_name: Option<String>) -> String {
326     let crate_name =
327         db.crate_graph()[module.krate().into()].display_name.as_ref().map(|it| it.to_string());
328     let module_path = module
329         .path_to_root(db)
330         .into_iter()
331         .rev()
332         .flat_map(|it| it.name(db).map(|name| name.to_string()));
333     crate_name.into_iter().chain(module_path).chain(item_name).join("::")
334 }
335
336 pub(super) fn definition(
337     db: &RootDatabase,
338     def: Definition,
339     famous_defs: Option<&FamousDefs<'_, '_>>,
340     config: &HoverConfig,
341 ) -> Option<Markup> {
342     let mod_path = definition_mod_path(db, &def);
343     let (label, docs) = match def {
344         Definition::Macro(it) => label_and_docs(db, it),
345         Definition::Field(it) => label_and_docs(db, it),
346         Definition::Module(it) => label_and_docs(db, it),
347         Definition::Function(it) => label_and_docs(db, it),
348         Definition::Adt(it) => label_and_docs(db, it),
349         Definition::Variant(it) => label_and_docs(db, it),
350         Definition::Const(it) => label_value_and_docs(db, it, |it| {
351             let body = it.eval(db);
352             match body {
353                 Ok(x) => Some(format!("{}", x)),
354                 Err(_) => {
355                     let source = it.source(db)?;
356                     let mut body = source.value.body()?.syntax().clone();
357                     if source.file_id.is_macro() {
358                         body = insert_whitespace_into_node::insert_ws_into(body);
359                     }
360                     Some(body.to_string())
361                 }
362             }
363         }),
364         Definition::Static(it) => label_value_and_docs(db, it, |it| {
365             let source = it.source(db)?;
366             let mut body = source.value.body()?.syntax().clone();
367             if source.file_id.is_macro() {
368                 body = insert_whitespace_into_node::insert_ws_into(body);
369             }
370             Some(body.to_string())
371         }),
372         Definition::Trait(it) => label_and_docs(db, it),
373         Definition::TypeAlias(it) => label_and_docs(db, it),
374         Definition::BuiltinType(it) => {
375             return famous_defs
376                 .and_then(|fd| builtin(fd, it))
377                 .or_else(|| Some(Markup::fenced_block(&it.name())))
378         }
379         Definition::Local(it) => return local(db, it),
380         Definition::SelfType(impl_def) => {
381             impl_def.self_ty(db).as_adt().map(|adt| label_and_docs(db, adt))?
382         }
383         Definition::GenericParam(it) => label_and_docs(db, it),
384         Definition::Label(it) => return Some(Markup::fenced_block(&it.name(db))),
385         // FIXME: We should be able to show more info about these
386         Definition::BuiltinAttr(it) => return render_builtin_attr(db, it),
387         Definition::ToolModule(it) => return Some(Markup::fenced_block(&it.name(db))),
388         Definition::DeriveHelper(it) => (format!("derive_helper {}", it.name(db)), None),
389     };
390
391     let docs = match config.documentation {
392         Some(_) => docs.or_else(|| {
393             // docs are missing, for assoc items of trait impls try to fall back to the docs of the
394             // original item of the trait
395             let assoc = def.as_assoc_item(db)?;
396             let trait_ = assoc.containing_trait_impl(db)?;
397             let name = Some(assoc.name(db)?);
398             let item = trait_.items(db).into_iter().find(|it| it.name(db) == name)?;
399             item.docs(db)
400         }),
401         None => None,
402     };
403     let docs = docs.filter(|_| config.documentation.is_some()).map(Into::into);
404     markup(docs, label, mod_path)
405 }
406
407 fn render_builtin_attr(db: &RootDatabase, attr: hir::BuiltinAttr) -> Option<Markup> {
408     let name = attr.name(db);
409     let desc = format!("#[{}]", name);
410
411     let AttributeTemplate { word, list, name_value_str } = match attr.template(db) {
412         Some(template) => template,
413         None => return Some(Markup::fenced_block(&attr.name(db))),
414     };
415     let mut docs = "Valid forms are:".to_owned();
416     if word {
417         format_to!(docs, "\n - #\\[{}]", name);
418     }
419     if let Some(list) = list {
420         format_to!(docs, "\n - #\\[{}({})]", name, list);
421     }
422     if let Some(name_value_str) = name_value_str {
423         format_to!(docs, "\n - #\\[{} = {}]", name, name_value_str);
424     }
425     markup(Some(docs.replace('*', "\\*")), desc, None)
426 }
427
428 fn label_and_docs<D>(db: &RootDatabase, def: D) -> (String, Option<hir::Documentation>)
429 where
430     D: HasAttrs + HirDisplay,
431 {
432     let label = def.display(db).to_string();
433     let docs = def.attrs(db).docs();
434     (label, docs)
435 }
436
437 fn label_value_and_docs<D, E, V>(
438     db: &RootDatabase,
439     def: D,
440     value_extractor: E,
441 ) -> (String, Option<hir::Documentation>)
442 where
443     D: HasAttrs + HirDisplay,
444     E: Fn(&D) -> Option<V>,
445     V: Display,
446 {
447     let label = if let Some(value) = value_extractor(&def) {
448         format!("{} = {}", def.display(db), value)
449     } else {
450         def.display(db).to_string()
451     };
452     let docs = def.attrs(db).docs();
453     (label, docs)
454 }
455
456 fn definition_mod_path(db: &RootDatabase, def: &Definition) -> Option<String> {
457     if let Definition::GenericParam(_) = def {
458         return None;
459     }
460     def.module(db).map(|module| path(db, module, definition_owner_name(db, def)))
461 }
462
463 fn markup(docs: Option<String>, desc: String, mod_path: Option<String>) -> Option<Markup> {
464     let mut buf = String::new();
465
466     if let Some(mod_path) = mod_path {
467         if !mod_path.is_empty() {
468             format_to!(buf, "```rust\n{}\n```\n\n", mod_path);
469         }
470     }
471     format_to!(buf, "```rust\n{}\n```", desc);
472
473     if let Some(doc) = docs {
474         format_to!(buf, "\n___\n\n{}", doc);
475     }
476     Some(buf.into())
477 }
478
479 fn builtin(famous_defs: &FamousDefs<'_, '_>, builtin: hir::BuiltinType) -> Option<Markup> {
480     // std exposes prim_{} modules with docstrings on the root to document the builtins
481     let primitive_mod = format!("prim_{}", builtin.name());
482     let doc_owner = find_std_module(famous_defs, &primitive_mod)?;
483     let docs = doc_owner.attrs(famous_defs.0.db).docs()?;
484     markup(Some(docs.into()), builtin.name().to_string(), None)
485 }
486
487 fn find_std_module(famous_defs: &FamousDefs<'_, '_>, name: &str) -> Option<hir::Module> {
488     let db = famous_defs.0.db;
489     let std_crate = famous_defs.std()?;
490     let std_root_module = std_crate.root_module(db);
491     std_root_module
492         .children(db)
493         .find(|module| module.name(db).map_or(false, |module| module.to_string() == name))
494 }
495
496 fn local(db: &RootDatabase, it: hir::Local) -> Option<Markup> {
497     let ty = it.ty(db);
498     let ty = ty.display_truncated(db, None);
499     let is_mut = if it.is_mut(db) { "mut " } else { "" };
500     let desc = match it.source(db).value {
501         Either::Left(ident) => {
502             let name = it.name(db);
503             let let_kw = if ident
504                 .syntax()
505                 .parent()
506                 .map_or(false, |p| p.kind() == LET_STMT || p.kind() == LET_EXPR)
507             {
508                 "let "
509             } else {
510                 ""
511             };
512             format!("{}{}{}: {}", let_kw, is_mut, name, ty)
513         }
514         Either::Right(_) => format!("{}self: {}", is_mut, ty),
515     };
516     markup(None, desc, None)
517 }
518
519 struct KeywordHint {
520     description: String,
521     keyword_mod: String,
522     actions: Vec<HoverAction>,
523 }
524
525 impl KeywordHint {
526     fn new(description: String, keyword_mod: String) -> Self {
527         Self { description, keyword_mod, actions: Vec::default() }
528     }
529 }
530
531 fn keyword_hints(
532     sema: &Semantics<'_, RootDatabase>,
533     token: &SyntaxToken,
534     parent: syntax::SyntaxNode,
535 ) -> KeywordHint {
536     match token.kind() {
537         T![await] | T![loop] | T![match] | T![unsafe] | T![as] | T![try] | T![if] | T![else] => {
538             let keyword_mod = format!("{}_keyword", token.text());
539
540             match ast::Expr::cast(parent).and_then(|site| sema.type_of_expr(&site)) {
541                 // ignore the unit type ()
542                 Some(ty) if !ty.adjusted.as_ref().unwrap_or(&ty.original).is_unit() => {
543                     let mut targets: Vec<hir::ModuleDef> = Vec::new();
544                     let mut push_new_def = |item: hir::ModuleDef| {
545                         if !targets.contains(&item) {
546                             targets.push(item);
547                         }
548                     };
549                     walk_and_push_ty(sema.db, &ty.original, &mut push_new_def);
550
551                     let ty = ty.adjusted();
552                     let description = format!("{}: {}", token.text(), ty.display(sema.db));
553
554                     KeywordHint {
555                         description,
556                         keyword_mod,
557                         actions: vec![HoverAction::goto_type_from_targets(sema.db, targets)],
558                     }
559                 }
560                 _ => KeywordHint {
561                     description: token.text().to_string(),
562                     keyword_mod,
563                     actions: Vec::new(),
564                 },
565             }
566         }
567         T![fn] => {
568             let module = match ast::FnPtrType::cast(parent) {
569                 // treat fn keyword inside function pointer type as primitive
570                 Some(_) => format!("prim_{}", token.text()),
571                 None => format!("{}_keyword", token.text()),
572             };
573             KeywordHint::new(token.text().to_string(), module)
574         }
575         T![Self] => KeywordHint::new(token.text().to_string(), "self_upper_keyword".into()),
576         _ => KeywordHint::new(token.text().to_string(), format!("{}_keyword", token.text())),
577     }
578 }