Skip to content

Commit 63573d4

Browse files
committed
Auto merge of #11971 - jonas-schievink:on-type-fmt-assignments, r=jonas-schievink
feat: Add trailing `;` when typing `=` in assignment ![Peek 2022-04-12 19-41](https://user-images.githubusercontent.com/1786438/163022079-1ed114ef-7c75-490f-a8ed-731a13f0b44d.gif) This does have a false positive to keep in mind, it will add a trailing `;` in the following snippet too, which is probably not desired: ```rust fn is_zero(i: i32) -> bool { i $0 0 } ``` However, that function is unlikely to be written from the "inside out" like that, so it might be acceptable. Typically `=` is only inserted last when the author realizes that an existing expression should be assigned to some variable.
2 parents 1b15638 + f96fd40 commit 63573d4

File tree

1 file changed

+183
-9
lines changed

1 file changed

+183
-9
lines changed

crates/ide/src/typing.rs

Lines changed: 183 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>{";
3939
// Some features trigger on typing certain characters:
4040
//
4141
// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
42+
// - typing `=` between two expressions adds `;` when in statement position
43+
// - typing `=` to turn an assignment into an equality comparison removes `;` when in expression position
4244
// - typing `.` in a chain method call auto-indents
4345
// - typing `{` in front of an expression inserts a closing `}` after the expression
46+
// - typing `{` in a use item adds a closing `}` in the right place
4447
//
4548
// VS Code::
4649
//
@@ -166,23 +169,83 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
166169
if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
167170
return None;
168171
}
169-
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
170-
if let_stmt.semicolon_token().is_some() {
171-
return None;
172+
173+
if let Some(edit) = let_stmt(file, offset) {
174+
return Some(edit);
175+
}
176+
if let Some(edit) = assign_expr(file, offset) {
177+
return Some(edit);
178+
}
179+
if let Some(edit) = assign_to_eq(file, offset) {
180+
return Some(edit);
172181
}
173-
if let Some(expr) = let_stmt.initializer() {
182+
183+
return None;
184+
185+
fn assign_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
186+
let binop: ast::BinExpr = find_node_at_offset(file.syntax(), offset)?;
187+
if !matches!(binop.op_kind(), Some(ast::BinaryOp::Assignment { op: None })) {
188+
return None;
189+
}
190+
191+
// Parent must be `ExprStmt` or `StmtList` for `;` to be valid.
192+
if let Some(expr_stmt) = ast::ExprStmt::cast(binop.syntax().parent()?) {
193+
if expr_stmt.semicolon_token().is_some() {
194+
return None;
195+
}
196+
} else {
197+
if !ast::StmtList::can_cast(binop.syntax().parent()?.kind()) {
198+
return None;
199+
}
200+
}
201+
202+
let expr = binop.rhs()?;
174203
let expr_range = expr.syntax().text_range();
175204
if expr_range.contains(offset) && offset != expr_range.start() {
176205
return None;
177206
}
178207
if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
179208
return None;
180209
}
181-
} else {
182-
return None;
210+
let offset = expr.syntax().text_range().end();
211+
Some(TextEdit::insert(offset, ";".to_string()))
212+
}
213+
214+
/// `a =$0 b;` removes the semicolon if an expression is valid in this context.
215+
fn assign_to_eq(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
216+
let binop: ast::BinExpr = find_node_at_offset(file.syntax(), offset)?;
217+
if !matches!(binop.op_kind(), Some(ast::BinaryOp::CmpOp(ast::CmpOp::Eq { negated: false })))
218+
{
219+
return None;
220+
}
221+
222+
let expr_stmt = ast::ExprStmt::cast(binop.syntax().parent()?)?;
223+
let semi = expr_stmt.semicolon_token()?;
224+
225+
if expr_stmt.syntax().next_sibling().is_some() {
226+
// Not the last statement in the list.
227+
return None;
228+
}
229+
230+
Some(TextEdit::delete(semi.text_range()))
231+
}
232+
233+
fn let_stmt(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
234+
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
235+
if let_stmt.semicolon_token().is_some() {
236+
return None;
237+
}
238+
let expr = let_stmt.initializer()?;
239+
let expr_range = expr.syntax().text_range();
240+
if expr_range.contains(offset) && offset != expr_range.start() {
241+
return None;
242+
}
243+
if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
244+
return None;
245+
}
246+
let offset = let_stmt.syntax().text_range().end();
247+
Some(TextEdit::insert(offset, ";".to_string()))
183248
}
184-
let offset = let_stmt.syntax().text_range().end();
185-
Some(TextEdit::insert(offset, ";".to_string()))
186249
}
187250

188251
/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
@@ -286,7 +349,7 @@ mod tests {
286349
}
287350

288351
#[test]
289-
fn test_on_eq_typed() {
352+
fn test_semi_after_let() {
290353
// do_check(r"
291354
// fn foo() {
292355
// let foo =$0
@@ -322,6 +385,117 @@ fn foo() {
322385
// ");
323386
}
324387

388+
#[test]
389+
fn test_semi_after_assign() {
390+
type_char(
391+
'=',
392+
r#"
393+
fn f() {
394+
i $0 0
395+
}
396+
"#,
397+
r#"
398+
fn f() {
399+
i = 0;
400+
}
401+
"#,
402+
);
403+
type_char(
404+
'=',
405+
r#"
406+
fn f() {
407+
i $0 0
408+
i
409+
}
410+
"#,
411+
r#"
412+
fn f() {
413+
i = 0;
414+
i
415+
}
416+
"#,
417+
);
418+
type_char_noop(
419+
'=',
420+
r#"
421+
fn f(x: u8) {
422+
if x $0
423+
}
424+
"#,
425+
);
426+
type_char_noop(
427+
'=',
428+
r#"
429+
fn f(x: u8) {
430+
if x $0 {}
431+
}
432+
"#,
433+
);
434+
type_char_noop(
435+
'=',
436+
r#"
437+
fn f(x: u8) {
438+
if x $0 0 {}
439+
}
440+
"#,
441+
);
442+
type_char_noop(
443+
'=',
444+
r#"
445+
fn f() {
446+
g(i $0 0);
447+
}
448+
"#,
449+
);
450+
}
451+
452+
#[test]
453+
fn assign_to_eq() {
454+
type_char(
455+
'=',
456+
r#"
457+
fn f(a: u8) {
458+
a =$0 0;
459+
}
460+
"#,
461+
r#"
462+
fn f(a: u8) {
463+
a == 0
464+
}
465+
"#,
466+
);
467+
type_char(
468+
'=',
469+
r#"
470+
fn f(a: u8) {
471+
a $0= 0;
472+
}
473+
"#,
474+
r#"
475+
fn f(a: u8) {
476+
a == 0
477+
}
478+
"#,
479+
);
480+
type_char_noop(
481+
'=',
482+
r#"
483+
fn f(a: u8) {
484+
let e = a =$0 0;
485+
}
486+
"#,
487+
);
488+
type_char_noop(
489+
'=',
490+
r#"
491+
fn f(a: u8) {
492+
let e = a =$0 0;
493+
e
494+
}
495+
"#,
496+
);
497+
}
498+
325499
#[test]
326500
fn indents_new_chain_call() {
327501
type_char(

0 commit comments

Comments
 (0)