From 083b16f33314d0d9349c3e24689e317ad0135907 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Sun, 24 Aug 2025 12:02:25 +0800 Subject: [PATCH] Add BreakExpr completion suggest - Move `ide::goto_definition::find_loops` into `ide_db::syntax_helpers::node_ext::find_loops` --- crates/ide-completion/src/context/analysis.rs | 13 +- crates/ide-completion/src/context/tests.rs | 119 ++++++++++++++++++ crates/ide-db/src/syntax_helpers/node_ext.rs | 42 +++++++ crates/ide/src/goto_definition.rs | 53 +------- crates/ide/src/highlight_related.rs | 6 +- 5 files changed, 177 insertions(+), 56 deletions(-) diff --git a/crates/ide-completion/src/context/analysis.rs b/crates/ide-completion/src/context/analysis.rs index d6d3978385d9..7e1dd295e3c5 100644 --- a/crates/ide-completion/src/context/analysis.rs +++ b/crates/ide-completion/src/context/analysis.rs @@ -2,7 +2,9 @@ use std::iter; use hir::{ExpandResult, InFile, Semantics, Type, TypeInfo, Variant}; -use ide_db::{RootDatabase, active_parameter::ActiveParameter}; +use ide_db::{ + RootDatabase, active_parameter::ActiveParameter, syntax_helpers::node_ext::find_loops, +}; use itertools::Either; use stdx::always; use syntax::{ @@ -752,6 +754,12 @@ fn expected_type_and_name<'db>( }); (ty, None) }, + ast::BreakExpr(it) => { + let ty = it.break_token() + .and_then(|it| find_loops(sema, &it)?.next()) + .and_then(|expr| sema.type_of_expr(&expr)); + (ty.map(TypeInfo::original), None) + }, ast::ClosureExpr(it) => { let ty = sema.type_of_expr(&it.into()); ty.and_then(|ty| ty.original.as_callable(sema.db)) @@ -2006,7 +2014,8 @@ fn prev_special_biased_token_at_trivia(mut token: SyntaxToken) -> SyntaxToken { | T![|] | T![return] | T![break] - | T![continue] = prev.kind() + | T![continue] + | T![lifetime_ident] = prev.kind() { token = prev } diff --git a/crates/ide-completion/src/context/tests.rs b/crates/ide-completion/src/context/tests.rs index 51d28bd4ff98..6208a5c2dc79 100644 --- a/crates/ide-completion/src/context/tests.rs +++ b/crates/ide-completion/src/context/tests.rs @@ -593,6 +593,125 @@ fn foo() { ); } +#[test] +fn expected_type_break_expr_in_loop() { + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + let _x: State = loop { + { + break State::Stop; + break $0; + } + }; +} +"#, + expect![[r#"ty: State, name: ?"#]], + ); + + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + let _x: State = 'a: loop { + { + break State::Stop; + break $0; + } + }; +} +"#, + expect![[r#"ty: State, name: ?"#]], + ); + + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + let _x: State = 'a: loop { + while true { + break $0; + } + }; +} +"#, + expect![[r#"ty: (), name: ?"#]], + ); +} + +#[test] +fn expected_type_break_expr_in_labeled_loop() { + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + let _x: State = 'a: loop { + let _y: i32 = loop { + { + break 'a State::Stop; + break 'a $0; + } + }; + }; +} +"#, + expect![[r#"ty: State, name: ?"#]], + ); + + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + let _x: State = 'a: loop { + let _y: i32 = loop { + while true { + break 'a State::Stop; + break 'a $0; + } + }; + }; +} +"#, + expect![[r#"ty: State, name: ?"#]], + ); + + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + 'a: while true { + let _x: State = loop { + break State::Stop; + break 'a $0; + }; + } +} +"#, + expect![[r#"ty: (), name: ?"#]], + ); +} + +#[test] +fn expected_type_break_expr_in_labeled_block() { + check_expected_type_and_name( + r#" +enum State { Stop } +fn foo() { + let _x: State = 'a: { + let _y: i32 = 'b: { + { + break 'a State::Stop; + break 'a $0; + }; + }; + }; +} +"#, + expect![[r#"ty: State, name: ?"#]], + ); +} + #[test] fn expected_type_logic_op() { check_expected_type_and_name( diff --git a/crates/ide-db/src/syntax_helpers/node_ext.rs b/crates/ide-db/src/syntax_helpers/node_ext.rs index e1d140730edc..acce066b8323 100644 --- a/crates/ide-db/src/syntax_helpers/node_ext.rs +++ b/crates/ide-db/src/syntax_helpers/node_ext.rs @@ -419,6 +419,48 @@ pub fn eq_label_lt(lt1: &Option, lt2: &Option) -> lt1.as_ref().zip(lt2.as_ref()).is_some_and(|(lt, lbl)| lt.text() == lbl.text()) } +/// Find the loop or block to break or continue, multiple results may be caused by macros. +pub fn find_loops( + sema: &hir::Semantics<'_, crate::RootDatabase>, + token: &syntax::SyntaxToken, +) -> Option> { + let parent = token.parent()?; + let lbl = syntax::match_ast! { + match parent { + ast::BreakExpr(break_) => break_.lifetime(), + ast::ContinueExpr(continue_) => continue_.lifetime(), + _ => None, + } + }; + let label_matches = + move |it: Option| match (lbl.as_ref(), it.and_then(|it| it.lifetime())) { + (Some(lbl), Some(it)) => lbl.text() == it.text(), + (None, _) => true, + (Some(_), None) => false, + }; + + let find_ancestors = move |token| { + for anc in sema.token_ancestors_with_macros(token).filter_map(ast::Expr::cast) { + let node = match &anc { + ast::Expr::LoopExpr(loop_) if label_matches(loop_.label()) => anc, + ast::Expr::WhileExpr(while_) if label_matches(while_.label()) => anc, + ast::Expr::ForExpr(for_) if label_matches(for_.label()) => anc, + ast::Expr::BlockExpr(blk) + if blk.label().is_some() && label_matches(blk.label()) => + { + anc + } + _ => continue, + }; + + return Some(node); + } + None + }; + + sema.descend_into_macros(token.clone()).into_iter().filter_map(find_ancestors).into() +} + struct TreeWithDepthIterator { preorder: Preorder, depth: u32, diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index d663b703c3a1..63a2dd35a089 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -16,15 +16,12 @@ use ide_db::{ defs::{Definition, IdentClass}, famous_defs::FamousDefs, helpers::pick_best_token, + syntax_helpers::node_ext::find_loops, }; use itertools::Itertools; use span::{Edition, FileId}; use syntax::{ - AstNode, AstToken, - SyntaxKind::*, - SyntaxNode, SyntaxToken, T, TextRange, - ast::{self, HasLoopBody}, - match_ast, + AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, T, TextRange, ast, match_ast, }; #[derive(Debug)] @@ -511,51 +508,6 @@ fn nav_for_branch_exit_points( Some(navs) } -pub(crate) fn find_loops( - sema: &Semantics<'_, RootDatabase>, - token: &SyntaxToken, -) -> Option> { - let parent = token.parent()?; - let lbl = match_ast! { - match parent { - ast::BreakExpr(break_) => break_.lifetime(), - ast::ContinueExpr(continue_) => continue_.lifetime(), - _ => None, - } - }; - let label_matches = - |it: Option| match (lbl.as_ref(), it.and_then(|it| it.lifetime())) { - (Some(lbl), Some(it)) => lbl.text() == it.text(), - (None, _) => true, - (Some(_), None) => false, - }; - - let find_ancestors = |token: SyntaxToken| { - for anc in sema.token_ancestors_with_macros(token).filter_map(ast::Expr::cast) { - let node = match &anc { - ast::Expr::LoopExpr(loop_) if label_matches(loop_.label()) => anc, - ast::Expr::WhileExpr(while_) if label_matches(while_.label()) => anc, - ast::Expr::ForExpr(for_) if label_matches(for_.label()) => anc, - ast::Expr::BlockExpr(blk) - if blk.label().is_some() && label_matches(blk.label()) => - { - anc - } - _ => continue, - }; - - return Some(node); - } - None - }; - - sema.descend_into_macros(token.clone()) - .into_iter() - .filter_map(find_ancestors) - .collect_vec() - .into() -} - fn nav_for_break_points( sema: &Semantics<'_, RootDatabase>, token: &SyntaxToken, @@ -563,7 +515,6 @@ fn nav_for_break_points( let db = sema.db; let navs = find_loops(sema, token)? - .into_iter() .filter_map(|expr| { let file_id = sema.hir_file_for(expr.syntax()); let expr_in_file = InFile::new(file_id, expr.clone()); diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs index 04ce5a7567f3..42084b74012e 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -7,8 +7,8 @@ use ide_db::{ helpers::pick_best_token, search::{FileReference, ReferenceCategory, SearchScope}, syntax_helpers::node_ext::{ - eq_label_lt, for_each_tail_expr, full_path_of_name_ref, is_closure_or_blk_with_modif, - preorder_expr_with_ctx_checker, + eq_label_lt, find_loops, for_each_tail_expr, full_path_of_name_ref, + is_closure_or_blk_with_modif, preorder_expr_with_ctx_checker, }, }; use syntax::{ @@ -564,7 +564,7 @@ pub(crate) fn highlight_break_points( Some(highlights) } - let Some(loops) = goto_definition::find_loops(sema, &token) else { + let Some(loops) = find_loops(sema, &token) else { return FxHashMap::default(); };