use ide_db::{
source_change::SourceChangeBuilder, syntax_helpers::node_ext::for_each_break_and_continue_expr,
};
use syntax::{
SyntaxToken, T,
ast::{self, AstNode, HasLoopBody, syntax_factory::SyntaxFactory},
syntax_editor::{Position, SyntaxEditor},
};
use crate::{AssistContext, AssistId, Assists};
pub(crate) fn add_label_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let loop_expr = ctx.find_node_at_offset::<ast::AnyHasLoopBody>()?;
let loop_kw = loop_token(&loop_expr)?;
if loop_expr.label().is_some() || !loop_kw.text_range().contains_inclusive(ctx.offset()) {
return None;
}
acc.add(
AssistId::generate("add_label_to_loop"),
"Add Label",
loop_expr.syntax().text_range(),
|builder| {
let make = SyntaxFactory::with_mappings();
let mut editor = builder.make_editor(loop_expr.syntax());
let label = make.lifetime("'l");
let elements = vec![
label.syntax().clone().into(),
make.token(T![:]).into(),
make.whitespace(" ").into(),
];
editor.insert_all(Position::before(&loop_kw), elements);
if let Some(cap) = ctx.config.snippet_cap {
editor.add_annotation(label.syntax(), builder.make_placeholder_snippet(cap));
}
let loop_body = loop_expr.loop_body().and_then(|it| it.stmt_list());
for_each_break_and_continue_expr(loop_expr.label(), loop_body, &mut |expr| {
let token = match expr {
ast::Expr::BreakExpr(break_expr) => break_expr.break_token(),
ast::Expr::ContinueExpr(continue_expr) => continue_expr.continue_token(),
_ => return,
};
if let Some(token) = token {
insert_label_after_token(&mut editor, &make, &token, ctx, builder);
}
});
editor.add_mappings(make.finish_with_mappings());
builder.add_file_edits(ctx.vfs_file_id(), editor);
builder.rename();
},
)
}
fn loop_token(loop_expr: &ast::AnyHasLoopBody) -> Option<syntax::SyntaxToken> {
loop_expr
.syntax()
.children_with_tokens()
.filter_map(|it| it.into_token())
.find(|it| matches!(it.kind(), T![for] | T![loop] | T![while]))
}
fn insert_label_after_token(
editor: &mut SyntaxEditor,
make: &SyntaxFactory,
token: &SyntaxToken,
ctx: &AssistContext<'_>,
builder: &mut SourceChangeBuilder,
) {
let label = make.lifetime("'l");
let elements = vec![make.whitespace(" ").into(), label.syntax().clone().into()];
editor.insert_all(Position::after(token), elements);
if let Some(cap) = ctx.config.snippet_cap {
editor.add_annotation(label.syntax(), builder.make_placeholder_snippet(cap));
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn add_label() {
check_assist(
add_label_to_loop,
r#"
fn main() {
loop$0 {
break;
continue;
}
}"#,
r#"
fn main() {
${1:'l}: loop {
break ${2:'l};
continue ${0:'l};
}
}"#,
);
}
#[test]
fn add_label_to_while_expr() {
check_assist(
add_label_to_loop,
r#"
fn main() {
while$0 true {
break;
continue;
}
}"#,
r#"
fn main() {
${1:'l}: while true {
break ${2:'l};
continue ${0:'l};
}
}"#,
);
}
#[test]
fn add_label_to_for_expr() {
check_assist(
add_label_to_loop,
r#"
fn main() {
for$0 _ in 0..5 {
break;
continue;
}
}"#,
r#"
fn main() {
${1:'l}: for _ in 0..5 {
break ${2:'l};
continue ${0:'l};
}
}"#,
);
}
#[test]
fn add_label_to_outer_loop() {
check_assist(
add_label_to_loop,
r#"
fn main() {
loop$0 {
break;
continue;
loop {
break;
continue;
}
}
}"#,
r#"
fn main() {
${1:'l}: loop {
break ${2:'l};
continue ${0:'l};
loop {
break;
continue;
}
}
}"#,
);
}
#[test]
fn add_label_to_inner_loop() {
check_assist(
add_label_to_loop,
r#"
fn main() {
loop {
break;
continue;
loop$0 {
break;
continue;
}
}
}"#,
r#"
fn main() {
loop {
break;
continue;
${1:'l}: loop {
break ${2:'l};
continue ${0:'l};
}
}
}"#,
);
}
#[test]
fn do_not_add_label_if_exists() {
check_assist_not_applicable(
add_label_to_loop,
r#"
fn main() {
'l: loop$0 {
break 'l;
continue 'l;
}
}"#,
);
}
#[test]
fn do_not_add_label_if_outside_keyword() {
check_assist_not_applicable(
add_label_to_loop,
r#"
fn main() {
'l: loop {$0
break 'l;
continue 'l;
}
}"#,
);
check_assist_not_applicable(
add_label_to_loop,
r#"
fn main() {
'l: while true {$0
break 'l;
continue 'l;
}
}"#,
);
}
}