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