mod render; #[cfg(test)] mod tests; use std::iter; use either::Either; use hir::{HasSource, Semantics}; use ide_db::{ base_db::FileRange, defs::{Definition, IdentClass, OperatorClass}, famous_defs::FamousDefs, helpers::pick_best_token, FxIndexSet, RootDatabase, }; use itertools::Itertools; use syntax::{ast, AstNode, SyntaxKind::*, SyntaxNode, T}; use crate::{ doc_links::token_as_doc_comment, markdown_remove::remove_markdown, markup::Markup, runnables::{runnable_fn, runnable_mod}, FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav, }; #[derive(Clone, Debug, PartialEq, Eq)] pub struct HoverConfig { pub links_in_hover: bool, pub documentation: bool, pub keywords: bool, pub format: HoverDocFormat, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum HoverDocFormat { Markdown, PlainText, } #[derive(Debug, Clone)] pub enum HoverAction { Runnable(Runnable), Implementation(FilePosition), Reference(FilePosition), GoToType(Vec), } impl HoverAction { fn goto_type_from_targets(db: &RootDatabase, targets: Vec) -> Self { let targets = targets .into_iter() .filter_map(|it| { Some(HoverGotoTypeData { mod_path: render::path( db, it.module(db)?, it.name(db).map(|name| name.to_string()), ), nav: it.try_to_nav(db)?, }) }) .collect(); HoverAction::GoToType(targets) } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct HoverGotoTypeData { pub mod_path: String, pub nav: NavigationTarget, } /// Contains the results when hovering over an item #[derive(Debug, Default)] pub struct HoverResult { pub markup: Markup, pub actions: Vec, } // Feature: Hover // // Shows additional information, like the type of an expression or the documentation for a definition when "focusing" code. // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. // // image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[] pub(crate) fn hover( db: &RootDatabase, frange @ FileRange { file_id, range }: FileRange, config: &HoverConfig, ) -> Option> { let sema = &hir::Semantics::new(db); let file = sema.parse(file_id).syntax().clone(); let mut res = if range.is_empty() { hover_simple(sema, FilePosition { file_id, offset: range.start() }, file, config) } else { hover_ranged(sema, frange, file, config) }?; if let HoverDocFormat::PlainText = config.format { res.info.markup = remove_markdown(res.info.markup.as_str()).into(); } Some(res) } fn hover_simple( sema: &Semantics<'_, RootDatabase>, FilePosition { file_id, offset }: FilePosition, file: SyntaxNode, config: &HoverConfig, ) -> Option> { let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind { IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] | T![Self] | T![_] => 4, // index and prefix ops T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] => 3, kind if kind.is_keyword() => 2, T!['('] | T![')'] => 2, kind if kind.is_trivia() => 0, _ => 1, })?; if let Some(doc_comment) = token_as_doc_comment(&original_token) { cov_mark::hit!(no_highlight_on_comment_hover); return doc_comment.get_definition_with_descend_at(sema, offset, |def, node, range| { let res = hover_for_definition(sema, file_id, def, &node, config)?; Some(RangeInfo::new(range, res)) }); } let in_attr = original_token .parent_ancestors() .filter_map(ast::Item::cast) .any(|item| sema.is_attr_macro_call(&item)) && !matches!( original_token.parent().and_then(ast::TokenTree::cast), Some(tt) if tt.syntax().ancestors().any(|it| ast::Meta::can_cast(it.kind())) ); // prefer descending the same token kind in attribute expansions, in normal macros text // equivalency is more important let descended = if in_attr { [sema.descend_into_macros_with_kind_preference(original_token.clone())].into() } else { sema.descend_into_macros_with_same_text(original_token.clone()) }; let descended = || descended.iter(); let result = descended() // try lint hover .find_map(|token| { // FIXME: Definition should include known lints and the like instead of having this special case here let attr = token.parent_ancestors().find_map(ast::Attr::cast)?; render::try_for_lint(&attr, token) }) // try definitions .or_else(|| { descended() .filter_map(|token| { let node = token.parent()?; let class = IdentClass::classify_token(sema, token)?; if let IdentClass::Operator(OperatorClass::Await(_)) = class { // It's better for us to fall back to the keyword hover here, // rendering poll is very confusing return None; } Some(class.definitions().into_iter().zip(iter::once(node).cycle())) }) .flatten() .unique_by(|&(def, _)| def) .filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config)) .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| { acc.actions.extend(actions); acc.markup = Markup::from(format!("{}\n---\n{markup}", acc.markup)); acc }) }) // try keywords .or_else(|| descended().find_map(|token| render::keyword(sema, config, token))) // try _ hovers .or_else(|| descended().find_map(|token| render::underscore(sema, config, token))) // try rest pattern hover .or_else(|| { descended().find_map(|token| { if token.kind() != DOT2 { return None; } let rest_pat = token.parent().and_then(ast::RestPat::cast)?; let record_pat_field_list = rest_pat.syntax().parent().and_then(ast::RecordPatFieldList::cast)?; let record_pat = record_pat_field_list.syntax().parent().and_then(ast::RecordPat::cast)?; Some(render::struct_rest_pat(sema, config, &record_pat)) }) }) // try () call hovers .or_else(|| { descended().find_map(|token| { if token.kind() != T!['('] && token.kind() != T![')'] { return None; } let arg_list = token.parent().and_then(ast::ArgList::cast)?.syntax().parent()?; let call_expr = syntax::match_ast! { match arg_list { ast::CallExpr(expr) => expr.into(), ast::MethodCallExpr(expr) => expr.into(), _ => return None, } }; render::type_info_of(sema, config, &Either::Left(call_expr)) }) }); result.map(|mut res: HoverResult| { res.actions = dedupe_or_merge_hover_actions(res.actions); RangeInfo::new(original_token.text_range(), res) }) } fn hover_ranged( sema: &Semantics<'_, RootDatabase>, FileRange { range, .. }: FileRange, file: SyntaxNode, config: &HoverConfig, ) -> Option> { // FIXME: make this work in attributes let expr_or_pat = file .covering_element(range) .ancestors() .take_while(|it| ast::MacroCall::can_cast(it.kind()) || !ast::Item::can_cast(it.kind())) .find_map(Either::::cast)?; let res = match &expr_or_pat { Either::Left(ast::Expr::TryExpr(try_expr)) => render::try_expr(sema, config, try_expr), Either::Left(ast::Expr::PrefixExpr(prefix_expr)) if prefix_expr.op_kind() == Some(ast::UnaryOp::Deref) => { render::deref_expr(sema, config, prefix_expr) } _ => None, }; let res = res.or_else(|| render::type_info_of(sema, config, &expr_or_pat)); res.map(|it| { let range = match expr_or_pat { Either::Left(it) => it.syntax().text_range(), Either::Right(it) => it.syntax().text_range(), }; RangeInfo::new(range, it) }) } pub(crate) fn hover_for_definition( sema: &Semantics<'_, RootDatabase>, file_id: FileId, definition: Definition, node: &SyntaxNode, config: &HoverConfig, ) -> Option { let famous_defs = match &definition { Definition::BuiltinType(_) => Some(FamousDefs(sema, sema.scope(node)?.krate())), _ => None, }; render::definition(sema.db, definition, famous_defs.as_ref(), config).map(|markup| { HoverResult { markup: render::process_markup(sema.db, definition, &markup, config), actions: [ show_implementations_action(sema.db, definition), show_fn_references_action(sema.db, definition), runnable_action(sema, definition, file_id), goto_type_action_for_def(sema.db, definition), ] .into_iter() .flatten() .collect(), } }) } fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option { fn to_action(nav_target: NavigationTarget) -> HoverAction { HoverAction::Implementation(FilePosition { file_id: nav_target.file_id, offset: nav_target.focus_or_full_range().start(), }) } let adt = match def { Definition::Trait(it) => return it.try_to_nav(db).map(to_action), Definition::Adt(it) => Some(it), Definition::SelfType(it) => it.self_ty(db).as_adt(), _ => None, }?; adt.try_to_nav(db).map(to_action) } fn show_fn_references_action(db: &RootDatabase, def: Definition) -> Option { match def { Definition::Function(it) => it.try_to_nav(db).map(|nav_target| { HoverAction::Reference(FilePosition { file_id: nav_target.file_id, offset: nav_target.focus_or_full_range().start(), }) }), _ => None, } } fn runnable_action( sema: &hir::Semantics<'_, RootDatabase>, def: Definition, file_id: FileId, ) -> Option { match def { Definition::Module(it) => runnable_mod(sema, it).map(HoverAction::Runnable), Definition::Function(func) => { let src = func.source(sema.db)?; if src.file_id != file_id.into() { cov_mark::hit!(hover_macro_generated_struct_fn_doc_comment); cov_mark::hit!(hover_macro_generated_struct_fn_doc_attr); return None; } runnable_fn(sema, func).map(HoverAction::Runnable) } _ => None, } } fn goto_type_action_for_def(db: &RootDatabase, def: Definition) -> Option { let mut targets: Vec = Vec::new(); let mut push_new_def = |item: hir::ModuleDef| { if !targets.contains(&item) { targets.push(item); } }; if let Definition::GenericParam(hir::GenericParam::TypeParam(it)) = def { it.trait_bounds(db).into_iter().for_each(|it| push_new_def(it.into())); } else { let ty = match def { Definition::Local(it) => it.ty(db), Definition::GenericParam(hir::GenericParam::ConstParam(it)) => it.ty(db), Definition::Field(field) => field.ty(db), Definition::Function(function) => function.ret_type(db), _ => return None, }; walk_and_push_ty(db, &ty, &mut push_new_def); } Some(HoverAction::goto_type_from_targets(db, targets)) } fn walk_and_push_ty( db: &RootDatabase, ty: &hir::Type, push_new_def: &mut dyn FnMut(hir::ModuleDef), ) { ty.walk(db, |t| { if let Some(adt) = t.as_adt() { push_new_def(adt.into()); } else if let Some(trait_) = t.as_dyn_trait() { push_new_def(trait_.into()); } else if let Some(traits) = t.as_impl_traits(db) { traits.for_each(|it| push_new_def(it.into())); } else if let Some(trait_) = t.as_associated_type_parent_trait(db) { push_new_def(trait_.into()); } }); } fn dedupe_or_merge_hover_actions(actions: Vec) -> Vec { let mut deduped_actions = Vec::with_capacity(actions.len()); let mut go_to_type_targets = FxIndexSet::default(); let mut seen_implementation = false; let mut seen_reference = false; let mut seen_runnable = false; for action in actions { match action { HoverAction::GoToType(targets) => { go_to_type_targets.extend(targets); } HoverAction::Implementation(..) => { if !seen_implementation { seen_implementation = true; deduped_actions.push(action); } } HoverAction::Reference(..) => { if !seen_reference { seen_reference = true; deduped_actions.push(action); } } HoverAction::Runnable(..) => { if !seen_runnable { seen_runnable = true; deduped_actions.push(action); } } }; } if !go_to_type_targets.is_empty() { deduped_actions.push(HoverAction::GoToType(go_to_type_targets.into_iter().collect())); } deduped_actions }