1 mod render;
2
3 #[cfg(test)]
4 mod tests;
5
6 use std::iter;
7
8 use either::Either;
9 use hir::{db::DefDatabase, HasSource, LangItem, Semantics};
10 use ide_db::{
11 base_db::FileRange,
12 defs::{Definition, IdentClass, OperatorClass},
13 famous_defs::FamousDefs,
14 helpers::pick_best_token,
15 FxIndexSet, RootDatabase,
16 };
17 use itertools::Itertools;
18 use syntax::{ast, AstNode, SyntaxKind::*, SyntaxNode, T};
19
20 use crate::{
21 doc_links::token_as_doc_comment,
22 markdown_remove::remove_markdown,
23 markup::Markup,
24 runnables::{runnable_fn, runnable_mod},
25 FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav,
26 };
27 #[derive(Clone, Debug, PartialEq, Eq)]
28 pub struct HoverConfig {
29 pub links_in_hover: bool,
30 pub memory_layout: Option<MemoryLayoutHoverConfig>,
31 pub documentation: bool,
32 pub keywords: bool,
33 pub format: HoverDocFormat,
34 }
35
36 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
37 pub struct MemoryLayoutHoverConfig {
38 pub size: Option<MemoryLayoutHoverRenderKind>,
39 pub offset: Option<MemoryLayoutHoverRenderKind>,
40 pub alignment: Option<MemoryLayoutHoverRenderKind>,
41 pub niches: bool,
42 }
43
44 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
45 pub enum MemoryLayoutHoverRenderKind {
46 Decimal,
47 Hexadecimal,
48 Both,
49 }
50
51 #[derive(Clone, Debug, PartialEq, Eq)]
52 pub enum HoverDocFormat {
53 Markdown,
54 PlainText,
55 }
56
57 #[derive(Debug, Clone)]
58 pub enum HoverAction {
59 Runnable(Runnable),
60 Implementation(FilePosition),
61 Reference(FilePosition),
62 GoToType(Vec<HoverGotoTypeData>),
63 }
64
65 impl HoverAction {
goto_type_from_targets(db: &RootDatabase, targets: Vec<hir::ModuleDef>) -> Self66 fn goto_type_from_targets(db: &RootDatabase, targets: Vec<hir::ModuleDef>) -> Self {
67 let targets = targets
68 .into_iter()
69 .filter_map(|it| {
70 Some(HoverGotoTypeData {
71 mod_path: render::path(
72 db,
73 it.module(db)?,
74 it.name(db).map(|name| name.display(db).to_string()),
75 ),
76 nav: it.try_to_nav(db)?,
77 })
78 })
79 .collect();
80 HoverAction::GoToType(targets)
81 }
82 }
83
84 #[derive(Debug, Clone, Eq, PartialEq, Hash)]
85 pub struct HoverGotoTypeData {
86 pub mod_path: String,
87 pub nav: NavigationTarget,
88 }
89
90 /// Contains the results when hovering over an item
91 #[derive(Debug, Default)]
92 pub struct HoverResult {
93 pub markup: Markup,
94 pub actions: Vec<HoverAction>,
95 }
96
97 // Feature: Hover
98 //
99 // Shows additional information, like the type of an expression or the documentation for a definition when "focusing" code.
100 // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut.
101 //
102 // image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[]
103 pub(crate) fn hover(
104 db: &RootDatabase,
105 frange @ FileRange { file_id, range }: FileRange,
106 config: &HoverConfig,
107 ) -> Option<RangeInfo<HoverResult>> {
108 let sema = &hir::Semantics::new(db);
109 let file = sema.parse(file_id).syntax().clone();
110 let mut res = if range.is_empty() {
111 hover_simple(sema, FilePosition { file_id, offset: range.start() }, file, config)
112 } else {
113 hover_ranged(sema, frange, file, config)
114 }?;
115
116 if let HoverDocFormat::PlainText = config.format {
117 res.info.markup = remove_markdown(res.info.markup.as_str()).into();
118 }
119 Some(res)
120 }
121
122 fn hover_simple(
123 sema: &Semantics<'_, RootDatabase>,
124 FilePosition { file_id, offset }: FilePosition,
125 file: SyntaxNode,
126 config: &HoverConfig,
127 ) -> Option<RangeInfo<HoverResult>> {
128 let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind {
129 IDENT
130 | INT_NUMBER
131 | LIFETIME_IDENT
132 | T![self]
133 | T![super]
134 | T![crate]
135 | T![Self]
136 | T![_] => 4,
137 // index and prefix ops and closure pipe
138 T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] | T![|] => 3,
139 kind if kind.is_keyword() => 2,
140 T!['('] | T![')'] => 2,
141 kind if kind.is_trivia() => 0,
142 _ => 1,
143 })?;
144
145 if let Some(doc_comment) = token_as_doc_comment(&original_token) {
146 cov_mark::hit!(no_highlight_on_comment_hover);
147 return doc_comment.get_definition_with_descend_at(sema, offset, |def, node, range| {
148 let res = hover_for_definition(sema, file_id, def, &node, config)?;
149 Some(RangeInfo::new(range, res))
150 });
151 }
152
153 let in_attr = original_token
154 .parent_ancestors()
155 .filter_map(ast::Item::cast)
156 .any(|item| sema.is_attr_macro_call(&item))
157 && !matches!(
158 original_token.parent().and_then(ast::TokenTree::cast),
159 Some(tt) if tt.syntax().ancestors().any(|it| ast::Meta::can_cast(it.kind()))
160 );
161
162 // prefer descending the same token kind in attribute expansions, in normal macros text
163 // equivalency is more important
164 let descended = if in_attr {
165 [sema.descend_into_macros_with_kind_preference(original_token.clone())].into()
166 } else {
167 sema.descend_into_macros_with_same_text(original_token.clone())
168 };
169 let descended = || descended.iter();
170
171 let result = descended()
172 // try lint hover
173 .find_map(|token| {
174 // FIXME: Definition should include known lints and the like instead of having this special case here
175 let attr = token.parent_ancestors().find_map(ast::Attr::cast)?;
176 render::try_for_lint(&attr, token)
177 })
178 // try definitions
179 .or_else(|| {
180 descended()
181 .filter_map(|token| {
182 let node = token.parent()?;
183 let class = IdentClass::classify_token(sema, token)?;
184 if let IdentClass::Operator(OperatorClass::Await(_)) = class {
185 // It's better for us to fall back to the keyword hover here,
186 // rendering poll is very confusing
187 return None;
188 }
189 Some(class.definitions().into_iter().zip(iter::once(node).cycle()))
190 })
191 .flatten()
192 .unique_by(|&(def, _)| def)
193 .filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config))
194 .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| {
195 acc.actions.extend(actions);
196 acc.markup = Markup::from(format!("{}\n---\n{markup}", acc.markup));
197 acc
198 })
199 })
200 // try keywords
201 .or_else(|| descended().find_map(|token| render::keyword(sema, config, token)))
202 // try _ hovers
203 .or_else(|| descended().find_map(|token| render::underscore(sema, config, token)))
204 // try rest pattern hover
205 .or_else(|| {
206 descended().find_map(|token| {
207 if token.kind() != DOT2 {
208 return None;
209 }
210
211 let rest_pat = token.parent().and_then(ast::RestPat::cast)?;
212 let record_pat_field_list =
213 rest_pat.syntax().parent().and_then(ast::RecordPatFieldList::cast)?;
214
215 let record_pat =
216 record_pat_field_list.syntax().parent().and_then(ast::RecordPat::cast)?;
217
218 Some(render::struct_rest_pat(sema, config, &record_pat))
219 })
220 })
221 // try () call hovers
222 .or_else(|| {
223 descended().find_map(|token| {
224 if token.kind() != T!['('] && token.kind() != T![')'] {
225 return None;
226 }
227 let arg_list = token.parent().and_then(ast::ArgList::cast)?.syntax().parent()?;
228 let call_expr = syntax::match_ast! {
229 match arg_list {
230 ast::CallExpr(expr) => expr.into(),
231 ast::MethodCallExpr(expr) => expr.into(),
232 _ => return None,
233 }
234 };
235 render::type_info_of(sema, config, &Either::Left(call_expr))
236 })
237 })
238 // try closure
239 .or_else(|| {
240 descended().find_map(|token| {
241 if token.kind() != T![|] {
242 return None;
243 }
244 let c = token.parent().and_then(|x| x.parent()).and_then(ast::ClosureExpr::cast)?;
245 render::closure_expr(sema, config, c)
246 })
247 });
248
249 result.map(|mut res: HoverResult| {
250 res.actions = dedupe_or_merge_hover_actions(res.actions);
251 RangeInfo::new(original_token.text_range(), res)
252 })
253 }
254
255 fn hover_ranged(
256 sema: &Semantics<'_, RootDatabase>,
257 FileRange { range, .. }: FileRange,
258 file: SyntaxNode,
259 config: &HoverConfig,
260 ) -> Option<RangeInfo<HoverResult>> {
261 // FIXME: make this work in attributes
262 let expr_or_pat = file
263 .covering_element(range)
264 .ancestors()
265 .take_while(|it| ast::MacroCall::can_cast(it.kind()) || !ast::Item::can_cast(it.kind()))
266 .find_map(Either::<ast::Expr, ast::Pat>::cast)?;
267 let res = match &expr_or_pat {
268 Either::Left(ast::Expr::TryExpr(try_expr)) => render::try_expr(sema, config, try_expr),
269 Either::Left(ast::Expr::PrefixExpr(prefix_expr))
270 if prefix_expr.op_kind() == Some(ast::UnaryOp::Deref) =>
271 {
272 render::deref_expr(sema, config, prefix_expr)
273 }
274 _ => None,
275 };
276 let res = res.or_else(|| render::type_info_of(sema, config, &expr_or_pat));
277 res.map(|it| {
278 let range = match expr_or_pat {
279 Either::Left(it) => it.syntax().text_range(),
280 Either::Right(it) => it.syntax().text_range(),
281 };
282 RangeInfo::new(range, it)
283 })
284 }
285
hover_for_definition( sema: &Semantics<'_, RootDatabase>, file_id: FileId, definition: Definition, node: &SyntaxNode, config: &HoverConfig, ) -> Option<HoverResult>286 pub(crate) fn hover_for_definition(
287 sema: &Semantics<'_, RootDatabase>,
288 file_id: FileId,
289 definition: Definition,
290 node: &SyntaxNode,
291 config: &HoverConfig,
292 ) -> Option<HoverResult> {
293 let famous_defs = match &definition {
294 Definition::BuiltinType(_) => Some(FamousDefs(sema, sema.scope(node)?.krate())),
295 _ => None,
296 };
297 render::definition(sema.db, definition, famous_defs.as_ref(), config).map(|markup| {
298 HoverResult {
299 markup: render::process_markup(sema.db, definition, &markup, config),
300 actions: [
301 show_implementations_action(sema.db, definition),
302 show_fn_references_action(sema.db, definition),
303 runnable_action(sema, definition, file_id),
304 goto_type_action_for_def(sema.db, definition),
305 ]
306 .into_iter()
307 .flatten()
308 .collect(),
309 }
310 })
311 }
312
show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction>313 fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
314 fn to_action(nav_target: NavigationTarget) -> HoverAction {
315 HoverAction::Implementation(FilePosition {
316 file_id: nav_target.file_id,
317 offset: nav_target.focus_or_full_range().start(),
318 })
319 }
320
321 let adt = match def {
322 Definition::Trait(it) => return it.try_to_nav(db).map(to_action),
323 Definition::Adt(it) => Some(it),
324 Definition::SelfType(it) => it.self_ty(db).as_adt(),
325 _ => None,
326 }?;
327 adt.try_to_nav(db).map(to_action)
328 }
329
show_fn_references_action(db: &RootDatabase, def: Definition) -> Option<HoverAction>330 fn show_fn_references_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
331 match def {
332 Definition::Function(it) => it.try_to_nav(db).map(|nav_target| {
333 HoverAction::Reference(FilePosition {
334 file_id: nav_target.file_id,
335 offset: nav_target.focus_or_full_range().start(),
336 })
337 }),
338 _ => None,
339 }
340 }
341
runnable_action( sema: &hir::Semantics<'_, RootDatabase>, def: Definition, file_id: FileId, ) -> Option<HoverAction>342 fn runnable_action(
343 sema: &hir::Semantics<'_, RootDatabase>,
344 def: Definition,
345 file_id: FileId,
346 ) -> Option<HoverAction> {
347 match def {
348 Definition::Module(it) => runnable_mod(sema, it).map(HoverAction::Runnable),
349 Definition::Function(func) => {
350 let src = func.source(sema.db)?;
351 if src.file_id != file_id.into() {
352 cov_mark::hit!(hover_macro_generated_struct_fn_doc_comment);
353 cov_mark::hit!(hover_macro_generated_struct_fn_doc_attr);
354 return None;
355 }
356
357 runnable_fn(sema, func).map(HoverAction::Runnable)
358 }
359 _ => None,
360 }
361 }
362
goto_type_action_for_def(db: &RootDatabase, def: Definition) -> Option<HoverAction>363 fn goto_type_action_for_def(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
364 let mut targets: Vec<hir::ModuleDef> = Vec::new();
365 let mut push_new_def = |item: hir::ModuleDef| {
366 if !targets.contains(&item) {
367 targets.push(item);
368 }
369 };
370
371 if let Definition::GenericParam(hir::GenericParam::TypeParam(it)) = def {
372 let krate = it.module(db).krate();
373 let sized_trait =
374 db.lang_item(krate.into(), LangItem::Sized).and_then(|lang_item| lang_item.as_trait());
375
376 it.trait_bounds(db)
377 .into_iter()
378 .filter(|&it| Some(it.into()) != sized_trait)
379 .for_each(|it| push_new_def(it.into()));
380 } else {
381 let ty = match def {
382 Definition::Local(it) => it.ty(db),
383 Definition::GenericParam(hir::GenericParam::ConstParam(it)) => it.ty(db),
384 Definition::Field(field) => field.ty(db),
385 Definition::Function(function) => function.ret_type(db),
386 _ => return None,
387 };
388
389 walk_and_push_ty(db, &ty, &mut push_new_def);
390 }
391
392 Some(HoverAction::goto_type_from_targets(db, targets))
393 }
394
walk_and_push_ty( db: &RootDatabase, ty: &hir::Type, push_new_def: &mut dyn FnMut(hir::ModuleDef), )395 fn walk_and_push_ty(
396 db: &RootDatabase,
397 ty: &hir::Type,
398 push_new_def: &mut dyn FnMut(hir::ModuleDef),
399 ) {
400 ty.walk(db, |t| {
401 if let Some(adt) = t.as_adt() {
402 push_new_def(adt.into());
403 } else if let Some(trait_) = t.as_dyn_trait() {
404 push_new_def(trait_.into());
405 } else if let Some(traits) = t.as_impl_traits(db) {
406 traits.for_each(|it| push_new_def(it.into()));
407 } else if let Some(trait_) = t.as_associated_type_parent_trait(db) {
408 push_new_def(trait_.into());
409 }
410 });
411 }
412
dedupe_or_merge_hover_actions(actions: Vec<HoverAction>) -> Vec<HoverAction>413 fn dedupe_or_merge_hover_actions(actions: Vec<HoverAction>) -> Vec<HoverAction> {
414 let mut deduped_actions = Vec::with_capacity(actions.len());
415 let mut go_to_type_targets = FxIndexSet::default();
416
417 let mut seen_implementation = false;
418 let mut seen_reference = false;
419 let mut seen_runnable = false;
420 for action in actions {
421 match action {
422 HoverAction::GoToType(targets) => {
423 go_to_type_targets.extend(targets);
424 }
425 HoverAction::Implementation(..) => {
426 if !seen_implementation {
427 seen_implementation = true;
428 deduped_actions.push(action);
429 }
430 }
431 HoverAction::Reference(..) => {
432 if !seen_reference {
433 seen_reference = true;
434 deduped_actions.push(action);
435 }
436 }
437 HoverAction::Runnable(..) => {
438 if !seen_runnable {
439 seen_runnable = true;
440 deduped_actions.push(action);
441 }
442 }
443 };
444 }
445
446 if !go_to_type_targets.is_empty() {
447 deduped_actions.push(HoverAction::GoToType(go_to_type_targets.into_iter().collect()));
448 }
449
450 deduped_actions
451 }
452