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