ide/
hover.rs

1mod render;
2
3#[cfg(test)]
4mod tests;
5
6use std::{iter, ops::Not};
7
8use either::Either;
9use hir::{
10    DisplayTarget, GenericDef, GenericSubstitution, HasCrate, HasSource, LangItem, Semantics,
11    db::DefDatabase,
12};
13use ide_db::{
14    FileRange, FxIndexSet, Ranker, RootDatabase,
15    defs::{Definition, IdentClass, NameRefClass, OperatorClass},
16    famous_defs::FamousDefs,
17    helpers::pick_best_token,
18};
19use itertools::{Itertools, multizip};
20use span::Edition;
21use syntax::{
22    AstNode,
23    SyntaxKind::{self, *},
24    SyntaxNode, T, ast,
25};
26
27use crate::{
28    FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav,
29    doc_links::token_as_doc_comment,
30    markdown_remove::remove_markdown,
31    markup::Markup,
32    navigation_target::UpmappingResult,
33    runnables::{runnable_fn, runnable_mod},
34};
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct HoverConfig {
37    pub links_in_hover: bool,
38    pub memory_layout: Option<MemoryLayoutHoverConfig>,
39    pub documentation: bool,
40    pub keywords: bool,
41    pub format: HoverDocFormat,
42    pub max_trait_assoc_items_count: Option<usize>,
43    pub max_fields_count: Option<usize>,
44    pub max_enum_variants_count: Option<usize>,
45    pub max_subst_ty_len: SubstTyLen,
46    pub show_drop_glue: bool,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum SubstTyLen {
51    Unlimited,
52    LimitTo(usize),
53    Hide,
54}
55
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57pub struct MemoryLayoutHoverConfig {
58    pub size: Option<MemoryLayoutHoverRenderKind>,
59    pub offset: Option<MemoryLayoutHoverRenderKind>,
60    pub alignment: Option<MemoryLayoutHoverRenderKind>,
61    pub padding: Option<MemoryLayoutHoverRenderKind>,
62    pub niches: bool,
63}
64
65#[derive(Copy, Clone, Debug, PartialEq, Eq)]
66pub enum MemoryLayoutHoverRenderKind {
67    Decimal,
68    Hexadecimal,
69    Both,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum HoverDocFormat {
74    Markdown,
75    PlainText,
76}
77
78#[derive(Debug, Clone, Hash, PartialEq, Eq)]
79pub enum HoverAction {
80    Runnable(Runnable),
81    Implementation(FilePosition),
82    Reference(FilePosition),
83    GoToType(Vec<HoverGotoTypeData>),
84}
85
86impl HoverAction {
87    fn goto_type_from_targets(
88        db: &RootDatabase,
89        targets: Vec<hir::ModuleDef>,
90        edition: Edition,
91    ) -> Option<Self> {
92        let targets = targets
93            .into_iter()
94            .filter_map(|it| {
95                Some(HoverGotoTypeData {
96                    mod_path: render::path(
97                        db,
98                        it.module(db)?,
99                        it.name(db).map(|name| name.display(db, edition).to_string()),
100                        edition,
101                    ),
102                    nav: it.try_to_nav(db)?.call_site(),
103                })
104            })
105            .collect::<Vec<_>>();
106        targets.is_empty().not().then_some(HoverAction::GoToType(targets))
107    }
108}
109
110#[derive(Debug, Clone, Eq, PartialEq, Hash)]
111pub struct HoverGotoTypeData {
112    pub mod_path: String,
113    pub nav: NavigationTarget,
114}
115
116/// Contains the results when hovering over an item
117#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
118pub struct HoverResult {
119    pub markup: Markup,
120    pub actions: Vec<HoverAction>,
121}
122
123// Feature: Hover
124//
125// Shows additional information, like the type of an expression or the documentation for a definition when "focusing" code.
126// Focusing is usually hovering with a mouse, but can also be triggered with a shortcut.
127//
128// ![Hover](https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif)
129pub(crate) fn hover(
130    db: &RootDatabase,
131    frange @ FileRange { file_id, range }: FileRange,
132    config: &HoverConfig,
133) -> Option<RangeInfo<HoverResult>> {
134    let sema = &hir::Semantics::new(db);
135    let file = sema.parse_guess_edition(file_id).syntax().clone();
136    let edition =
137        sema.attach_first_edition(file_id).map(|it| it.edition(db)).unwrap_or(Edition::CURRENT);
138    let display_target = sema.first_crate(file_id)?.to_display_target(db);
139    let mut res = if range.is_empty() {
140        hover_offset(
141            sema,
142            FilePosition { file_id, offset: range.start() },
143            file,
144            config,
145            edition,
146            display_target,
147        )
148    } else {
149        hover_ranged(sema, frange, file, config, edition, display_target)
150    }?;
151
152    if let HoverDocFormat::PlainText = config.format {
153        res.info.markup = remove_markdown(res.info.markup.as_str()).into();
154    }
155    Some(res)
156}
157
158#[allow(clippy::field_reassign_with_default)]
159fn hover_offset(
160    sema: &Semantics<'_, RootDatabase>,
161    FilePosition { file_id, offset }: FilePosition,
162    file: SyntaxNode,
163    config: &HoverConfig,
164    edition: Edition,
165    display_target: DisplayTarget,
166) -> Option<RangeInfo<HoverResult>> {
167    let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind {
168        IDENT
169        | INT_NUMBER
170        | LIFETIME_IDENT
171        | T![self]
172        | T![super]
173        | T![crate]
174        | T![Self]
175        | T![_] => 4,
176        // index and prefix ops and closure pipe
177        T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] | T![|] => 3,
178        kind if kind.is_keyword(edition) => 2,
179        T!['('] | T![')'] => 2,
180        kind if kind.is_trivia() => 0,
181        _ => 1,
182    })?;
183
184    if let Some(doc_comment) = token_as_doc_comment(&original_token) {
185        cov_mark::hit!(no_highlight_on_comment_hover);
186        return doc_comment.get_definition_with_descend_at(sema, offset, |def, node, range| {
187            let res = hover_for_definition(
188                sema,
189                file_id,
190                def,
191                None,
192                &node,
193                None,
194                false,
195                config,
196                edition,
197                display_target,
198            );
199            Some(RangeInfo::new(range, res))
200        });
201    }
202
203    if let Some((range, _, _, resolution)) =
204        sema.check_for_format_args_template(original_token.clone(), offset)
205    {
206        let res = hover_for_definition(
207            sema,
208            file_id,
209            Definition::from(resolution?),
210            None,
211            &original_token.parent()?,
212            None,
213            false,
214            config,
215            edition,
216            display_target,
217        );
218        return Some(RangeInfo::new(range, res));
219    }
220
221    // prefer descending the same token kind in attribute expansions, in normal macros text
222    // equivalency is more important
223    let mut descended = sema.descend_into_macros(original_token.clone());
224
225    let ranker = Ranker::from_token(&original_token);
226
227    descended.sort_by_cached_key(|tok| !ranker.rank_token(tok));
228
229    let mut res = vec![];
230    for token in descended {
231        let is_same_kind = token.kind() == ranker.kind;
232        let lint_hover = (|| {
233            // FIXME: Definition should include known lints and the like instead of having this special case here
234            let attr = token.parent_ancestors().find_map(ast::Attr::cast)?;
235            render::try_for_lint(&attr, &token)
236        })();
237        if let Some(lint_hover) = lint_hover {
238            res.push(lint_hover);
239            continue;
240        }
241        let definitions = (|| {
242            Some(
243                'a: {
244                    let node = token.parent()?;
245
246                    // special case macro calls, we wanna render the invoked arm index
247                    if let Some(name) = ast::NameRef::cast(node.clone())
248                        && let Some(path_seg) =
249                            name.syntax().parent().and_then(ast::PathSegment::cast)
250                            && let Some(macro_call) = path_seg
251                                .parent_path()
252                                .syntax()
253                                .parent()
254                                .and_then(ast::MacroCall::cast)
255                                && let Some(macro_) = sema.resolve_macro_call(&macro_call) {
256                                    break 'a vec![(
257                                        (Definition::Macro(macro_), None),
258                                        sema.resolve_macro_call_arm(&macro_call),
259                                        false,
260                                        node,
261                                    )];
262                                }
263
264                    match IdentClass::classify_node(sema, &node)? {
265                        // It's better for us to fall back to the keyword hover here,
266                        // rendering poll is very confusing
267                        IdentClass::Operator(OperatorClass::Await(_)) => return None,
268
269                        IdentClass::NameRefClass(NameRefClass::ExternCrateShorthand {
270                            decl,
271                            ..
272                        }) => {
273                            vec![((Definition::ExternCrateDecl(decl), None), None, false, node)]
274                        }
275
276                        class => {
277                            let render_extras = matches!(class, IdentClass::NameClass(_))
278                                // Render extra information for `Self` keyword as well
279                                || ast::NameRef::cast(node.clone()).is_some_and(|name_ref| name_ref.token_kind() == SyntaxKind::SELF_TYPE_KW);
280                            multizip((
281                                class.definitions(),
282                                iter::repeat(None),
283                                iter::repeat(render_extras),
284                                iter::repeat(node),
285                            ))
286                            .collect::<Vec<_>>()
287                        }
288                    }
289                }
290                .into_iter()
291                .unique_by(|&((def, _), _, _, _)| def)
292                .map(|((def, subst), macro_arm, hovered_definition, node)| {
293                    hover_for_definition(
294                        sema,
295                        file_id,
296                        def,
297                        subst,
298                        &node,
299                        macro_arm,
300                        hovered_definition,
301                        config,
302                        edition,
303                        display_target,
304                    )
305                })
306                .collect::<Vec<_>>(),
307            )
308        })();
309        if let Some(definitions) = definitions {
310            res.extend(definitions);
311            continue;
312        }
313        let keywords = || render::keyword(sema, config, &token, edition, display_target);
314        let underscore = || {
315            if !is_same_kind {
316                return None;
317            }
318            render::underscore(sema, config, &token, edition, display_target)
319        };
320        let rest_pat = || {
321            if !is_same_kind || token.kind() != DOT2 {
322                return None;
323            }
324
325            let rest_pat = token.parent().and_then(ast::RestPat::cast)?;
326            let record_pat_field_list =
327                rest_pat.syntax().parent().and_then(ast::RecordPatFieldList::cast)?;
328
329            let record_pat =
330                record_pat_field_list.syntax().parent().and_then(ast::RecordPat::cast)?;
331
332            Some(render::struct_rest_pat(sema, config, &record_pat, edition, display_target))
333        };
334        let call = || {
335            if !is_same_kind || token.kind() != T!['('] && token.kind() != T![')'] {
336                return None;
337            }
338            let arg_list = token.parent().and_then(ast::ArgList::cast)?.syntax().parent()?;
339            let call_expr = syntax::match_ast! {
340                match arg_list {
341                    ast::CallExpr(expr) => expr.into(),
342                    ast::MethodCallExpr(expr) => expr.into(),
343                    _ => return None,
344                }
345            };
346            render::type_info_of(sema, config, &Either::Left(call_expr), edition, display_target)
347        };
348        let closure = || {
349            if !is_same_kind || token.kind() != T![|] {
350                return None;
351            }
352            let c = token.parent().and_then(|x| x.parent()).and_then(ast::ClosureExpr::cast)?;
353            render::closure_expr(sema, config, c, edition, display_target)
354        };
355        let literal = || {
356            render::literal(sema, original_token.clone(), display_target)
357                .map(|markup| HoverResult { markup, actions: vec![] })
358        };
359        if let Some(result) = keywords()
360            .or_else(underscore)
361            .or_else(rest_pat)
362            .or_else(call)
363            .or_else(closure)
364            .or_else(literal)
365        {
366            res.push(result)
367        }
368    }
369
370    res.into_iter()
371        .unique()
372        .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| {
373            acc.actions.extend(actions);
374            acc.markup = Markup::from(format!("{}\n\n---\n{markup}", acc.markup));
375            acc
376        })
377        .map(|mut res: HoverResult| {
378            res.actions = dedupe_or_merge_hover_actions(res.actions);
379            RangeInfo::new(original_token.text_range(), res)
380        })
381}
382
383fn hover_ranged(
384    sema: &Semantics<'_, RootDatabase>,
385    FileRange { range, .. }: FileRange,
386    file: SyntaxNode,
387    config: &HoverConfig,
388    edition: Edition,
389    display_target: DisplayTarget,
390) -> Option<RangeInfo<HoverResult>> {
391    // FIXME: make this work in attributes
392    let expr_or_pat = file
393        .covering_element(range)
394        .ancestors()
395        .take_while(|it| ast::MacroCall::can_cast(it.kind()) || !ast::Item::can_cast(it.kind()))
396        .find_map(Either::<ast::Expr, ast::Pat>::cast)?;
397    let res = match &expr_or_pat {
398        Either::Left(ast::Expr::TryExpr(try_expr)) => {
399            render::try_expr(sema, config, try_expr, edition, display_target)
400        }
401        Either::Left(ast::Expr::PrefixExpr(prefix_expr))
402            if prefix_expr.op_kind() == Some(ast::UnaryOp::Deref) =>
403        {
404            render::deref_expr(sema, config, prefix_expr, edition, display_target)
405        }
406        _ => None,
407    };
408    let res =
409        res.or_else(|| render::type_info_of(sema, config, &expr_or_pat, edition, display_target));
410    res.map(|it| {
411        let range = match expr_or_pat {
412            Either::Left(it) => it.syntax().text_range(),
413            Either::Right(it) => it.syntax().text_range(),
414        };
415        RangeInfo::new(range, it)
416    })
417}
418
419// FIXME: Why is this pub(crate)?
420pub(crate) fn hover_for_definition(
421    sema: &Semantics<'_, RootDatabase>,
422    file_id: FileId,
423    def: Definition,
424    subst: Option<GenericSubstitution<'_>>,
425    scope_node: &SyntaxNode,
426    macro_arm: Option<u32>,
427    render_extras: bool,
428    config: &HoverConfig,
429    edition: Edition,
430    display_target: DisplayTarget,
431) -> HoverResult {
432    let famous_defs = match &def {
433        Definition::BuiltinType(_) => sema.scope(scope_node).map(|it| FamousDefs(sema, it.krate())),
434        _ => None,
435    };
436
437    let db = sema.db;
438    let def_ty = match def {
439        Definition::Local(it) => Some(it.ty(db)),
440        Definition::GenericParam(hir::GenericParam::ConstParam(it)) => Some(it.ty(db)),
441        Definition::GenericParam(hir::GenericParam::TypeParam(it)) => Some(it.ty(db)),
442        Definition::Field(field) => Some(field.ty(db)),
443        Definition::TupleField(it) => Some(it.ty(db)),
444        Definition::Function(it) => Some(it.ty(db)),
445        Definition::Adt(it) => Some(it.ty(db)),
446        Definition::Const(it) => Some(it.ty(db)),
447        Definition::Static(it) => Some(it.ty(db)),
448        Definition::TypeAlias(it) => Some(it.ty(db)),
449        Definition::BuiltinType(it) => Some(it.ty(db)),
450        _ => None,
451    };
452    let notable_traits = def_ty.map(|ty| notable_traits(db, &ty)).unwrap_or_default();
453    let subst_types = subst.map(|subst| subst.types(db));
454
455    let (markup, range_map) = render::definition(
456        sema.db,
457        def,
458        famous_defs.as_ref(),
459        &notable_traits,
460        macro_arm,
461        render_extras,
462        subst_types.as_ref(),
463        config,
464        edition,
465        display_target,
466    );
467    HoverResult {
468        markup: render::process_markup(sema.db, def, &markup, range_map, config),
469        actions: [
470            show_fn_references_action(sema.db, def),
471            show_implementations_action(sema.db, def),
472            runnable_action(sema, def, file_id),
473            goto_type_action_for_def(sema.db, def, &notable_traits, subst_types, edition),
474        ]
475        .into_iter()
476        .flatten()
477        .collect(),
478    }
479}
480
481fn notable_traits<'db>(
482    db: &'db RootDatabase,
483    ty: &hir::Type<'db>,
484) -> Vec<(hir::Trait, Vec<(Option<hir::Type<'db>>, hir::Name)>)> {
485    db.notable_traits_in_deps(ty.krate(db).into())
486        .iter()
487        .flat_map(|it| &**it)
488        .filter_map(move |&trait_| {
489            let trait_ = trait_.into();
490            ty.impls_trait(db, trait_, &[]).then(|| {
491                (
492                    trait_,
493                    trait_
494                        .items(db)
495                        .into_iter()
496                        .filter_map(hir::AssocItem::as_type_alias)
497                        .map(|alias| {
498                            (ty.normalize_trait_assoc_type(db, &[], alias), alias.name(db))
499                        })
500                        .collect::<Vec<_>>(),
501                )
502            })
503        })
504        .sorted_by_cached_key(|(trait_, _)| trait_.name(db))
505        .collect::<Vec<_>>()
506}
507
508fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
509    fn to_action(nav_target: NavigationTarget) -> HoverAction {
510        HoverAction::Implementation(FilePosition {
511            file_id: nav_target.file_id,
512            offset: nav_target.focus_or_full_range().start(),
513        })
514    }
515
516    let adt = match def {
517        Definition::Trait(it) => {
518            return it.try_to_nav(db).map(UpmappingResult::call_site).map(to_action);
519        }
520        Definition::Adt(it) => Some(it),
521        Definition::SelfType(it) => it.self_ty(db).as_adt(),
522        _ => None,
523    }?;
524    adt.try_to_nav(db).map(UpmappingResult::call_site).map(to_action)
525}
526
527fn show_fn_references_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
528    match def {
529        Definition::Function(it) => {
530            it.try_to_nav(db).map(UpmappingResult::call_site).map(|nav_target| {
531                HoverAction::Reference(FilePosition {
532                    file_id: nav_target.file_id,
533                    offset: nav_target.focus_or_full_range().start(),
534                })
535            })
536        }
537        _ => None,
538    }
539}
540
541fn runnable_action(
542    sema: &hir::Semantics<'_, RootDatabase>,
543    def: Definition,
544    file_id: FileId,
545) -> Option<HoverAction> {
546    match def {
547        Definition::Module(it) => runnable_mod(sema, it).map(HoverAction::Runnable),
548        Definition::Function(func) => {
549            let src = func.source(sema.db)?;
550            if src.file_id.file_id().is_none_or(|f| f.file_id(sema.db) != file_id) {
551                cov_mark::hit!(hover_macro_generated_struct_fn_doc_comment);
552                cov_mark::hit!(hover_macro_generated_struct_fn_doc_attr);
553                return None;
554            }
555
556            runnable_fn(sema, func).map(HoverAction::Runnable)
557        }
558        _ => None,
559    }
560}
561
562fn goto_type_action_for_def(
563    db: &RootDatabase,
564    def: Definition,
565    notable_traits: &[(hir::Trait, Vec<(Option<hir::Type<'_>>, hir::Name)>)],
566    subst_types: Option<Vec<(hir::Symbol, hir::Type<'_>)>>,
567    edition: Edition,
568) -> Option<HoverAction> {
569    let mut targets: Vec<hir::ModuleDef> = Vec::new();
570    let mut push_new_def = |item: hir::ModuleDef| {
571        if !targets.contains(&item) {
572            targets.push(item);
573        }
574    };
575
576    for &(trait_, ref assocs) in notable_traits {
577        push_new_def(trait_.into());
578        assocs.iter().filter_map(|(ty, _)| ty.as_ref()).for_each(|ty| {
579            walk_and_push_ty(db, ty, &mut push_new_def);
580        });
581    }
582
583    if let Ok(generic_def) = GenericDef::try_from(def) {
584        generic_def.type_or_const_params(db).into_iter().for_each(|it| {
585            walk_and_push_ty(db, &it.ty(db), &mut push_new_def);
586        });
587    }
588
589    let ty = match def {
590        Definition::Local(it) => Some(it.ty(db)),
591        Definition::Field(field) => Some(field.ty(db)),
592        Definition::TupleField(field) => Some(field.ty(db)),
593        Definition::Const(it) => Some(it.ty(db)),
594        Definition::Static(it) => Some(it.ty(db)),
595        Definition::Function(func) => {
596            for param in func.assoc_fn_params(db) {
597                walk_and_push_ty(db, param.ty(), &mut push_new_def);
598            }
599            Some(func.ret_type(db))
600        }
601        Definition::GenericParam(hir::GenericParam::ConstParam(it)) => Some(it.ty(db)),
602        Definition::GenericParam(hir::GenericParam::TypeParam(it)) => Some(it.ty(db)),
603        _ => None,
604    };
605    if let Some(ty) = ty {
606        walk_and_push_ty(db, &ty, &mut push_new_def);
607    }
608
609    if let Some(subst_types) = subst_types {
610        for (_, ty) in subst_types {
611            walk_and_push_ty(db, &ty, &mut push_new_def);
612        }
613    }
614
615    HoverAction::goto_type_from_targets(db, targets, edition)
616}
617
618fn walk_and_push_ty(
619    db: &RootDatabase,
620    ty: &hir::Type<'_>,
621    push_new_def: &mut dyn FnMut(hir::ModuleDef),
622) {
623    ty.walk(db, |t| {
624        if let Some(adt) = t.as_adt() {
625            push_new_def(adt.into());
626        } else if let Some(trait_) = t.as_dyn_trait() {
627            push_new_def(trait_.into());
628        } else if let Some(traits) = t.as_impl_traits(db) {
629            traits.for_each(|it| push_new_def(it.into()));
630        } else if let Some(trait_) = t.as_associated_type_parent_trait(db) {
631            push_new_def(trait_.into());
632        } else if let Some(tp) = t.as_type_param(db) {
633            let sized_trait = LangItem::Sized.resolve_trait(db, t.krate(db).into());
634            tp.trait_bounds(db)
635                .into_iter()
636                .filter(|&it| Some(it.into()) != sized_trait)
637                .for_each(|it| push_new_def(it.into()));
638        }
639    });
640}
641
642fn dedupe_or_merge_hover_actions(actions: Vec<HoverAction>) -> Vec<HoverAction> {
643    let mut deduped_actions = Vec::with_capacity(actions.len());
644    let mut go_to_type_targets = FxIndexSet::default();
645
646    let mut seen_implementation = false;
647    let mut seen_reference = false;
648    let mut seen_runnable = false;
649    for action in actions {
650        match action {
651            HoverAction::GoToType(targets) => {
652                go_to_type_targets.extend(targets);
653            }
654            HoverAction::Implementation(..) => {
655                if !seen_implementation {
656                    seen_implementation = true;
657                    deduped_actions.push(action);
658                }
659            }
660            HoverAction::Reference(..) => {
661                if !seen_reference {
662                    seen_reference = true;
663                    deduped_actions.push(action);
664                }
665            }
666            HoverAction::Runnable(..) => {
667                if !seen_runnable {
668                    seen_runnable = true;
669                    deduped_actions.push(action);
670                }
671            }
672        };
673    }
674
675    if !go_to_type_targets.is_empty() {
676        deduped_actions.push(HoverAction::GoToType(
677            go_to_type_targets.into_iter().sorted_by(|a, b| a.mod_path.cmp(&b.mod_path)).collect(),
678        ));
679    }
680
681    deduped_actions
682}