diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index aa485fb63d..962e3502b4 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -2,9 +2,12 @@ use std::{iter, mem::discriminant}; use crate::{ doc_links::token_as_doc_comment, navigation_target::ToNav, FilePosition, NavigationTarget, - RangeInfo, TryToNav, + RangeInfo, TryToNav, UpmappingResult, +}; +use hir::{ + AsAssocItem, AssocItem, DescendPreference, HirFileId, InFile, MacroFileIdExt, ModuleDef, + Semantics, }; -use hir::{AsAssocItem, AssocItem, DescendPreference, MacroFileIdExt, ModuleDef, Semantics}; use ide_db::{ base_db::{AnchoredPath, FileLoader}, defs::{Definition, IdentClass}, @@ -12,7 +15,12 @@ use ide_db::{ FileId, RootDatabase, }; use itertools::Itertools; -use syntax::{ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, TextRange, T}; +use syntax::{ + ast::{self, HasLoopBody}, + match_ast, AstNode, AstToken, + SyntaxKind::{self, *}, + SyntaxNode, SyntaxToken, TextRange, T, +}; // Feature: Go to Definition // @@ -68,6 +76,10 @@ pub(crate) fn goto_definition( )); } + 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() @@ -190,6 +202,194 @@ fn try_filter_trait_item_definition( } } +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>, + file_id: HirFileId, + ancestors: impl Iterator, + ) -> Option> { + let db = sema.db; + + for anc in ancestors { + match_ast! { + match anc { + ast::Fn(fn_) => { + let hir_fn: hir::Function = sema.to_def(&fn_)?; + let nav = hir_fn.try_to_nav(db)?; + + // For async token, we navigate to itself, which triggers + // VSCode to find the references + let focus_token = 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 nav = NavigationTarget::from_expr(db, InFile::new(file_id, c.into()), pipe_tok); + return Some(nav); + }, + ast::BlockExpr(blk) => match blk.modifier() { + Some(ast::BlockModifier::Async(_)) => { + let async_tok = blk.async_token()?.into(); + let nav = NavigationTarget::from_expr(db, InFile::new(file_id, blk.into()), async_tok); + return Some(nav); + }, + Some(ast::BlockModifier::Try(_)) if cursor_token_kind != T![return] => { + let try_tok = blk.try_token()?.into(); + let nav = NavigationTarget::from_expr(db, InFile::new(file_id, blk.into()), try_tok); + return Some(nav); + }, + _ => {} + }, + _ => {} + } + } + } + None + } + + sema.descend_into_macros(DescendPreference::None, token.clone()) + .into_iter() + .filter_map(|descended| { + let file_id = sema.hir_file_for(&descended.parent()?); + + // Try to find the function in the macro file + find_exit_point(sema, file_id, descended.parent_ancestors()).or_else(|| { + // If not found, try to find it in the root file + if file_id.is_macro() { + token + .parent_ancestors() + .find(|it| ast::TokenTree::can_cast(it.kind())) + .and_then(|parent| { + let file_id = sema.hir_file_for(&parent); + find_exit_point(sema, file_id, parent.ancestors()) + }) + } else { + None + } + }) + }) + .flatten() + .collect_vec() + .into() +} + +fn try_find_loop( + sema: &Semantics<'_, RootDatabase>, + token: &SyntaxToken, +) -> Option> { + fn find_break_point( + sema: &Semantics<'_, RootDatabase>, + file_id: HirFileId, + ancestors: impl Iterator, + lbl: &Option, + ) -> Option> { + let db = sema.db; + let label_matches = |it: Option| match lbl { + Some(lbl) => { + Some(lbl.text()) == it.and_then(|it| it.lifetime()).as_ref().map(|it| it.text()) + } + None => true, + }; + + for anc in ancestors.filter_map(ast::Expr::cast) { + match anc { + ast::Expr::LoopExpr(loop_) if label_matches(loop_.label()) => { + let expr = ast::Expr::LoopExpr(loop_.clone()); + let loop_tok = loop_.loop_token()?.into(); + let nav = NavigationTarget::from_expr(db, InFile::new(file_id, expr), loop_tok); + return Some(nav); + } + ast::Expr::WhileExpr(while_) if label_matches(while_.label()) => { + let expr = ast::Expr::WhileExpr(while_.clone()); + let while_tok = while_.while_token()?.into(); + let nav = + NavigationTarget::from_expr(db, InFile::new(file_id, expr), while_tok); + return Some(nav); + } + ast::Expr::ForExpr(for_) if label_matches(for_.label()) => { + let expr = ast::Expr::ForExpr(for_.clone()); + let for_tok = for_.for_token()?.into(); + let nav = NavigationTarget::from_expr(db, InFile::new(file_id, expr), for_tok); + return Some(nav); + } + ast::Expr::BlockExpr(blk) + if blk.label().is_some() && label_matches(blk.label()) => + { + let expr = ast::Expr::BlockExpr(blk.clone()); + let lbl_tok = blk.label().unwrap().lifetime()?.lifetime_ident_token()?.into(); + let nav = NavigationTarget::from_expr(db, InFile::new(file_id, expr), lbl_tok); + return Some(nav); + } + _ => {} + } + } + None + } + + let parent = token.parent()?; + let lbl = match_ast! { + match parent { + ast::BreakExpr(break_) => break_.lifetime(), + ast::ContinueExpr(continue_) => continue_.lifetime(), + _ => None, + } + }; + + sema.descend_into_macros(DescendPreference::None, token.clone()) + .into_iter() + .filter_map(|descended| { + let file_id = sema.hir_file_for(&descended.parent()?); + + // Try to find the function in the macro file + find_break_point(sema, file_id, descended.parent_ancestors(), &lbl).or_else(|| { + // If not found, try to find it in the root file + if file_id.is_macro() { + token + .parent_ancestors() + .find(|it| ast::TokenTree::can_cast(it.kind())) + .and_then(|parent| { + let file_id = sema.hir_file_for(&parent); + find_break_point(sema, file_id, parent.ancestors(), &lbl) + }) + } else { + None + } + }) + }) + .flatten() + .collect_vec() + .into() +} + fn def_to_nav(db: &RootDatabase, def: Definition) -> Vec { def.try_to_nav(db).map(|it| it.collect()).unwrap_or_default() } @@ -2313,4 +2513,200 @@ pub mod prelude { "#, ); } + + #[test] + fn goto_def_on_return_kw() { + check( + r#" +macro_rules! N { + ($i:ident, $x:expr, $blk:expr) => { + for $i in 0..$x { + $blk + } + }; +} + +fn main() { + fn f() { + // ^^ + N!(i, 5, { + println!("{}", i); + return$0; + }); + + for i in 1..5 { + return; + } + (|| { + return; + })(); + } +} +"#, + ) + } + + #[test] + fn goto_def_on_return_kw_in_closure() { + check( + r#" +macro_rules! N { + ($i:ident, $x:expr, $blk:expr) => { + for $i in 0..$x { + $blk + } + }; +} + +fn main() { + fn f() { + N!(i, 5, { + println!("{}", i); + return; + }); + + for i in 1..5 { + return; + } + (|| { + // ^ + return$0; + })(); + } +} +"#, + ) + } + + #[test] + fn goto_def_on_break_kw() { + check( + r#" +fn main() { + for i in 1..5 { + // ^^^ + break$0; + } +} +"#, + ) + } + + #[test] + fn goto_def_on_continue_kw() { + check( + r#" +fn main() { + for i in 1..5 { + // ^^^ + continue$0; + } +} +"#, + ) + } + + #[test] + fn goto_def_on_break_kw_for_block() { + check( + r#" +fn main() { + 'a:{ + // ^^ + break$0 'a; + } +} +"#, + ) + } + + #[test] + fn goto_def_on_break_with_label() { + check( + r#" +fn foo() { + 'outer: loop { + // ^^^^ + 'inner: loop { + 'innermost: loop { + } + break$0 'outer; + } + } +} +"#, + ); + } + + #[test] + fn goto_def_on_return_in_try() { + check( + r#" +fn main() { + fn f() { + // ^^ + try { + return$0; + } + + return; + } +} +"#, + ) + } + + #[test] + fn goto_def_on_break_in_try() { + check( + r#" +fn main() { + for i in 1..100 { + // ^^^ + let x: Result<(), ()> = try { + break$0; + }; + } +} +"#, + ) + } + + #[test] + fn goto_def_on_return_in_async_block() { + check( + r#" +fn main() { + async { + // ^^^^^ + return$0; + } +} +"#, + ) + } + + #[test] + fn goto_def_on_for_kw() { + check( + r#" +fn main() { + for$0 i in 1..5 {} + // ^^^ +} +"#, + ) + } + + #[test] + fn goto_def_on_fn_kw() { + check( + r#" +fn main() { + fn$0 foo() {} + // ^^ +} +"#, + ) + } } diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs index c8803850d1..f1e80ab15d 100644 --- a/crates/ide/src/navigation_target.rs +++ b/crates/ide/src/navigation_target.rs @@ -16,7 +16,7 @@ use ide_db::{ use stdx::never; use syntax::{ ast::{self, HasName}, - format_smolstr, AstNode, SmolStr, SyntaxNode, TextRange, ToSmolStr, + format_smolstr, AstNode, SmolStr, SyntaxElement, SyntaxNode, TextRange, ToSmolStr, }; /// `NavigationTarget` represents an element in the editor's UI which you can @@ -152,6 +152,22 @@ impl NavigationTarget { ) } + pub(crate) fn from_expr( + db: &RootDatabase, + InFile { file_id, value }: InFile, + focus_syntax: SyntaxElement, + ) -> UpmappingResult { + let name: SmolStr = "".into(); + let kind = SymbolKind::Label; + let focus_range = Some(focus_syntax.text_range()); + + orig_range_with_focus_r(db, file_id, value.syntax().text_range(), focus_range).map( + |(FileRange { file_id, range: full_range }, focus_range)| { + NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind) + }, + ) + } + fn from_syntax( file_id: FileId, name: SmolStr, @@ -710,7 +726,7 @@ impl IntoIterator for UpmappingResult { } impl UpmappingResult { - fn map(self, f: impl Fn(T) -> U) -> UpmappingResult { + pub(crate) fn map(self, f: impl Fn(T) -> U) -> UpmappingResult { UpmappingResult { call_site: f(self.call_site), def_site: self.def_site.map(f) } } } @@ -736,9 +752,9 @@ fn orig_range_with_focus_r( db: &RootDatabase, hir_file: HirFileId, value: TextRange, - name: Option, + focus_range: Option, ) -> UpmappingResult<(FileRange, Option)> { - let Some(name) = name else { return orig_range_r(db, hir_file, value) }; + let Some(name) = focus_range else { return orig_range_r(db, hir_file, value) }; let call_kind = || db.lookup_intern_macro_call(hir_file.macro_file().unwrap().macro_call_id).kind;