use std::{iter, mem::discriminant}; use crate::{ doc_links::token_as_doc_comment, navigation_target::ToNav, FilePosition, NavigationTarget, RangeInfo, TryToNav, UpmappingResult, }; use hir::{ AsAssocItem, AssocItem, DescendPreference, InFile, MacroFileIdExt, ModuleDef, Semantics, }; use ide_db::{ base_db::{AnchoredPath, FileLoader}, defs::{Definition, IdentClass}, helpers::pick_best_token, FileId, RootDatabase, }; use itertools::Itertools; use span::FileRange; use syntax::{ ast::{self, HasLoopBody, Label}, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, TextRange, T, }; // Feature: Go to Definition // // Navigates to the definition of an identifier. // // For outline modules, this will navigate to the source file of the module. // // |=== // | Editor | Shortcut // // | VS Code | kbd:[F12] // |=== // // image::https://user-images.githubusercontent.com/48062697/113065563-025fbe00-91b1-11eb-83e4-a5a703610b23.gif[] pub(crate) fn goto_definition( db: &RootDatabase, FilePosition { file_id, offset }: FilePosition, ) -> Option>> { let sema = &Semantics::new(db); let file = sema.parse_guess_edition(file_id).syntax().clone(); 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] | COMMENT => 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) { return doc_comment.get_definition_with_descend_at(sema, offset, |def, _, link_range| { let nav = def.try_to_nav(db)?; Some(RangeInfo::new(link_range, nav.collect())) }); } if let Some((range, resolution)) = sema.check_for_format_args_template(original_token.clone(), offset) { return Some(RangeInfo::new( range, match resolution { Some(res) => def_to_nav(db, Definition::from(res)), None => vec![], }, )); } if let Some(navs) = handle_control_flow_keywords(sema, &original_token) { return Some(RangeInfo::new(original_token.text_range(), navs)); } let navs = sema .descend_into_macros(DescendPreference::None, original_token.clone()) .into_iter() .filter_map(|token| { let parent = token.parent()?; if let Some(token) = ast::String::cast(token.clone()) { if let Some(x) = try_lookup_include_path(sema, token, file_id) { return Some(vec![x]); } } if ast::TokenTree::can_cast(parent.kind()) { if let Some(x) = try_lookup_macro_def_in_macro_use(sema, token) { return Some(vec![x]); } } Some( IdentClass::classify_node(sema, &parent)? .definitions() .into_iter() .flat_map(|def| { if let Definition::ExternCrateDecl(crate_def) = def { return crate_def .resolved_crate(db) .map(|it| it.root_module().to_nav(sema.db)) .into_iter() .flatten() .collect(); } try_filter_trait_item_definition(sema, &def) .unwrap_or_else(|| def_to_nav(sema.db, def)) }) .collect(), ) }) .flatten() .unique() .collect::>(); Some(RangeInfo::new(original_token.text_range(), navs)) } fn try_lookup_include_path( sema: &Semantics<'_, RootDatabase>, token: ast::String, file_id: FileId, ) -> Option { let file = sema.hir_file_for(&token.syntax().parent()?).macro_file()?; if !iter::successors(Some(file), |file| file.parent(sema.db).macro_file()) // Check that we are in the eager argument expansion of an include macro .any(|file| file.is_include_like_macro(sema.db) && file.eager_arg(sema.db).is_none()) { return None; } let path = token.value().ok()?; let file_id = sema.db.resolve_path(AnchoredPath { anchor: file_id, path: &path })?; let size = sema.db.file_text(file_id).len().try_into().ok()?; Some(NavigationTarget { file_id, full_range: TextRange::new(0.into(), size), name: path.into(), alias: None, focus_range: None, kind: None, container_name: None, description: None, docs: None, }) } fn try_lookup_macro_def_in_macro_use( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, ) -> Option { let extern_crate = token.parent()?.ancestors().find_map(ast::ExternCrate::cast)?; let extern_crate = sema.to_def(&extern_crate)?; let krate = extern_crate.resolved_crate(sema.db)?; for mod_def in krate.root_module().declarations(sema.db) { if let ModuleDef::Macro(mac) = mod_def { if mac.name(sema.db).as_str() == token.text() { if let Some(nav) = mac.try_to_nav(sema.db) { return Some(nav.call_site); } } } } None } /// finds the trait definition of an impl'd item, except function /// e.g. /// ```rust /// trait A { type a; } /// struct S; /// impl A for S { type a = i32; } // <-- on this associate type, will get the location of a in the trait /// ``` fn try_filter_trait_item_definition( sema: &Semantics<'_, RootDatabase>, def: &Definition, ) -> Option> { let db = sema.db; let assoc = def.as_assoc_item(db)?; match assoc { AssocItem::Function(..) => None, AssocItem::Const(..) | AssocItem::TypeAlias(..) => { let trait_ = assoc.implemented_trait(db)?; let name = def.name(db)?; let discriminant_value = discriminant(&assoc); trait_ .items(db) .iter() .filter(|itm| discriminant(*itm) == discriminant_value) .find_map(|itm| (itm.name(db)? == name).then(|| itm.try_to_nav(db)).flatten()) .map(|it| it.collect()) } } } fn handle_control_flow_keywords( sema: &Semantics<'_, RootDatabase>, token: &SyntaxToken, ) -> Option> { match token.kind() { // For `fn` / `loop` / `while` / `for` / `async`, return the keyword it self, // so that VSCode will find the references when using `ctrl + click` T![fn] | T![async] | T![try] | T![return] => try_find_fn_or_closure(sema, token), T![loop] | T![while] | T![break] | T![continue] => try_find_loop(sema, token), T![for] if token.parent().and_then(ast::ForExpr::cast).is_some() => { try_find_loop(sema, token) } _ => None, } } fn try_find_fn_or_closure( sema: &Semantics<'_, RootDatabase>, token: &SyntaxToken, ) -> Option> { fn find_exit_point( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, ) -> Option> { let db = sema.db; for anc in sema.token_ancestors_with_macros(token.clone()) { let file_id = sema.hir_file_for(&anc); match_ast! { match anc { ast::Fn(fn_) => { let fn_: ast::Fn = fn_; let nav = sema.to_def(&fn_)?.try_to_nav(db)?; // For async token, we navigate to itself, which triggers // VSCode to find the references let focus_token = if matches!(token.kind(), T![async]) { fn_.async_token()? } else { fn_.fn_token()? }; let focus_range = InFile::new(file_id, focus_token.text_range()) .original_node_file_range_opt(db) .map(|(frange, _)| frange.range); return Some(nav.map(|it| { if focus_range.is_some_and(|range| it.full_range.contains_range(range)) { NavigationTarget { focus_range, ..it } } else { it } })); }, ast::ClosureExpr(c) => { let pipe_tok = c.param_list().and_then(|it| it.pipe_token())?.into(); let c_infile = InFile::new(file_id, c.into()); let nav = NavigationTarget::from_expr(db, c_infile, pipe_tok); return Some(nav); }, ast::BlockExpr(blk) => { match blk.modifier() { Some(ast::BlockModifier::Async(_)) => { let async_tok = blk.async_token()?.into(); let blk_infile = InFile::new(file_id, blk.into()); let nav = NavigationTarget::from_expr(db, blk_infile, async_tok); return Some(nav); }, Some(ast::BlockModifier::Try(_)) if token.kind() != T![return] => { let try_tok = blk.try_token()?.into(); let blk_infile = InFile::new(file_id, blk.into()); let nav = NavigationTarget::from_expr(db, blk_infile, try_tok); return Some(nav); }, _ => {} } }, _ => {} } } } None } sema.descend_into_macros(DescendPreference::None, token.clone()) .into_iter() .filter_map(|descended| find_exit_point(sema, descended)) .flatten() .collect_vec() .into() } fn try_find_loop( sema: &Semantics<'_, RootDatabase>, token: &SyntaxToken, ) -> Option> { fn find_break_point( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, label_matches: impl Fn(Option