From 9eb11f9220325b08ac92a23c78a4ba6a9543438b Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Tue, 16 Sep 2025 16:48:19 +0800 Subject: [PATCH] Add break value completion support ```rust fn foo() -> i32 { loop { $0 } } ``` **Before this PR**: ```rust fn foo() -> i32 { loop { break; } } ``` **After this PR**: ```rust fn foo() -> i32 { loop { break $0; } } ``` --- crates/ide-completion/src/completions/expr.rs | 17 +++++--- .../ide-completion/src/completions/postfix.rs | 2 +- .../ide-completion/src/completions/record.rs | 5 +-- crates/ide-completion/src/context.rs | 6 +-- crates/ide-completion/src/context/analysis.rs | 28 +++++++------ crates/ide-completion/src/tests/expression.rs | 39 +++++++++++++++++++ 6 files changed, 72 insertions(+), 25 deletions(-) diff --git a/crates/ide-completion/src/completions/expr.rs b/crates/ide-completion/src/completions/expr.rs index 4c7935794151..5cae7bd89fff 100644 --- a/crates/ide-completion/src/completions/expr.rs +++ b/crates/ide-completion/src/completions/expr.rs @@ -9,7 +9,7 @@ use syntax::ast; use crate::{ CompletionContext, Completions, completions::record::add_default_update, - context::{BreakableKind, PathCompletionCtx, PathExprCtx, Qualified}, + context::{PathCompletionCtx, PathExprCtx, Qualified}, }; struct PathCallback<'a, F> { @@ -57,7 +57,6 @@ pub(crate) fn complete_expr_path( let &PathExprCtx { in_block_expr, - in_breakable, after_if_expr, before_else_kw, in_condition, @@ -68,6 +67,7 @@ pub(crate) fn complete_expr_path( after_amp, ref is_func_update, ref innermost_ret_ty, + ref innermost_breakable_ty, ref impl_, in_match_guard, .. @@ -405,14 +405,21 @@ pub(crate) fn complete_expr_path( add_keyword("mut", "mut "); } - if in_breakable != BreakableKind::None { + if let Some(loop_ty) = innermost_breakable_ty { if in_block_expr { add_keyword("continue", "continue;"); - add_keyword("break", "break;"); } else { add_keyword("continue", "continue"); - add_keyword("break", "break"); } + add_keyword( + "break", + match (loop_ty.is_unit(), in_block_expr) { + (true, true) => "break;", + (true, false) => "break", + (false, true) => "break $0;", + (false, false) => "break $0", + }, + ); } if let Some(ret_ty) = innermost_ret_ty { diff --git a/crates/ide-completion/src/completions/postfix.rs b/crates/ide-completion/src/completions/postfix.rs index 51acdb7802b3..73cbe3f0aaab 100644 --- a/crates/ide-completion/src/completions/postfix.rs +++ b/crates/ide-completion/src/completions/postfix.rs @@ -291,7 +291,7 @@ pub(crate) fn complete_postfix( ) .add_to(acc, ctx.db); - if let BreakableKind::Block | BreakableKind::Loop = expr_ctx.in_breakable { + if let Some(BreakableKind::Block | BreakableKind::Loop) = expr_ctx.in_breakable { postfix_snippet( "break", "break expr", diff --git a/crates/ide-completion/src/completions/record.rs b/crates/ide-completion/src/completions/record.rs index 2f5abd18934f..28b324d61afa 100644 --- a/crates/ide-completion/src/completions/record.rs +++ b/crates/ide-completion/src/completions/record.rs @@ -135,10 +135,7 @@ fn complete_fields( receiver: None, receiver_ty: None, kind: DotAccessKind::Field { receiver_is_ambiguous_float_literal: false }, - ctx: DotAccessExprCtx { - in_block_expr: false, - in_breakable: crate::context::BreakableKind::None, - }, + ctx: DotAccessExprCtx { in_block_expr: false, in_breakable: None }, }, None, field, diff --git a/crates/ide-completion/src/context.rs b/crates/ide-completion/src/context.rs index d9b0d3296a96..4032329ac658 100644 --- a/crates/ide-completion/src/context.rs +++ b/crates/ide-completion/src/context.rs @@ -143,7 +143,7 @@ pub(crate) struct AttrCtx { #[derive(Debug, PartialEq, Eq)] pub(crate) struct PathExprCtx<'db> { pub(crate) in_block_expr: bool, - pub(crate) in_breakable: BreakableKind, + pub(crate) in_breakable: Option, pub(crate) after_if_expr: bool, pub(crate) before_else_kw: bool, /// Whether this expression is the direct condition of an if or while expression @@ -157,6 +157,7 @@ pub(crate) struct PathExprCtx<'db> { pub(crate) is_func_update: Option, pub(crate) self_param: Option, pub(crate) innermost_ret_ty: Option>, + pub(crate) innermost_breakable_ty: Option>, pub(crate) impl_: Option, /// Whether this expression occurs in match arm guard position: before the /// fat arrow token @@ -412,12 +413,11 @@ pub(crate) enum DotAccessKind { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct DotAccessExprCtx { pub(crate) in_block_expr: bool, - pub(crate) in_breakable: BreakableKind, + pub(crate) in_breakable: Option, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum BreakableKind { - None, Loop, For, While, diff --git a/crates/ide-completion/src/context/analysis.rs b/crates/ide-completion/src/context/analysis.rs index 440ac9ad0743..873eceff5f5f 100644 --- a/crates/ide-completion/src/context/analysis.rs +++ b/crates/ide-completion/src/context/analysis.rs @@ -926,7 +926,7 @@ fn classify_name_ref<'db>( receiver_ty, kind: DotAccessKind::Field { receiver_is_ambiguous_float_literal }, receiver, - ctx: DotAccessExprCtx { in_block_expr: is_in_block(field.syntax()), in_breakable: is_in_breakable(field.syntax()) } + ctx: DotAccessExprCtx { in_block_expr: is_in_block(field.syntax()), in_breakable: is_in_breakable(field.syntax()).unzip().0 } }); return Some(make_res(kind)); }, @@ -941,7 +941,7 @@ fn classify_name_ref<'db>( receiver_ty: receiver.as_ref().and_then(|it| sema.type_of_expr(it)), kind: DotAccessKind::Method { has_parens }, receiver, - ctx: DotAccessExprCtx { in_block_expr: is_in_block(method.syntax()), in_breakable: is_in_breakable(method.syntax()) } + ctx: DotAccessExprCtx { in_block_expr: is_in_block(method.syntax()), in_breakable: is_in_breakable(method.syntax()).unzip().0 } }); return Some(make_res(kind)); }, @@ -1229,7 +1229,7 @@ fn classify_name_ref<'db>( let make_path_kind_expr = |expr: ast::Expr| { let it = expr.syntax(); let in_block_expr = is_in_block(it); - let in_loop_body = is_in_breakable(it); + let (in_loop_body, innermost_breakable) = is_in_breakable(it).unzip(); let after_if_expr = after_if_expr(it.clone()); let ref_expr_parent = path.as_single_name_ref().and_then(|_| it.parent()).and_then(ast::RefExpr::cast); @@ -1283,6 +1283,11 @@ fn classify_name_ref<'db>( None => (None, None), } }; + let innermost_breakable_ty = innermost_breakable + .and_then(ast::Expr::cast) + .and_then(|expr| find_node_in_file_compensated(sema, original_file, &expr)) + .and_then(|expr| sema.type_of_expr(&expr)) + .map(|ty| if ty.original.is_never() { ty.adjusted() } else { ty.original() }); let is_func_update = func_update_record(it); let in_condition = is_in_condition(&expr); let after_incomplete_let = after_incomplete_let(it.clone()).is_some(); @@ -1316,6 +1321,7 @@ fn classify_name_ref<'db>( after_amp, is_func_update, innermost_ret_ty, + innermost_breakable_ty, self_param, in_value, incomplete_let, @@ -1865,24 +1871,22 @@ fn is_in_token_of_for_loop(path: &ast::Path) -> bool { .unwrap_or(false) } -fn is_in_breakable(node: &SyntaxNode) -> BreakableKind { +fn is_in_breakable(node: &SyntaxNode) -> Option<(BreakableKind, SyntaxNode)> { node.ancestors() .take_while(|it| it.kind() != SyntaxKind::FN && it.kind() != SyntaxKind::CLOSURE_EXPR) .find_map(|it| { let (breakable, loop_body) = match_ast! { match it { - ast::ForExpr(it) => (BreakableKind::For, it.loop_body()), - ast::WhileExpr(it) => (BreakableKind::While, it.loop_body()), - ast::LoopExpr(it) => (BreakableKind::Loop, it.loop_body()), - ast::BlockExpr(it) => return it.label().map(|_| BreakableKind::Block), + ast::ForExpr(it) => (BreakableKind::For, it.loop_body()?), + ast::WhileExpr(it) => (BreakableKind::While, it.loop_body()?), + ast::LoopExpr(it) => (BreakableKind::Loop, it.loop_body()?), + ast::BlockExpr(it) => return it.label().map(|_| (BreakableKind::Block, it.syntax().clone())), _ => return None, } }; - loop_body - .filter(|it| it.syntax().text_range().contains_range(node.text_range())) - .map(|_| breakable) + loop_body.syntax().text_range().contains_range(node.text_range()) + .then_some((breakable, it)) }) - .unwrap_or(BreakableKind::None) } fn is_in_block(node: &SyntaxNode) -> bool { diff --git a/crates/ide-completion/src/tests/expression.rs b/crates/ide-completion/src/tests/expression.rs index 98a6f95f334a..5363a68af723 100644 --- a/crates/ide-completion/src/tests/expression.rs +++ b/crates/ide-completion/src/tests/expression.rs @@ -1090,6 +1090,45 @@ fn return_value_no_block() { ); } +#[test] +fn break_unit_block() { + check_edit("break", r#"fn f() { loop { break; $0 } }"#, r#"fn f() { loop { break; break; } }"#); + check_edit("break", r#"fn f() { loop { $0 } }"#, r#"fn f() { loop { break; } }"#); +} + +#[test] +fn break_unit_no_block() { + check_edit( + "break", + r#"fn f() { loop { break; match () { () => $0 } } }"#, + r#"fn f() { loop { break; match () { () => break } } }"#, + ); + + check_edit( + "break", + r#"fn f() { loop { match () { () => $0 } } }"#, + r#"fn f() { loop { match () { () => break } } }"#, + ); +} + +#[test] +fn break_value_block() { + check_edit( + "break", + r#"fn f() -> i32 { loop { $0 } }"#, + r#"fn f() -> i32 { loop { break $0; } }"#, + ); +} + +#[test] +fn break_value_no_block() { + check_edit( + "break", + r#"fn f() -> i32 { loop { match () { () => $0 } } }"#, + r#"fn f() -> i32 { loop { match () { () => break $0 } } }"#, + ); +} + #[test] fn else_completion_after_if() { check(