]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/hover/render.rs
Merge #10360
[rust.git] / crates / ide / src / hover / render.rs
1 //! Logic for rendering the different hover messages
2 use either::Either;
3 use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics, TypeInfo};
4 use ide_db::{
5     base_db::SourceDatabase,
6     defs::Definition,
7     helpers::{
8         generated_lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES},
9         FamousDefs,
10     },
11     RootDatabase,
12 };
13 use itertools::Itertools;
14 use stdx::format_to;
15 use syntax::{
16     algo, ast,
17     display::{fn_as_proc_macro_label, macro_label},
18     match_ast, AstNode, Direction,
19     SyntaxKind::{CONDITION, LET_STMT},
20     SyntaxToken, T,
21 };
22
23 use crate::{
24     doc_links::{remove_links, rewrite_links},
25     hover::walk_and_push_ty,
26     markdown_remove::remove_markdown,
27     HoverAction, HoverConfig, HoverResult, Markup,
28 };
29
30 pub(super) fn type_info(
31     sema: &Semantics<RootDatabase>,
32     config: &HoverConfig,
33     expr_or_pat: &Either<ast::Expr, ast::Pat>,
34 ) -> Option<HoverResult> {
35     let TypeInfo { original, adjusted } = match expr_or_pat {
36         Either::Left(expr) => sema.type_of_expr(expr)?,
37         Either::Right(pat) => sema.type_of_pat(pat)?,
38     };
39
40     let mut res = HoverResult::default();
41     let mut targets: Vec<hir::ModuleDef> = Vec::new();
42     let mut push_new_def = |item: hir::ModuleDef| {
43         if !targets.contains(&item) {
44             targets.push(item);
45         }
46     };
47     walk_and_push_ty(sema.db, &original, &mut push_new_def);
48
49     res.markup = if let Some(adjusted_ty) = adjusted {
50         walk_and_push_ty(sema.db, &adjusted_ty, &mut push_new_def);
51         let original = original.display(sema.db).to_string();
52         let adjusted = adjusted_ty.display(sema.db).to_string();
53         let static_text_diff_len = "Coerced to: ".len() - "Type: ".len();
54         format!(
55             "{bt_start}Type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}",
56             original,
57             adjusted,
58             apad = static_text_diff_len + adjusted.len().max(original.len()),
59             opad = original.len(),
60             bt_start = if config.markdown() { "```text\n" } else { "" },
61             bt_end = if config.markdown() { "```\n" } else { "" }
62         )
63         .into()
64     } else {
65         if config.markdown() {
66             Markup::fenced_block(&original.display(sema.db))
67         } else {
68             original.display(sema.db).to_string().into()
69         }
70     };
71     res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
72     Some(res)
73 }
74
75 pub(super) fn try_expr(
76     sema: &Semantics<RootDatabase>,
77     config: &HoverConfig,
78     try_expr: &ast::TryExpr,
79 ) -> Option<HoverResult> {
80     let inner_ty = sema.type_of_expr(&try_expr.expr()?)?.original;
81     let mut ancestors = try_expr.syntax().ancestors();
82     let mut body_ty = loop {
83         let next = ancestors.next()?;
84         break match_ast! {
85             match next {
86                 ast::Fn(fn_) => sema.to_def(&fn_)?.ret_type(sema.db),
87                 ast::Item(__) => return None,
88                 ast::ClosureExpr(closure) => sema.type_of_expr(&closure.body()?)?.original,
89                 ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) {
90                     sema.type_of_expr(&block_expr.into())?.original
91                 } else {
92                     continue;
93                 },
94                 _ => continue,
95             }
96         };
97     };
98
99     if inner_ty == body_ty {
100         return None;
101     }
102
103     let mut inner_ty = inner_ty;
104     let mut s = "Try Target".to_owned();
105
106     let adts = inner_ty.as_adt().zip(body_ty.as_adt());
107     if let Some((hir::Adt::Enum(inner), hir::Adt::Enum(body))) = adts {
108         let famous_defs = FamousDefs(sema, sema.scope(&try_expr.syntax()).krate());
109         // special case for two options, there is no value in showing them
110         if let Some(option_enum) = famous_defs.core_option_Option() {
111             if inner == option_enum && body == option_enum {
112                 cov_mark::hit!(hover_try_expr_opt_opt);
113                 return None;
114             }
115         }
116
117         // special case two results to show the error variants only
118         if let Some(result_enum) = famous_defs.core_result_Result() {
119             if inner == result_enum && body == result_enum {
120                 let error_type_args =
121                     inner_ty.type_arguments().nth(1).zip(body_ty.type_arguments().nth(1));
122                 if let Some((inner, body)) = error_type_args {
123                     inner_ty = inner;
124                     body_ty = body;
125                     s = "Try Error".to_owned();
126                 }
127             }
128         }
129     }
130
131     let mut res = HoverResult::default();
132
133     let mut targets: Vec<hir::ModuleDef> = Vec::new();
134     let mut push_new_def = |item: hir::ModuleDef| {
135         if !targets.contains(&item) {
136             targets.push(item);
137         }
138     };
139     walk_and_push_ty(sema.db, &inner_ty, &mut push_new_def);
140     walk_and_push_ty(sema.db, &body_ty, &mut push_new_def);
141     res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
142
143     let inner_ty = inner_ty.display(sema.db).to_string();
144     let body_ty = body_ty.display(sema.db).to_string();
145     let ty_len_max = inner_ty.len().max(body_ty.len());
146
147     let l = "Propagated as: ".len() - " Type: ".len();
148     let static_text_len_diff = l as isize - s.len() as isize;
149     let tpad = static_text_len_diff.max(0) as usize;
150     let ppad = static_text_len_diff.min(0).abs() as usize;
151
152     res.markup = format!(
153         "{bt_start}{} Type: {:>pad0$}\nPropagated as: {:>pad1$}\n{bt_end}",
154         s,
155         inner_ty,
156         body_ty,
157         pad0 = ty_len_max + tpad,
158         pad1 = ty_len_max + ppad,
159         bt_start = if config.markdown() { "```text\n" } else { "" },
160         bt_end = if config.markdown() { "```\n" } else { "" }
161     )
162     .into();
163     Some(res)
164 }
165
166 pub(super) fn deref_expr(
167     sema: &Semantics<RootDatabase>,
168     config: &HoverConfig,
169     deref_expr: &ast::PrefixExpr,
170 ) -> Option<HoverResult> {
171     let inner_ty = sema.type_of_expr(&deref_expr.expr()?)?.original;
172     let TypeInfo { original, adjusted } =
173         sema.type_of_expr(&ast::Expr::from(deref_expr.clone()))?;
174
175     let mut res = HoverResult::default();
176     let mut targets: Vec<hir::ModuleDef> = Vec::new();
177     let mut push_new_def = |item: hir::ModuleDef| {
178         if !targets.contains(&item) {
179             targets.push(item);
180         }
181     };
182     walk_and_push_ty(sema.db, &inner_ty, &mut push_new_def);
183     walk_and_push_ty(sema.db, &original, &mut push_new_def);
184
185     res.markup = if let Some(adjusted_ty) = adjusted {
186         walk_and_push_ty(sema.db, &adjusted_ty, &mut push_new_def);
187         let original = original.display(sema.db).to_string();
188         let adjusted = adjusted_ty.display(sema.db).to_string();
189         let inner = inner_ty.display(sema.db).to_string();
190         let type_len = "To type: ".len();
191         let coerced_len = "Coerced to: ".len();
192         let deref_len = "Dereferenced from: ".len();
193         let max_len = (original.len() + type_len)
194             .max(adjusted.len() + coerced_len)
195             .max(inner.len() + deref_len);
196         format!(
197             "{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}",
198             inner,
199             original,
200             adjusted,
201             ipad = max_len - deref_len,
202             apad = max_len - type_len,
203             opad = max_len - coerced_len,
204             bt_start = if config.markdown() { "```text\n" } else { "" },
205             bt_end = if config.markdown() { "```\n" } else { "" }
206         )
207         .into()
208     } else {
209         let original = original.display(sema.db).to_string();
210         let inner = inner_ty.display(sema.db).to_string();
211         let type_len = "To type: ".len();
212         let deref_len = "Dereferenced from: ".len();
213         let max_len = (original.len() + type_len).max(inner.len() + deref_len);
214         format!(
215             "{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\n{bt_end}",
216             inner,
217             original,
218             ipad = max_len - deref_len,
219             apad = max_len - type_len,
220             bt_start = if config.markdown() { "```text\n" } else { "" },
221             bt_end = if config.markdown() { "```\n" } else { "" }
222         )
223         .into()
224     };
225     res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
226
227     Some(res)
228 }
229
230 pub(super) fn keyword(
231     sema: &Semantics<RootDatabase>,
232     config: &HoverConfig,
233     token: &SyntaxToken,
234 ) -> Option<HoverResult> {
235     if !token.kind().is_keyword() || !config.documentation.is_some() {
236         return None;
237     }
238     let famous_defs = FamousDefs(sema, sema.scope(&token.parent()?).krate());
239     // std exposes {}_keyword modules with docstrings on the root to document keywords
240     let keyword_mod = format!("{}_keyword", token.text());
241     let doc_owner = find_std_module(&famous_defs, &keyword_mod)?;
242     let docs = doc_owner.attrs(sema.db).docs()?;
243     let markup = process_markup(
244         sema.db,
245         Definition::ModuleDef(doc_owner.into()),
246         &markup(Some(docs.into()), token.text().into(), None)?,
247         config,
248     );
249     Some(HoverResult { markup, actions: Default::default() })
250 }
251
252 pub(super) fn try_for_lint(attr: &ast::Attr, token: &SyntaxToken) -> Option<HoverResult> {
253     let (path, tt) = attr.as_simple_call()?;
254     if !tt.syntax().text_range().contains(token.text_range().start()) {
255         return None;
256     }
257     let (is_clippy, lints) = match &*path {
258         "feature" => (false, FEATURES),
259         "allow" | "deny" | "forbid" | "warn" => {
260             let is_clippy = algo::non_trivia_sibling(token.clone().into(), Direction::Prev)
261                 .filter(|t| t.kind() == T![:])
262                 .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev))
263                 .filter(|t| t.kind() == T![:])
264                 .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev))
265                 .map_or(false, |t| {
266                     t.kind() == T![ident] && t.into_token().map_or(false, |t| t.text() == "clippy")
267                 });
268             if is_clippy {
269                 (true, CLIPPY_LINTS)
270             } else {
271                 (false, DEFAULT_LINTS)
272             }
273         }
274         _ => return None,
275     };
276
277     let tmp;
278     let needle = if is_clippy {
279         tmp = format!("clippy::{}", token.text());
280         &tmp
281     } else {
282         &*token.text()
283     };
284
285     let lint =
286         lints.binary_search_by_key(&needle, |lint| lint.label).ok().map(|idx| &lints[idx])?;
287     Some(HoverResult {
288         markup: Markup::from(format!("```\n{}\n```\n___\n\n{}", lint.label, lint.description)),
289         ..Default::default()
290     })
291 }
292
293 pub(super) fn process_markup(
294     db: &RootDatabase,
295     def: Definition,
296     markup: &Markup,
297     config: &HoverConfig,
298 ) -> Markup {
299     let markup = markup.as_str();
300     let markup = if !config.markdown() {
301         remove_markdown(markup)
302     } else if config.links_in_hover {
303         rewrite_links(db, markup, def)
304     } else {
305         remove_links(markup)
306     };
307     Markup::from(markup)
308 }
309
310 fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> {
311     match def {
312         Definition::Field(f) => Some(f.parent_def(db).name(db)),
313         Definition::Local(l) => l.parent(db).name(db),
314         Definition::ModuleDef(md) => match md {
315             hir::ModuleDef::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             hir::ModuleDef::Variant(e) => Some(e.parent_enum(db).name(db)),
320             _ => None,
321         },
322         _ => None,
323     }
324     .map(|name| name.to_string())
325 }
326
327 pub(super) fn path(db: &RootDatabase, module: hir::Module, item_name: Option<String>) -> String {
328     let crate_name =
329         db.crate_graph()[module.krate().into()].display_name.as_ref().map(|it| it.to_string());
330     let module_path = module
331         .path_to_root(db)
332         .into_iter()
333         .rev()
334         .flat_map(|it| it.name(db).map(|name| name.to_string()));
335     crate_name.into_iter().chain(module_path).chain(item_name).join("::")
336 }
337
338 pub(super) fn definition(
339     db: &RootDatabase,
340     def: Definition,
341     famous_defs: Option<&FamousDefs>,
342     config: &HoverConfig,
343 ) -> Option<Markup> {
344     let mod_path = definition_mod_path(db, &def);
345     let (label, docs) = match def {
346         Definition::Macro(it) => (
347             match &it.source(db)?.value {
348                 Either::Left(mac) => macro_label(mac),
349                 Either::Right(mac_fn) => fn_as_proc_macro_label(mac_fn),
350             },
351             it.attrs(db).docs(),
352         ),
353         Definition::Field(def) => label_and_docs(db, def),
354         Definition::ModuleDef(it) => match it {
355             hir::ModuleDef::Module(it) => label_and_docs(db, it),
356             hir::ModuleDef::Function(it) => label_and_docs(db, it),
357             hir::ModuleDef::Adt(it) => label_and_docs(db, it),
358             hir::ModuleDef::Variant(it) => label_and_docs(db, it),
359             hir::ModuleDef::Const(it) => label_and_docs(db, it),
360             hir::ModuleDef::Static(it) => label_and_docs(db, it),
361             hir::ModuleDef::Trait(it) => label_and_docs(db, it),
362             hir::ModuleDef::TypeAlias(it) => label_and_docs(db, it),
363             hir::ModuleDef::BuiltinType(it) => {
364                 return famous_defs
365                     .and_then(|fd| builtin(fd, it))
366                     .or_else(|| Some(Markup::fenced_block(&it.name())))
367             }
368         },
369         Definition::Local(it) => return local(db, it),
370         Definition::SelfType(impl_def) => {
371             impl_def.self_ty(db).as_adt().map(|adt| label_and_docs(db, adt))?
372         }
373         Definition::GenericParam(it) => label_and_docs(db, it),
374         Definition::Label(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 label_and_docs<D>(db: &RootDatabase, def: D) -> (String, Option<hir::Documentation>)
381 where
382     D: HasAttrs + HirDisplay,
383 {
384     let label = def.display(db).to_string();
385     let docs = def.attrs(db).docs();
386     (label, docs)
387 }
388
389 fn definition_mod_path(db: &RootDatabase, def: &Definition) -> Option<String> {
390     if let Definition::GenericParam(_) = def {
391         return None;
392     }
393     def.module(db).map(|module| path(db, module, definition_owner_name(db, def)))
394 }
395
396 fn markup(docs: Option<String>, desc: String, mod_path: Option<String>) -> Option<Markup> {
397     let mut buf = String::new();
398
399     if let Some(mod_path) = mod_path {
400         if !mod_path.is_empty() {
401             format_to!(buf, "```rust\n{}\n```\n\n", mod_path);
402         }
403     }
404     format_to!(buf, "```rust\n{}\n```", desc);
405
406     if let Some(doc) = docs {
407         format_to!(buf, "\n___\n\n{}", doc);
408     }
409     Some(buf.into())
410 }
411
412 fn builtin(famous_defs: &FamousDefs, builtin: hir::BuiltinType) -> Option<Markup> {
413     // std exposes prim_{} modules with docstrings on the root to document the builtins
414     let primitive_mod = format!("prim_{}", builtin.name());
415     let doc_owner = find_std_module(famous_defs, &primitive_mod)?;
416     let docs = doc_owner.attrs(famous_defs.0.db).docs()?;
417     markup(Some(docs.into()), builtin.name().to_string(), None)
418 }
419
420 fn find_std_module(famous_defs: &FamousDefs, name: &str) -> Option<hir::Module> {
421     let db = famous_defs.0.db;
422     let std_crate = famous_defs.std()?;
423     let std_root_module = std_crate.root_module(db);
424     std_root_module
425         .children(db)
426         .find(|module| module.name(db).map_or(false, |module| module.to_string() == name))
427 }
428
429 fn local(db: &RootDatabase, it: hir::Local) -> Option<Markup> {
430     let ty = it.ty(db);
431     let ty = ty.display(db);
432     let is_mut = if it.is_mut(db) { "mut " } else { "" };
433     let desc = match it.source(db).value {
434         Either::Left(ident) => {
435             let name = it.name(db).unwrap();
436             let let_kw = if ident
437                 .syntax()
438                 .parent()
439                 .map_or(false, |p| p.kind() == LET_STMT || p.kind() == CONDITION)
440             {
441                 "let "
442             } else {
443                 ""
444             };
445             format!("{}{}{}: {}", let_kw, is_mut, name, ty)
446         }
447         Either::Right(_) => format!("{}self: {}", is_mut, ty),
448     };
449     markup(None, desc, None)
450 }