]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/hover/render.rs
Merge #11194
[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::{CONDITION, 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     let keyword_mod = if token.kind() == T![fn] && ast::FnPtrType::cast(parent).is_some() {
243         // treat fn keyword inside function pointer type as primitive
244         format!("prim_{}", token.text())
245     } else {
246         // std exposes {}_keyword modules with docstrings on the root to document keywords
247         format!("{}_keyword", token.text())
248     };
249     let doc_owner = find_std_module(&famous_defs, &keyword_mod)?;
250     let docs = doc_owner.attrs(sema.db).docs()?;
251     let markup = process_markup(
252         sema.db,
253         Definition::Module(doc_owner),
254         &markup(Some(docs.into()), token.text().into(), None)?,
255         config,
256     );
257     Some(HoverResult { markup, actions: Default::default() })
258 }
259
260 pub(super) fn try_for_lint(attr: &ast::Attr, token: &SyntaxToken) -> Option<HoverResult> {
261     let (path, tt) = attr.as_simple_call()?;
262     if !tt.syntax().text_range().contains(token.text_range().start()) {
263         return None;
264     }
265     let (is_clippy, lints) = match &*path {
266         "feature" => (false, FEATURES),
267         "allow" | "deny" | "forbid" | "warn" => {
268             let is_clippy = algo::non_trivia_sibling(token.clone().into(), Direction::Prev)
269                 .filter(|t| t.kind() == T![:])
270                 .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev))
271                 .filter(|t| t.kind() == T![:])
272                 .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev))
273                 .map_or(false, |t| {
274                     t.kind() == T![ident] && t.into_token().map_or(false, |t| t.text() == "clippy")
275                 });
276             if is_clippy {
277                 (true, CLIPPY_LINTS)
278             } else {
279                 (false, DEFAULT_LINTS)
280             }
281         }
282         _ => return None,
283     };
284
285     let tmp;
286     let needle = if is_clippy {
287         tmp = format!("clippy::{}", token.text());
288         &tmp
289     } else {
290         &*token.text()
291     };
292
293     let lint =
294         lints.binary_search_by_key(&needle, |lint| lint.label).ok().map(|idx| &lints[idx])?;
295     Some(HoverResult {
296         markup: Markup::from(format!("```\n{}\n```\n___\n\n{}", lint.label, lint.description)),
297         ..Default::default()
298     })
299 }
300
301 pub(super) fn process_markup(
302     db: &RootDatabase,
303     def: Definition,
304     markup: &Markup,
305     config: &HoverConfig,
306 ) -> Markup {
307     let markup = markup.as_str();
308     let markup = if !config.markdown() {
309         remove_markdown(markup)
310     } else if config.links_in_hover {
311         rewrite_links(db, markup, def)
312     } else {
313         remove_links(markup)
314     };
315     Markup::from(markup)
316 }
317
318 fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> {
319     match def {
320         Definition::Field(f) => Some(f.parent_def(db).name(db)),
321         Definition::Local(l) => l.parent(db).name(db),
322         Definition::Function(f) => match f.as_assoc_item(db)?.container(db) {
323             hir::AssocItemContainer::Trait(t) => Some(t.name(db)),
324             hir::AssocItemContainer::Impl(i) => i.self_ty(db).as_adt().map(|adt| adt.name(db)),
325         },
326         Definition::Variant(e) => Some(e.parent_enum(db).name(db)),
327         _ => None,
328     }
329     .map(|name| name.to_string())
330 }
331
332 pub(super) fn path(db: &RootDatabase, module: hir::Module, item_name: Option<String>) -> String {
333     let crate_name =
334         db.crate_graph()[module.krate().into()].display_name.as_ref().map(|it| it.to_string());
335     let module_path = module
336         .path_to_root(db)
337         .into_iter()
338         .rev()
339         .flat_map(|it| it.name(db).map(|name| name.to_string()));
340     crate_name.into_iter().chain(module_path).chain(item_name).join("::")
341 }
342
343 pub(super) fn definition(
344     db: &RootDatabase,
345     def: Definition,
346     famous_defs: Option<&FamousDefs>,
347     config: &HoverConfig,
348 ) -> Option<Markup> {
349     let mod_path = definition_mod_path(db, &def);
350     let (label, docs) = match def {
351         Definition::Macro(it) => (
352             match &it.source(db)?.value {
353                 Either::Left(mac) => macro_label(mac),
354                 Either::Right(mac_fn) => fn_as_proc_macro_label(mac_fn),
355             },
356             it.attrs(db).docs(),
357         ),
358         Definition::Field(def) => label_and_docs(db, def),
359         Definition::Module(it) => label_and_docs(db, it),
360         Definition::Function(it) => label_and_docs(db, it),
361         Definition::Adt(it) => label_and_docs(db, it),
362         Definition::Variant(it) => label_and_docs(db, it),
363         Definition::Const(it) => label_value_and_docs(db, it, |it| {
364             let body = it.eval(db);
365             match body {
366                 Ok(x) => Some(format!("{}", x)),
367                 Err(_) => it.value(db).map(|x| format!("{}", x)),
368             }
369         }),
370         Definition::Static(it) => label_value_and_docs(db, it, |it| it.value(db)),
371         Definition::Trait(it) => label_and_docs(db, it),
372         Definition::TypeAlias(it) => label_and_docs(db, it),
373         Definition::BuiltinType(it) => {
374             return famous_defs
375                 .and_then(|fd| builtin(fd, it))
376                 .or_else(|| Some(Markup::fenced_block(&it.name())))
377         }
378         Definition::Local(it) => return local(db, it),
379         Definition::SelfType(impl_def) => {
380             impl_def.self_ty(db).as_adt().map(|adt| label_and_docs(db, adt))?
381         }
382         Definition::GenericParam(it) => label_and_docs(db, it),
383         Definition::Label(it) => return Some(Markup::fenced_block(&it.name(db))),
384         // FIXME: We should be able to show more info about these
385         Definition::BuiltinAttr(it) => return render_builtin_attr(db, it),
386         Definition::ToolModule(it) => return Some(Markup::fenced_block(&it.name(db))),
387     };
388
389     markup(docs.filter(|_| config.documentation.is_some()).map(Into::into), label, mod_path)
390 }
391
392 fn render_builtin_attr(db: &RootDatabase, attr: hir::BuiltinAttr) -> Option<Markup> {
393     let name = attr.name(db);
394     let desc = format!("#[{}]", name);
395
396     let AttributeTemplate { word, list, name_value_str } = match attr.template(db) {
397         Some(template) => template,
398         None => return Some(Markup::fenced_block(&attr.name(db))),
399     };
400     let mut docs = "Valid forms are:".to_owned();
401     if word {
402         format_to!(docs, "\n - #\\[{}]", name);
403     }
404     if let Some(list) = list {
405         format_to!(docs, "\n - #\\[{}({})]", name, list);
406     }
407     if let Some(name_value_str) = name_value_str {
408         format_to!(docs, "\n - #\\[{} = {}]", name, name_value_str);
409     }
410     markup(Some(docs.replace('*', "\\*")), desc, None)
411 }
412
413 fn label_and_docs<D>(db: &RootDatabase, def: D) -> (String, Option<hir::Documentation>)
414 where
415     D: HasAttrs + HirDisplay,
416 {
417     let label = def.display(db).to_string();
418     let docs = def.attrs(db).docs();
419     (label, docs)
420 }
421
422 fn label_value_and_docs<D, E, V>(
423     db: &RootDatabase,
424     def: D,
425     value_extractor: E,
426 ) -> (String, Option<hir::Documentation>)
427 where
428     D: HasAttrs + HirDisplay,
429     E: Fn(&D) -> Option<V>,
430     V: Display,
431 {
432     let label = if let Some(value) = value_extractor(&def) {
433         format!("{} = {}", def.display(db), value)
434     } else {
435         def.display(db).to_string()
436     };
437     let docs = def.attrs(db).docs();
438     (label, docs)
439 }
440
441 fn definition_mod_path(db: &RootDatabase, def: &Definition) -> Option<String> {
442     if let Definition::GenericParam(_) = def {
443         return None;
444     }
445     def.module(db).map(|module| path(db, module, definition_owner_name(db, def)))
446 }
447
448 fn markup(docs: Option<String>, desc: String, mod_path: Option<String>) -> Option<Markup> {
449     let mut buf = String::new();
450
451     if let Some(mod_path) = mod_path {
452         if !mod_path.is_empty() {
453             format_to!(buf, "```rust\n{}\n```\n\n", mod_path);
454         }
455     }
456     format_to!(buf, "```rust\n{}\n```", desc);
457
458     if let Some(doc) = docs {
459         format_to!(buf, "\n___\n\n{}", doc);
460     }
461     Some(buf.into())
462 }
463
464 fn builtin(famous_defs: &FamousDefs, builtin: hir::BuiltinType) -> Option<Markup> {
465     // std exposes prim_{} modules with docstrings on the root to document the builtins
466     let primitive_mod = format!("prim_{}", builtin.name());
467     let doc_owner = find_std_module(famous_defs, &primitive_mod)?;
468     let docs = doc_owner.attrs(famous_defs.0.db).docs()?;
469     markup(Some(docs.into()), builtin.name().to_string(), None)
470 }
471
472 fn find_std_module(famous_defs: &FamousDefs, name: &str) -> Option<hir::Module> {
473     let db = famous_defs.0.db;
474     let std_crate = famous_defs.std()?;
475     let std_root_module = std_crate.root_module(db);
476     std_root_module
477         .children(db)
478         .find(|module| module.name(db).map_or(false, |module| module.to_string() == name))
479 }
480
481 fn local(db: &RootDatabase, it: hir::Local) -> Option<Markup> {
482     let ty = it.ty(db);
483     let ty = ty.display_truncated(db, None);
484     let is_mut = if it.is_mut(db) { "mut " } else { "" };
485     let desc = match it.source(db).value {
486         Either::Left(ident) => {
487             let name = it.name(db).unwrap();
488             let let_kw = if ident
489                 .syntax()
490                 .parent()
491                 .map_or(false, |p| p.kind() == LET_STMT || p.kind() == CONDITION)
492             {
493                 "let "
494             } else {
495                 ""
496             };
497             format!("{}{}{}: {}", let_kw, is_mut, name, ty)
498         }
499         Either::Right(_) => format!("{}self: {}", is_mut, ty),
500     };
501     markup(None, desc, None)
502 }