|
1 | 1 | use ide_db::assists::AssistId; |
2 | 2 | use itertools::Itertools; |
3 | 3 | use syntax::{ |
4 | | - AstNode, T, |
| 4 | + AstNode, SyntaxElement, |
| 5 | + SyntaxKind::WHITESPACE, |
| 6 | + T, |
5 | 7 | algo::previous_non_trivia_token, |
6 | 8 | ast::{ |
7 | 9 | self, HasArgList, HasLoopBody, HasName, RangeItem, edit::AstNodeEdit, make, |
8 | 10 | syntax_factory::SyntaxFactory, |
9 | 11 | }, |
10 | | - syntax_editor::{Element, Position}, |
| 12 | + syntax_editor::{Element, Position, SyntaxEditor}, |
11 | 13 | }; |
12 | 14 |
|
13 | 15 | use crate::assist_context::{AssistContext, Assists}; |
@@ -40,8 +42,8 @@ pub(crate) fn convert_range_for_to_while(acc: &mut Assists, ctx: &AssistContext< |
40 | 42 | let iterable = for_.iterable()?; |
41 | 43 | let (start, end, step, inclusive) = extract_range(&iterable)?; |
42 | 44 | let name = pat.name()?; |
43 | | - let body = for_.loop_body()?; |
44 | | - let last = previous_non_trivia_token(body.stmt_list()?.r_curly_token()?)?; |
| 45 | + let body = for_.loop_body()?.stmt_list()?; |
| 46 | + let label = for_.label(); |
45 | 47 |
|
46 | 48 | let description = if end.is_some() { |
47 | 49 | "Replace with while expression" |
@@ -90,8 +92,10 @@ pub(crate) fn convert_range_for_to_while(acc: &mut Assists, ctx: &AssistContext< |
90 | 92 | ); |
91 | 93 |
|
92 | 94 | let op = ast::BinaryOp::Assignment { op: Some(ast::ArithOp::Add) }; |
93 | | - edit.insert_all( |
94 | | - Position::after(last), |
| 95 | + process_loop_body( |
| 96 | + body, |
| 97 | + label, |
| 98 | + &mut edit, |
95 | 99 | vec![ |
96 | 100 | make.whitespace(&format!("\n{}", indent + 1)).syntax_element(), |
97 | 101 | make.expr_bin(var_expr, op, step).syntax().syntax_element(), |
@@ -121,6 +125,86 @@ fn extract_range(iterable: &ast::Expr) -> Option<(ast::Expr, Option<ast::Expr>, |
121 | 125 | }) |
122 | 126 | } |
123 | 127 |
|
| 128 | +fn process_loop_body( |
| 129 | + body: ast::StmtList, |
| 130 | + label: Option<ast::Label>, |
| 131 | + edit: &mut SyntaxEditor, |
| 132 | + incrementer: Vec<SyntaxElement>, |
| 133 | +) -> Option<()> { |
| 134 | + let last = previous_non_trivia_token(body.r_curly_token()?)?.syntax_element(); |
| 135 | + |
| 136 | + let new_body = body.indent(1.into()).clone_subtree(); |
| 137 | + let mut continues = vec![]; |
| 138 | + collect_continue_to( |
| 139 | + &mut continues, |
| 140 | + &label.and_then(|it| it.lifetime()), |
| 141 | + new_body.syntax(), |
| 142 | + false, |
| 143 | + ); |
| 144 | + |
| 145 | + if continues.is_empty() { |
| 146 | + edit.insert_all(Position::after(last), incrementer); |
| 147 | + return Some(()); |
| 148 | + } |
| 149 | + |
| 150 | + let mut children = body |
| 151 | + .syntax() |
| 152 | + .children_with_tokens() |
| 153 | + .filter(|it| !matches!(it.kind(), WHITESPACE | T!['{'] | T!['}'])); |
| 154 | + let first = children.next()?; |
| 155 | + let block_content = first.clone()..=children.last().unwrap_or(first); |
| 156 | + |
| 157 | + let continue_label = make::lifetime("'cont"); |
| 158 | + let break_expr = make::expr_break(Some(continue_label.clone()), None).clone_for_update(); |
| 159 | + let mut new_edit = SyntaxEditor::new(new_body.syntax().clone()); |
| 160 | + for continue_expr in &continues { |
| 161 | + new_edit.replace(continue_expr.syntax(), break_expr.syntax()); |
| 162 | + } |
| 163 | + let new_body = new_edit.finish().new_root().clone(); |
| 164 | + let elements = itertools::chain( |
| 165 | + [ |
| 166 | + continue_label.syntax().clone_for_update().syntax_element(), |
| 167 | + make::token(T![:]).syntax_element(), |
| 168 | + make::tokens::single_space().syntax_element(), |
| 169 | + new_body.syntax_element(), |
| 170 | + ], |
| 171 | + incrementer, |
| 172 | + ); |
| 173 | + edit.replace_all(block_content, elements.collect()); |
| 174 | + |
| 175 | + Some(()) |
| 176 | +} |
| 177 | + |
| 178 | +fn collect_continue_to( |
| 179 | + acc: &mut Vec<ast::ContinueExpr>, |
| 180 | + label: &Option<ast::Lifetime>, |
| 181 | + node: &syntax::SyntaxNode, |
| 182 | + only_label: bool, |
| 183 | +) { |
| 184 | + let match_label = |it: &Option<ast::Lifetime>, label: &Option<ast::Lifetime>| match (it, label) |
| 185 | + { |
| 186 | + (None, _) => !only_label, |
| 187 | + (Some(a), Some(b)) if a.text() == b.text() => true, |
| 188 | + _ => false, |
| 189 | + }; |
| 190 | + if let Some(expr) = ast::ContinueExpr::cast(node.clone()) |
| 191 | + && match_label(&expr.lifetime(), label) |
| 192 | + { |
| 193 | + acc.push(expr); |
| 194 | + } else if let Some(any_loop) = ast::AnyHasLoopBody::cast(node.clone()) { |
| 195 | + if match_label(label, &any_loop.label().and_then(|it| it.lifetime())) { |
| 196 | + return; |
| 197 | + } |
| 198 | + for children in node.children() { |
| 199 | + collect_continue_to(acc, label, &children, true); |
| 200 | + } |
| 201 | + } else { |
| 202 | + for children in node.children() { |
| 203 | + collect_continue_to(acc, label, &children, only_label); |
| 204 | + } |
| 205 | + } |
| 206 | +} |
| 207 | + |
124 | 208 | #[cfg(test)] |
125 | 209 | mod tests { |
126 | 210 | use crate::tests::{check_assist, check_assist_not_applicable}; |
@@ -219,6 +303,67 @@ fn foo() { |
219 | 303 | ); |
220 | 304 | } |
221 | 305 |
|
| 306 | + #[test] |
| 307 | + fn test_convert_range_for_to_while_with_continue() { |
| 308 | + check_assist( |
| 309 | + convert_range_for_to_while, |
| 310 | + " |
| 311 | +fn foo() { |
| 312 | + $0for mut i in 3..7 { |
| 313 | + foo(i); |
| 314 | + continue; |
| 315 | + loop { break; continue } |
| 316 | + bar(i); |
| 317 | + } |
| 318 | +} |
| 319 | + ", |
| 320 | + " |
| 321 | +fn foo() { |
| 322 | + let mut i = 3; |
| 323 | + while i < 7 { |
| 324 | + 'cont: { |
| 325 | + foo(i); |
| 326 | + break 'cont; |
| 327 | + loop { break; continue } |
| 328 | + bar(i); |
| 329 | + } |
| 330 | + i += 1; |
| 331 | + } |
| 332 | +} |
| 333 | + ", |
| 334 | + ); |
| 335 | + |
| 336 | + check_assist( |
| 337 | + convert_range_for_to_while, |
| 338 | + " |
| 339 | +fn foo() { |
| 340 | + 'x: $0for mut i in 3..7 { |
| 341 | + foo(i); |
| 342 | + continue 'x; |
| 343 | + loop { break; continue 'x } |
| 344 | + 'x: loop { continue 'x } |
| 345 | + bar(i); |
| 346 | + } |
| 347 | +} |
| 348 | + ", |
| 349 | + " |
| 350 | +fn foo() { |
| 351 | + let mut i = 3; |
| 352 | + 'x: while i < 7 { |
| 353 | + 'cont: { |
| 354 | + foo(i); |
| 355 | + break 'cont; |
| 356 | + loop { break; break 'cont } |
| 357 | + 'x: loop { continue 'x } |
| 358 | + bar(i); |
| 359 | + } |
| 360 | + i += 1; |
| 361 | + } |
| 362 | +} |
| 363 | + ", |
| 364 | + ); |
| 365 | + } |
| 366 | + |
222 | 367 | #[test] |
223 | 368 | fn test_convert_range_for_to_while_step_by() { |
224 | 369 | check_assist( |
|
0 commit comments