Improve completions in if / while expression conditions

This commit is contained in:
Lukas Wirth 2025-06-17 13:33:30 +02:00
parent 69886cfe8a
commit 646fecf315
3 changed files with 236 additions and 134 deletions

View file

@ -11,6 +11,7 @@ use ide_db::{
text_edit::TextEdit,
ty_filter::TryEnum,
};
use itertools::Either;
use stdx::never;
use syntax::{
SyntaxKind::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR},
@ -86,98 +87,10 @@ pub(crate) fn complete_postfix(
}
}
let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty.strip_references());
if let Some(try_enum) = &try_enum {
match try_enum {
TryEnum::Result => {
postfix_snippet(
"ifl",
"if let Ok {}",
&format!("if let Ok($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"lete",
"let Ok else {}",
&format!("let Ok($1) = {receiver_text} else {{\n $2\n}};\n$0"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"while",
"while let Ok {}",
&format!("while let Ok($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
}
TryEnum::Option => {
postfix_snippet(
"ifl",
"if let Some {}",
&format!("if let Some($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"lete",
"let Some else {}",
&format!("let Some($1) = {receiver_text} else {{\n $2\n}};\n$0"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"while",
"while let Some {}",
&format!("while let Some($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
}
}
} else if receiver_ty.is_bool() || receiver_ty.is_unknown() {
postfix_snippet("if", "if expr {}", &format!("if {receiver_text} {{\n $0\n}}"))
.add_to(acc, ctx.db);
postfix_snippet("while", "while expr {}", &format!("while {receiver_text} {{\n $0\n}}"))
.add_to(acc, ctx.db);
postfix_snippet("not", "!expr", &format!("!{receiver_text}")).add_to(acc, ctx.db);
} else if let Some(trait_) = ctx.famous_defs().core_iter_IntoIterator() {
if receiver_ty.impls_trait(ctx.db, trait_, &[]) {
postfix_snippet(
"for",
"for ele in expr {}",
&format!("for ele in {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
}
}
postfix_snippet("ref", "&expr", &format!("&{receiver_text}")).add_to(acc, ctx.db);
postfix_snippet("refm", "&mut expr", &format!("&mut {receiver_text}")).add_to(acc, ctx.db);
postfix_snippet("deref", "*expr", &format!("*{receiver_text}")).add_to(acc, ctx.db);
let mut block_should_be_wrapped = true;
if dot_receiver.syntax().kind() == BLOCK_EXPR {
block_should_be_wrapped = false;
if let Some(parent) = dot_receiver.syntax().parent() {
if matches!(parent.kind(), IF_EXPR | WHILE_EXPR | LOOP_EXPR | FOR_EXPR) {
block_should_be_wrapped = true;
}
}
};
let unsafe_completion_string = if block_should_be_wrapped {
format!("unsafe {{ {receiver_text} }}")
} else {
format!("unsafe {receiver_text}")
};
postfix_snippet("unsafe", "unsafe {}", &unsafe_completion_string).add_to(acc, ctx.db);
let const_completion_string = if block_should_be_wrapped {
format!("const {{ {receiver_text} }}")
} else {
format!("const {receiver_text}")
};
postfix_snippet("const", "const {}", &const_completion_string).add_to(acc, ctx.db);
// The rest of the postfix completions create an expression that moves an argument,
// so it's better to consider references now to avoid breaking the compilation
@ -195,37 +108,6 @@ pub(crate) fn complete_postfix(
add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
}
match try_enum {
Some(try_enum) => match try_enum {
TryEnum::Result => {
postfix_snippet(
"match",
"match expr {}",
&format!("match {receiver_text} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}"),
)
.add_to(acc, ctx.db);
}
TryEnum::Option => {
postfix_snippet(
"match",
"match expr {}",
&format!(
"match {receiver_text} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}"
),
)
.add_to(acc, ctx.db);
}
},
None => {
postfix_snippet(
"match",
"match expr {}",
&format!("match {receiver_text} {{\n ${{1:_}} => {{$0}},\n}}"),
)
.add_to(acc, ctx.db);
}
}
postfix_snippet("box", "Box::new(expr)", &format!("Box::new({receiver_text})"))
.add_to(acc, ctx.db);
postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({receiver_text})")).add_to(acc, ctx.db); // fixme
@ -233,15 +115,187 @@ pub(crate) fn complete_postfix(
postfix_snippet("call", "function(expr)", &format!("${{1}}({receiver_text})"))
.add_to(acc, ctx.db);
if let Some(parent) = dot_receiver_including_refs.syntax().parent().and_then(|p| p.parent()) {
if matches!(parent.kind(), STMT_LIST | EXPR_STMT) {
postfix_snippet("let", "let", &format!("let $0 = {receiver_text};"))
.add_to(acc, ctx.db);
postfix_snippet("letm", "let mut", &format!("let mut $0 = {receiver_text};"))
.add_to(acc, ctx.db);
let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty.strip_references());
let mut is_in_cond = false;
if let Some(parent) = dot_receiver_including_refs.syntax().parent() {
if let Some(second_ancestor) = parent.parent() {
let sec_ancestor_kind = second_ancestor.kind();
if let Some(expr) = <Either<ast::IfExpr, ast::WhileExpr>>::cast(second_ancestor) {
is_in_cond = match expr {
Either::Left(it) => it.condition().is_some_and(|cond| *cond.syntax() == parent),
Either::Right(it) => {
it.condition().is_some_and(|cond| *cond.syntax() == parent)
}
}
}
match &try_enum {
Some(try_enum) if is_in_cond => match try_enum {
TryEnum::Result => {
postfix_snippet(
"let",
"let Ok(_)",
&format!("let Ok($0) = {receiver_text}"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"letm",
"let Ok(mut _)",
&format!("let Ok(mut $0) = {receiver_text}"),
)
.add_to(acc, ctx.db);
}
TryEnum::Option => {
postfix_snippet(
"let",
"let Some(_)",
&format!("let Some($0) = {receiver_text}"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"letm",
"let Some(mut _)",
&format!("let Some(mut $0) = {receiver_text}"),
)
.add_to(acc, ctx.db);
}
},
_ if matches!(sec_ancestor_kind, STMT_LIST | EXPR_STMT) => {
postfix_snippet("let", "let", &format!("let $0 = {receiver_text};"))
.add_to(acc, ctx.db);
postfix_snippet("letm", "let mut", &format!("let mut $0 = {receiver_text};"))
.add_to(acc, ctx.db);
}
_ => (),
}
}
}
if !is_in_cond {
match try_enum {
Some(try_enum) => match try_enum {
TryEnum::Result => {
postfix_snippet(
"match",
"match expr {}",
&format!("match {receiver_text} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}"),
)
.add_to(acc, ctx.db);
}
TryEnum::Option => {
postfix_snippet(
"match",
"match expr {}",
&format!(
"match {receiver_text} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}"
),
)
.add_to(acc, ctx.db);
}
},
None => {
postfix_snippet(
"match",
"match expr {}",
&format!("match {receiver_text} {{\n ${{1:_}} => {{$0}},\n}}"),
)
.add_to(acc, ctx.db);
}
}
if let Some(try_enum) = &try_enum {
match try_enum {
TryEnum::Result => {
postfix_snippet(
"ifl",
"if let Ok {}",
&format!("if let Ok($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"lete",
"let Ok else {}",
&format!("let Ok($1) = {receiver_text} else {{\n $2\n}};\n$0"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"while",
"while let Ok {}",
&format!("while let Ok($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
}
TryEnum::Option => {
postfix_snippet(
"ifl",
"if let Some {}",
&format!("if let Some($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"lete",
"let Some else {}",
&format!("let Some($1) = {receiver_text} else {{\n $2\n}};\n$0"),
)
.add_to(acc, ctx.db);
postfix_snippet(
"while",
"while let Some {}",
&format!("while let Some($1) = {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
}
}
} else if receiver_ty.is_bool() || receiver_ty.is_unknown() {
postfix_snippet("if", "if expr {}", &format!("if {receiver_text} {{\n $0\n}}"))
.add_to(acc, ctx.db);
postfix_snippet(
"while",
"while expr {}",
&format!("while {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
postfix_snippet("not", "!expr", &format!("!{receiver_text}")).add_to(acc, ctx.db);
} else if let Some(trait_) = ctx.famous_defs().core_iter_IntoIterator() {
if receiver_ty.impls_trait(ctx.db, trait_, &[]) {
postfix_snippet(
"for",
"for ele in expr {}",
&format!("for ele in {receiver_text} {{\n $0\n}}"),
)
.add_to(acc, ctx.db);
}
}
}
let mut block_should_be_wrapped = true;
if dot_receiver.syntax().kind() == BLOCK_EXPR {
block_should_be_wrapped = false;
if let Some(parent) = dot_receiver.syntax().parent() {
if matches!(parent.kind(), IF_EXPR | WHILE_EXPR | LOOP_EXPR | FOR_EXPR) {
block_should_be_wrapped = true;
}
}
};
{
let (open_brace, close_brace) =
if block_should_be_wrapped { ("{ ", " }") } else { ("", "") };
let (open_paren, close_paren) = if is_in_cond { ("(", ")") } else { ("", "") };
let unsafe_completion_string = format!(
"{}unsafe {}{receiver_text}{}{}",
open_paren, open_brace, close_brace, close_paren
);
postfix_snippet("unsafe", "unsafe {}", &unsafe_completion_string).add_to(acc, ctx.db);
let const_completion_string = format!(
"{}const {}{receiver_text}{}{}",
open_paren, open_brace, close_brace, close_paren
);
postfix_snippet("const", "const {}", &const_completion_string).add_to(acc, ctx.db);
}
if let ast::Expr::Literal(literal) = dot_receiver_including_refs.clone() {
if let Some(literal_text) = ast::String::cast(literal.token()) {
add_format_like_completions(acc, ctx, &dot_receiver_including_refs, cap, &literal_text);
@ -567,6 +621,54 @@ fn main() {
);
}
#[test]
fn option_iflet_cond() {
check(
r#"
//- minicore: option
fn main() {
let bar = Some(true);
if bar.$0
}
"#,
expect![[r#"
me and() fn(self, Option<U>) -> Option<U>
me as_ref() const fn(&self) -> Option<&T>
me ok_or() const fn(self, E) -> Result<T, E>
me unwrap() const fn(self) -> T
me unwrap_or() fn(self, T) -> T
sn box Box::new(expr)
sn call function(expr)
sn const const {}
sn dbg dbg!(expr)
sn dbgr dbg!(&expr)
sn deref *expr
sn let let Some(_)
sn letm let Some(mut _)
sn ref &expr
sn refm &mut expr
sn return return expr
sn unsafe unsafe {}
"#]],
);
check_edit(
"let",
r#"
//- minicore: option
fn main() {
let bar = Some(true);
if bar.$0
}
"#,
r#"
fn main() {
let bar = Some(true);
if let Some($0) = bar
}
"#,
);
}
#[test]
fn option_letelse() {
check_edit(

View file

@ -3000,18 +3000,18 @@ fn main() {
expect![[r#"
sn not !expr [snippet]
me not() fn(self) -> <Self as Not>::Output [type_could_unify+requires_import]
sn if if expr {} []
sn while while expr {} []
sn ref &expr []
sn refm &mut expr []
sn deref *expr []
sn unsafe unsafe {} []
sn const const {} []
sn match match expr {} []
sn box Box::new(expr) []
sn dbg dbg!(expr) []
sn dbgr dbg!(&expr) []
sn call function(expr) []
sn match match expr {} []
sn if if expr {} []
sn while while expr {} []
sn unsafe unsafe {} []
sn const const {} []
sn return return expr []
"#]],
);
@ -3036,15 +3036,15 @@ fn main() {
sn ref &expr []
sn refm &mut expr []
sn deref *expr []
sn unsafe unsafe {} []
sn const const {} []
sn match match expr {} []
sn box Box::new(expr) []
sn dbg dbg!(expr) []
sn dbgr dbg!(&expr) []
sn call function(expr) []
sn let let []
sn letm let mut []
sn match match expr {} []
sn unsafe unsafe {} []
sn const const {} []
sn return return expr []
"#]],
);

View file

@ -10,7 +10,7 @@ use syntax::ast::{self, Pat, make};
use crate::RootDatabase;
/// Enum types that implement `std::ops::Try` trait.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum TryEnum {
Result,
Option,