use ide_db::{
source_change::SourceChangeBuilder, syntax_helpers::node_ext::for_each_break_and_continue_expr,
};
use syntax::{
SyntaxToken, T,
ast::{
self, AstNode, HasLoopBody,
make::{self, tokens},
syntax_factory::SyntaxFactory,
},
syntax_editor::{Position, SyntaxEditor},
};
use crate::{AssistContext, AssistId, Assists};
// Assist: add_label_to_loop
//
// Adds a label to a loop.
//
// ```
// fn main() {
// loop$0 {
// break;
// continue;
// }
// }
// ```
// ->
// ```
// fn main() {
// ${1:'l}: loop {
// break ${2:'l};
// continue ${0:'l};
// }
// }
// ```
pub(crate) fn add_label_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let loop_kw = ctx.find_token_syntax_at_offset(T![loop])?;
let loop_expr = loop_kw.parent().and_then(ast::LoopExpr::cast)?;
if loop_expr.label().is_some() {
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(),
tokens::single_space().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 insert_label_after_token(
editor: &mut SyntaxEditor,
make: &SyntaxFactory,
token: &SyntaxToken,
ctx: &AssistContext<'_>,
builder: &mut SourceChangeBuilder,
) {
let label = make.lifetime("'l");
let elements = vec![tokens::single_space().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_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;
}
}"#,
);
}
}