Rollup merge of #154241 - lnicola:sync-from-ra, r=lnicola

`rust-analyzer` subtree update

Subtree update of `rust-analyzer` to https://github.com/rust-lang/rust-analyzer/commit/b42b63f390a4dab14e6efa34a70e67f5b087cc62.

Created using https://github.com/rust-lang/josh-sync.

r? @ghost
This commit is contained in:
Jonathan Brouwer
2026-03-23 12:00:56 +01:00
committed by GitHub
73 changed files with 2813 additions and 810 deletions
+2
View File
@@ -20,6 +20,8 @@ analyzing Rust code. See
[Architecture](https://rust-analyzer.github.io/book/contributing/architecture.html)
in the manual.
[![codecov](https://codecov.io/github/rust-lang/rust-analyzer/graph/badge.svg)](https://app.codecov.io/github/rust-lang/rust-analyzer/tree/master)
## Quick Start
https://rust-analyzer.github.io/book/installation.html
@@ -1,7 +1,5 @@
//! Things related to IR printing in the next-trait-solver.
use std::any::type_name_of_val;
use rustc_type_ir::{self as ty, ir_print::IrPrint};
use super::SolverDefId;
@@ -82,7 +80,10 @@ fn print_debug(
t: &ty::TraitPredicate<Self>,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
match t.polarity {
ty::PredicatePolarity::Positive => write!(fmt, "{:?}", t.trait_ref),
ty::PredicatePolarity::Negative => write!(fmt, "!{:?}", t.trait_ref),
}
}
}
impl<'db> IrPrint<rustc_type_ir::HostEffectPredicate<Self>> for DbInterner<'db> {
@@ -97,7 +98,11 @@ fn print_debug(
t: &rustc_type_ir::HostEffectPredicate<Self>,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
let prefix = match t.constness {
ty::BoundConstness::Const => "const",
ty::BoundConstness::Maybe => "[const]",
};
write!(fmt, "{prefix} {:?}", t.trait_ref)
}
}
impl<'db> IrPrint<ty::ExistentialTraitRef<Self>> for DbInterner<'db> {
@@ -183,7 +188,7 @@ fn print_debug(
t: &ty::NormalizesTo<Self>,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
write!(fmt, "NormalizesTo({} -> {:?})", t.alias, t.term)
}
}
impl<'db> IrPrint<ty::SubtypePredicate<Self>> for DbInterner<'db> {
@@ -198,7 +203,7 @@ fn print_debug(
t: &ty::SubtypePredicate<Self>,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
write!(fmt, "{:?} <: {:?}", t.a, t.b)
}
}
impl<'db> IrPrint<ty::CoercePredicate<Self>> for DbInterner<'db> {
@@ -210,7 +215,7 @@ fn print_debug(
t: &ty::CoercePredicate<Self>,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
write!(fmt, "CoercePredicate({:?} -> {:?})", t.a, t.b)
}
}
impl<'db> IrPrint<ty::FnSig<Self>> for DbInterner<'db> {
@@ -219,7 +224,9 @@ fn print(t: &ty::FnSig<Self>, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Re
}
fn print_debug(t: &ty::FnSig<Self>, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
let tys = t.inputs_and_output.as_slice();
let (output, inputs) = tys.split_last().unwrap();
write!(fmt, "fn({:?}) -> {:?}", inputs, output)
}
}
@@ -235,6 +242,10 @@ fn print_debug(
t: &rustc_type_ir::PatternKind<DbInterner<'db>>,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
fmt.write_str(&format!("TODO: {:?}", type_name_of_val(t)))
match t {
ty::PatternKind::Range { start, end } => write!(fmt, "{:?}..={:?}", start, end),
ty::PatternKind::Or(list) => write!(fmt, "or({:?})", list),
ty::PatternKind::NotNull => fmt.write_str("!null"),
}
}
}
@@ -51,20 +51,6 @@ fn from(d: $diag $(<$lt>)?) -> $AnyDiagnostic<$db> {
)*
};
}
// FIXME Accept something like the following in the macro call instead
// diagnostics![
// pub struct BreakOutsideOfLoop {
// pub expr: InFile<AstPtr<ast::Expr>>,
// pub is_break: bool,
// pub bad_value_break: bool,
// }, ...
// or more concisely
// BreakOutsideOfLoop {
// expr: InFile<AstPtr<ast::Expr>>,
// is_break: bool,
// bad_value_break: bool,
// }, ...
// ]
diagnostics![AnyDiagnostic<'db> ->
AwaitOutsideOfAsync,
@@ -84,6 +84,7 @@ fn get_replacement_node(ctx: &AssistContext<'_>) -> Option<(ParentType, ast::Exp
match parent {
ast::LetStmt(it) => it.initializer()?,
ast::LetExpr(it) => it.expr()?,
ast::BinExpr(it) => it.rhs()?,
ast::Static(it) => it.body()?,
ast::Const(it) => it.body()?,
_ => return None,
@@ -174,6 +175,70 @@ fn foo() {
n + 100
};
}
"#,
);
check_assist(
add_braces,
r#"
fn foo() {
let x;
x =$0 n + 100;
}
"#,
r#"
fn foo() {
let x;
x = {
n + 100
};
}
"#,
);
check_assist(
add_braces,
r#"
fn foo() {
if let x =$0 n + 100 {}
}
"#,
r#"
fn foo() {
if let x = {
n + 100
} {}
}
"#,
);
}
#[test]
fn suggest_add_braces_for_const_initializer() {
check_assist(
add_braces,
r#"
const X: i32 =$0 1 + 2;
"#,
r#"
const X: i32 = {
1 + 2
};
"#,
);
}
#[test]
fn suggest_add_braces_for_static_initializer() {
check_assist(
add_braces,
r#"
static X: i32 $0= 1 + 2;
"#,
r#"
static X: i32 = {
1 + 2
};
"#,
);
}
@@ -3,11 +3,7 @@
};
use syntax::{
SyntaxToken, T,
ast::{
self, AstNode, HasLoopBody,
make::{self, tokens},
syntax_factory::SyntaxFactory,
},
ast::{self, AstNode, HasLoopBody, syntax_factory::SyntaxFactory},
syntax_editor::{Position, SyntaxEditor},
};
@@ -35,9 +31,9 @@
// }
// ```
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() {
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;
}
@@ -52,8 +48,8 @@ pub(crate) fn add_label_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
let label = make.lifetime("'l");
let elements = vec![
label.syntax().clone().into(),
make::token(T![:]).into(),
tokens::single_space().into(),
make.token(T![:]).into(),
make.whitespace(" ").into(),
];
editor.insert_all(Position::before(&loop_kw), elements);
@@ -80,6 +76,14 @@ pub(crate) fn add_label_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
)
}
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,
@@ -88,7 +92,7 @@ fn insert_label_after_token(
builder: &mut SourceChangeBuilder,
) {
let label = make.lifetime("'l");
let elements = vec![tokens::single_space().into(), label.syntax().clone().into()];
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 {
@@ -123,6 +127,48 @@ fn main() {
);
}
#[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(
@@ -191,6 +237,31 @@ fn main() {
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;
}
}"#,
);
}
@@ -1,4 +1,7 @@
use syntax::ast::{self, AstNode, HasGenericParams, HasName};
use syntax::{
SyntaxKind, SyntaxNode, SyntaxToken,
ast::{self, AstNode, HasGenericParams, HasName},
};
use crate::{AssistContext, AssistId, Assists};
@@ -21,7 +24,7 @@
// ```
pub(crate) fn add_lifetime_to_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let ref_type_focused = ctx.find_node_at_offset::<ast::RefType>()?;
if ref_type_focused.lifetime().is_some() {
if ref_type_focused.lifetime().is_some_and(|lifetime| lifetime.text() != "'_") {
return None;
}
@@ -34,10 +37,10 @@ pub(crate) fn add_lifetime_to_type(acc: &mut Assists, ctx: &AssistContext<'_>) -
return None;
}
let ref_types = fetch_borrowed_types(&node)?;
let changes = fetch_borrowed_types(&node)?;
let target = node.syntax().text_range();
acc.add(AssistId::generate("add_lifetime_to_type"), "Add lifetime", target, |builder| {
acc.add(AssistId::quick_fix("add_lifetime_to_type"), "Add lifetime", target, |builder| {
match node.generic_param_list() {
Some(gen_param) => {
if let Some(left_angle) = gen_param.l_angle_token() {
@@ -51,16 +54,21 @@ pub(crate) fn add_lifetime_to_type(acc: &mut Assists, ctx: &AssistContext<'_>) -
}
}
for ref_type in ref_types {
if let Some(amp_token) = ref_type.amp_token() {
builder.insert(amp_token.text_range().end(), "'a ");
for change in changes {
match change {
Change::Replace(it) => {
builder.replace(it.text_range(), "'a");
}
Change::Insert(it) => {
builder.insert(it.text_range().end(), "'a ");
}
}
}
})
}
fn fetch_borrowed_types(node: &ast::Adt) -> Option<Vec<ast::RefType>> {
let ref_types: Vec<ast::RefType> = match node {
fn fetch_borrowed_types(node: &ast::Adt) -> Option<Vec<Change>> {
let ref_types: Vec<_> = match node {
ast::Adt::Enum(enum_) => {
let variant_list = enum_.variant_list()?;
variant_list
@@ -79,55 +87,50 @@ fn fetch_borrowed_types(node: &ast::Adt) -> Option<Vec<ast::RefType>> {
}
ast::Adt::Union(un) => {
let record_field_list = un.record_field_list()?;
record_field_list
.fields()
.filter_map(|r_field| {
if let ast::Type::RefType(ref_type) = r_field.ty()?
&& ref_type.lifetime().is_none()
{
return Some(ref_type);
}
None
})
.collect()
find_ref_types_from_field_list(&record_field_list.into())?
}
};
if ref_types.is_empty() { None } else { Some(ref_types) }
}
fn find_ref_types_from_field_list(field_list: &ast::FieldList) -> Option<Vec<ast::RefType>> {
let ref_types: Vec<ast::RefType> = match field_list {
ast::FieldList::RecordFieldList(record_list) => record_list
.fields()
.filter_map(|f| {
if let ast::Type::RefType(ref_type) = f.ty()?
&& ref_type.lifetime().is_none()
{
return Some(ref_type);
}
None
})
.collect(),
ast::FieldList::TupleFieldList(tuple_field_list) => tuple_field_list
.fields()
.filter_map(|f| {
if let ast::Type::RefType(ref_type) = f.ty()?
&& ref_type.lifetime().is_none()
{
return Some(ref_type);
}
None
})
.collect(),
fn find_ref_types_from_field_list(field_list: &ast::FieldList) -> Option<Vec<Change>> {
let ref_types: Vec<_> = match field_list {
ast::FieldList::RecordFieldList(record_list) => {
record_list.fields().flat_map(|f| infer_lifetimes(f.syntax())).collect()
}
ast::FieldList::TupleFieldList(tuple_field_list) => {
tuple_field_list.fields().flat_map(|f| infer_lifetimes(f.syntax())).collect()
}
};
if ref_types.is_empty() { None } else { Some(ref_types) }
}
enum Change {
Replace(SyntaxToken),
Insert(SyntaxToken),
}
fn infer_lifetimes(node: &SyntaxNode) -> Vec<Change> {
node.children()
.filter(|it| !matches!(it.kind(), SyntaxKind::FN_PTR_TYPE | SyntaxKind::TYPE_BOUND_LIST))
.flat_map(|it| {
infer_lifetimes(&it)
.into_iter()
.chain(ast::Lifetime::cast(it.clone()).and_then(|lt| {
lt.lifetime_ident_token().filter(|lt| lt.text() == "'_").map(Change::Replace)
}))
.chain(
ast::RefType::cast(it)
.filter(|ty| ty.lifetime().is_none())
.and_then(|ty| ty.amp_token())
.map(Change::Insert),
)
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
@@ -164,6 +167,24 @@ fn add_lifetime_to_struct() {
check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo { a: &'a$0 i32 }"#);
}
#[test]
fn add_lifetime_to_nested_types() {
check_assist(
add_lifetime_to_type,
r#"struct Foo { a: &$0i32, b: &(&i32, fn(&str) -> &str) }"#,
r#"struct Foo<'a> { a: &'a i32, b: &'a (&'a i32, fn(&str) -> &str) }"#,
);
}
#[test]
fn add_lifetime_to_explicit_infer_lifetime() {
check_assist(
add_lifetime_to_type,
r#"struct Foo { a: &'_ $0i32, b: &'_ (&'_ i32, fn(&str) -> &str) }"#,
r#"struct Foo<'a> { a: &'a i32, b: &'a (&'a i32, fn(&str) -> &str) }"#,
);
}
#[test]
fn add_lifetime_to_enum() {
check_assist(
@@ -245,14 +245,7 @@ pub(crate) fn add_missing_match_arms(acc: &mut Assists, ctx: &AssistContext<'_>)
.arms()
.filter(|arm| {
if matches!(arm.pat(), Some(ast::Pat::WildcardPat(_))) {
let is_empty_expr = arm.expr().is_none_or(|e| match e {
ast::Expr::BlockExpr(b) => {
b.statements().next().is_none() && b.tail_expr().is_none()
}
ast::Expr::TupleExpr(t) => t.fields().next().is_none(),
_ => false,
});
if is_empty_expr {
if arm.expr().is_none_or(is_empty_expr) {
false
} else {
cov_mark::hit!(add_missing_match_arms_empty_expr);
@@ -347,12 +340,24 @@ fn cursor_at_trivial_match_arm_list(
// $0
// }
if let Some(last_arm) = match_arm_list.arms().last() {
let last_arm_range = ctx.sema.original_range_opt(last_arm.syntax())?.range;
let last_node = match last_arm.expr() {
Some(expr) => expr.syntax().clone(),
None => last_arm.syntax().clone(),
};
let last_node_range = ctx.sema.original_range_opt(&last_node)?.range;
let match_expr_range = ctx.sema.original_range_opt(match_expr.syntax())?.range;
if last_arm_range.end() <= ctx.offset() && ctx.offset() < match_expr_range.end() {
if last_node_range.end() <= ctx.offset() && ctx.offset() < match_expr_range.end() {
cov_mark::hit!(add_missing_match_arms_end_of_last_arm);
return Some(());
}
if ast::Expr::cast(last_node.clone()).is_some_and(is_empty_expr)
&& last_node_range.contains(ctx.offset())
&& !last_node.text().contains_char('\n')
{
cov_mark::hit!(add_missing_match_arms_end_of_last_empty_arm);
return Some(());
}
}
// match { _$0 => {...} }
@@ -371,6 +376,14 @@ fn is_variant_missing(existing_pats: &[Pat], var: &Pat) -> bool {
!existing_pats.iter().any(|pat| does_pat_match_variant(pat, var))
}
fn is_empty_expr(e: ast::Expr) -> bool {
match e {
ast::Expr::BlockExpr(b) => b.statements().next().is_none() && b.tail_expr().is_none(),
ast::Expr::TupleExpr(t) => t.fields().next().is_none(),
_ => false,
}
}
// Fixme: this is still somewhat limited, use hir_ty::diagnostics::match_check?
fn does_pat_match_variant(pat: &Pat, var: &Pat) -> bool {
match (pat, var) {
@@ -1066,7 +1079,7 @@ fn main() {
#[test]
fn add_missing_match_arms_end_of_last_arm() {
cov_mark::check!(add_missing_match_arms_end_of_last_arm);
cov_mark::check_count!(add_missing_match_arms_end_of_last_arm, 2);
check_assist(
add_missing_match_arms,
r#"
@@ -1095,6 +1108,103 @@ fn main() {
(A::Two, B::Two) => ${3:todo!()},$0
}
}
"#,
);
check_assist(
add_missing_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
(A::Two, B::One) => 2$0,
}
}
"#,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
(A::Two, B::One) => 2,
(A::One, B::One) => ${1:todo!()},
(A::One, B::Two) => ${2:todo!()},
(A::Two, B::Two) => ${3:todo!()},$0
}
}
"#,
);
}
#[test]
fn add_missing_match_arms_end_of_last_empty_arm() {
cov_mark::check_count!(add_missing_match_arms_end_of_last_empty_arm, 2);
check_assist(
add_missing_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
(A::Two, B::One) => {$0}
}
}
"#,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
(A::Two, B::One) => {}
(A::One, B::One) => ${1:todo!()},
(A::One, B::Two) => ${2:todo!()},
(A::Two, B::Two) => ${3:todo!()},$0
}
}
"#,
);
check_assist(
add_missing_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
(A::Two, B::One) => ($0)
}
}
"#,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
(A::Two, B::One) => (),
(A::One, B::One) => ${1:todo!()},
(A::One, B::Two) => ${2:todo!()},
(A::Two, B::Two) => ${3:todo!()},$0
}
}
"#,
);
}
@@ -197,7 +197,7 @@ pub(crate) fn apply_demorgan_iterator(acc: &mut Assists, ctx: &AssistContext<'_>
let (name, arg_expr) = validate_method_call_expr(ctx, &method_call)?;
let ast::Expr::ClosureExpr(closure_expr) = arg_expr else { return None };
let closure_body = closure_expr.body()?.clone_for_update();
let closure_body = closure_expr.body()?;
let op_range = method_call.syntax().text_range();
let label = format!("Apply De Morgan's law to `Iterator::{}`", name.text().as_str());
@@ -533,7 +533,7 @@ fn make_bool_enum(make_pub: bool, make: &SyntaxFactory) -> ast::Enum {
],
),
));
make.enum_(
make.item_enum(
[derive_eq],
if make_pub { Some(make.visibility_pub()) } else { None },
make.name("Bool"),
@@ -220,7 +220,7 @@ pub(crate) fn convert_closure_to_fn(acc: &mut Assists, ctx: &AssistContext<'_>)
}
let body = if wrap_body_in_block {
make::block_expr([], Some(body))
make::block_expr([], Some(body.reset_indent().indent(1.into())))
} else {
ast::BlockExpr::cast(body.syntax().clone()).unwrap()
};
@@ -969,6 +969,32 @@ fn closure() {
}
closure();
}
"#,
);
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
{
let closure = |$0| match () {
() => {},
};
closure();
}
}
"#,
r#"
fn foo() {
{
fn closure() {
match () {
() => {},
}
}
closure();
}
}
"#,
);
}
@@ -2,7 +2,7 @@
use ide_db::{famous_defs::FamousDefs, syntax_helpers::suggest_name};
use syntax::{
AstNode,
ast::{self, HasAttrs, HasLoopBody, edit::IndentLevel, make, syntax_factory::SyntaxFactory},
ast::{self, HasAttrs, HasLoopBody, edit::IndentLevel, syntax_factory::SyntaxFactory},
syntax_editor::Position,
};
@@ -57,13 +57,13 @@ pub(crate) fn convert_for_loop_to_while_let(
{
(expr, Some(make.name_ref(method.as_str())))
} else if let ast::Expr::RefExpr(_) = iterable {
(make::expr_paren(iterable).into(), Some(make.name_ref("into_iter")))
(make.expr_paren(iterable).into(), Some(make.name_ref("into_iter")))
} else {
(iterable, Some(make.name_ref("into_iter")))
};
let iterable = if let Some(method) = method {
make::expr_method_call(iterable, method, make::arg_list([])).into()
make.expr_method_call(iterable, method, make.arg_list([])).into()
} else {
iterable
};
@@ -89,17 +89,18 @@ pub(crate) fn convert_for_loop_to_while_let(
for_loop.syntax(),
&mut editor,
for_loop.attrs().map(|it| it.clone_for_update()),
&make,
);
editor.insert(
Position::before(for_loop.syntax()),
make::tokens::whitespace(format!("\n{indent}").as_str()),
make.whitespace(format!("\n{indent}").as_str()),
);
editor.insert(Position::before(for_loop.syntax()), mut_expr.syntax());
let opt_pat = make.tuple_struct_pat(make::ext::ident_path("Some"), [pat]);
let opt_pat = make.tuple_struct_pat(make.ident_path("Some"), [pat]);
let iter_next_expr = make.expr_method_call(
make.expr_path(make::ext::ident_path(&tmp_var)),
make.expr_path(make.ident_path(&tmp_var)),
make.name_ref("next"),
make.arg_list([]),
);
@@ -1,6 +1,6 @@
use ide_db::{famous_defs::FamousDefs, traits::resolve_target_trait};
use syntax::ast::edit::IndentLevel;
use syntax::ast::{self, AstNode, HasGenericArgs, HasName, make};
use syntax::ast::{self, AstNode, HasGenericArgs, HasName, syntax_factory::SyntaxFactory};
use syntax::syntax_editor::{Element, Position};
use crate::{AssistContext, AssistId, Assists};
@@ -74,36 +74,25 @@ pub(crate) fn convert_from_to_tryfrom(acc: &mut Assists, ctx: &AssistContext<'_>
"Convert From to TryFrom",
impl_.syntax().text_range(),
|builder| {
let make = SyntaxFactory::with_mappings();
let mut editor = builder.make_editor(impl_.syntax());
editor.replace(
trait_ty.syntax(),
make::ty(&format!("TryFrom<{from_type}>")).syntax().clone_for_update(),
);
editor.replace(trait_ty.syntax(), make.ty(&format!("TryFrom<{from_type}>")).syntax());
editor.replace(
from_fn_return_type.syntax(),
make::ty("Result<Self, Self::Error>").syntax().clone_for_update(),
);
editor
.replace(from_fn_name.syntax(), make::name("try_from").syntax().clone_for_update());
editor.replace(
tail_expr.syntax(),
wrap_ok(tail_expr.clone()).syntax().clone_for_update(),
make.ty("Result<Self, Self::Error>").syntax(),
);
editor.replace(from_fn_name.syntax(), make.name("try_from").syntax());
editor.replace(tail_expr.syntax(), wrap_ok(&make, tail_expr.clone()).syntax());
for r in return_exprs {
let t = r.expr().unwrap_or_else(make::ext::expr_unit);
editor.replace(t.syntax(), wrap_ok(t.clone()).syntax().clone_for_update());
let t = r.expr().unwrap_or_else(|| make.expr_unit());
editor.replace(t.syntax(), wrap_ok(&make, t.clone()).syntax());
}
let error_type = ast::AssocItem::TypeAlias(make::ty_alias(
None,
"Error",
None,
None,
None,
Some((make::ty_unit(), None)),
))
.clone_for_update();
let error_type_alias =
make.ty_alias(None, "Error", None, None, None, Some((make.ty("()"), None)));
let error_type = ast::AssocItem::TypeAlias(error_type_alias);
if let Some(cap) = ctx.config.snippet_cap
&& let ast::AssocItem::TypeAlias(type_alias) = &error_type
@@ -117,22 +106,19 @@ pub(crate) fn convert_from_to_tryfrom(acc: &mut Assists, ctx: &AssistContext<'_>
editor.insert_all(
Position::after(associated_l_curly),
vec![
make::tokens::whitespace(&format!("\n{indent}")).syntax_element(),
make.whitespace(&format!("\n{indent}")).syntax_element(),
error_type.syntax().syntax_element(),
make::tokens::whitespace("\n").syntax_element(),
make.whitespace("\n").syntax_element(),
],
);
editor.add_mappings(make.finish_with_mappings());
builder.add_file_edits(ctx.vfs_file_id(), editor);
},
)
}
fn wrap_ok(expr: ast::Expr) -> ast::Expr {
make::expr_call(
make::expr_path(make::ext::ident_path("Ok")),
make::arg_list(std::iter::once(expr)),
)
.into()
fn wrap_ok(make: &SyntaxFactory, expr: ast::Expr) -> ast::Expr {
make.expr_call(make.expr_path(make.path_from_text("Ok")), make.arg_list([expr])).into()
}
#[cfg(test)]
@@ -1,14 +1,18 @@
use either::Either;
use ide_db::{defs::Definition, search::FileReference};
use itertools::Itertools;
use syntax::{
SyntaxKind,
ast::{self, AstNode, HasAttrs, HasGenericParams, HasVisibility},
NodeOrToken, SyntaxKind, SyntaxNode, T,
algo::next_non_trivia_token,
ast::{
self, AstNode, HasAttrs, HasGenericParams, HasVisibility, syntax_factory::SyntaxFactory,
},
match_ast,
syntax_editor::{Position, SyntaxEditor},
syntax_editor::{Element, Position, SyntaxEditor},
};
use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
use crate::{
AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder, utils::cover_edit_range,
};
// Assist: convert_named_struct_to_tuple_struct
//
@@ -81,17 +85,17 @@ pub(crate) fn convert_named_struct_to_tuple_struct(
AssistId::refactor_rewrite("convert_named_struct_to_tuple_struct"),
"Convert to tuple struct",
strukt_or_variant.syntax().text_range(),
|edit| {
edit_field_references(ctx, edit, record_fields.fields());
edit_struct_references(ctx, edit, strukt_def);
edit_struct_def(ctx, edit, &strukt_or_variant, record_fields);
|builder| {
edit_field_references(ctx, builder, record_fields.fields());
edit_struct_references(ctx, builder, strukt_def);
edit_struct_def(ctx, builder, &strukt_or_variant, record_fields);
},
)
}
fn edit_struct_def(
ctx: &AssistContext<'_>,
edit: &mut SourceChangeBuilder,
builder: &mut SourceChangeBuilder,
strukt: &Either<ast::Struct, ast::Variant>,
record_fields: ast::RecordFieldList,
) {
@@ -108,24 +112,23 @@ fn edit_struct_def(
let field = ast::TupleField::cast(field_syntax)?;
Some(field)
});
let tuple_fields = ast::make::tuple_field_list(tuple_fields);
let record_fields_text_range = record_fields.syntax().text_range();
edit.edit_file(ctx.vfs_file_id());
edit.replace(record_fields_text_range, tuple_fields.syntax().text());
let make = SyntaxFactory::without_mappings();
let mut edit = builder.make_editor(strukt.syntax());
let tuple_fields = make.tuple_field_list(tuple_fields);
let mut elements = vec![tuple_fields.syntax().clone().into()];
if let Either::Left(strukt) = strukt {
if let Some(w) = strukt.where_clause() {
let mut where_clause = w.to_string();
if where_clause.ends_with(',') {
where_clause.pop();
}
where_clause.push(';');
edit.delete(w.syntax());
edit.delete(w.syntax().text_range());
edit.insert(record_fields_text_range.end(), ast::make::tokens::single_newline().text());
edit.insert(record_fields_text_range.end(), where_clause);
edit.insert(record_fields_text_range.end(), ast::make::tokens::single_newline().text());
elements.extend([
make.whitespace("\n").into(),
remove_trailing_comma(w).into(),
make.token(T![;]).into(),
make.whitespace("\n").into(),
]);
if let Some(tok) = strukt
.generic_param_list()
@@ -133,25 +136,28 @@ fn edit_struct_def(
.and_then(|tok| tok.next_token())
.filter(|tok| tok.kind() == SyntaxKind::WHITESPACE)
{
edit.delete(tok.text_range());
edit.delete(tok);
}
} else {
edit.insert(record_fields_text_range.end(), ";");
elements.push(make.token(T![;]).into());
}
}
edit.replace_with_many(record_fields.syntax(), elements);
if let Some(tok) = record_fields
.l_curly_token()
.and_then(|tok| tok.prev_token())
.filter(|tok| tok.kind() == SyntaxKind::WHITESPACE)
{
edit.delete(tok.text_range())
edit.delete(tok)
}
builder.add_file_edits(ctx.vfs_file_id(), edit);
}
fn edit_struct_references(
ctx: &AssistContext<'_>,
edit: &mut SourceChangeBuilder,
builder: &mut SourceChangeBuilder,
strukt: Either<hir::Struct, hir::Variant>,
) {
let strukt_def = match strukt {
@@ -161,17 +167,20 @@ fn edit_struct_references(
let usages = strukt_def.usages(&ctx.sema).include_self_refs().all();
for (file_id, refs) in usages {
edit.edit_file(file_id.file_id(ctx.db()));
let source = ctx.sema.parse(file_id);
let mut edit = builder.make_editor(source.syntax());
for r in refs {
process_struct_name_reference(ctx, r, edit);
process_struct_name_reference(ctx, r, &mut edit, &source);
}
builder.add_file_edits(file_id.file_id(ctx.db()), edit);
}
}
fn process_struct_name_reference(
ctx: &AssistContext<'_>,
r: FileReference,
edit: &mut SourceChangeBuilder,
edit: &mut SyntaxEditor,
source: &ast::SourceFile,
) -> Option<()> {
// First check if it's the last semgnet of a path that directly belongs to a record
// expression/pattern.
@@ -192,36 +201,26 @@ fn process_struct_name_reference(
match_ast! {
match parent {
ast::RecordPat(record_struct_pat) => {
// When we failed to get the original range for the whole struct expression node,
// When we failed to get the original range for the whole struct pattern node,
// we can't provide any reasonable edit. Leave it untouched.
let file_range = ctx.sema.original_range_opt(record_struct_pat.syntax())?;
edit.replace(
file_range.range,
ast::make::tuple_struct_pat(
record_struct_pat.path()?,
record_struct_pat
.record_pat_field_list()?
.fields()
.filter_map(|pat| pat.pat())
.chain(record_struct_pat.record_pat_field_list()?
.rest_pat()
.map(Into::into))
)
.to_string()
record_to_tuple_struct_like(
ctx,
source,
edit,
record_struct_pat.record_pat_field_list()?,
|it| it.fields().filter_map(|it| it.name_ref()),
);
},
ast::RecordExpr(record_expr) => {
// When we failed to get the original range for the whole struct pattern node,
// When we failed to get the original range for the whole struct expression node,
// we can't provide any reasonable edit. Leave it untouched.
let file_range = ctx.sema.original_range_opt(record_expr.syntax())?;
let path = record_expr.path()?;
let args = record_expr
.record_expr_field_list()?
.fields()
.filter_map(|f| f.expr())
.join(", ");
edit.replace(file_range.range, format!("{path}({args})"));
record_to_tuple_struct_like(
ctx,
source,
edit,
record_expr.record_expr_field_list()?,
|it| it.fields().filter_map(|it| it.name_ref()),
);
},
_ => {}
}
@@ -230,11 +229,67 @@ fn process_struct_name_reference(
Some(())
}
fn record_to_tuple_struct_like<T, I>(
ctx: &AssistContext<'_>,
source: &ast::SourceFile,
edit: &mut SyntaxEditor,
field_list: T,
fields: impl FnOnce(&T) -> I,
) -> Option<()>
where
T: AstNode,
I: IntoIterator<Item = ast::NameRef>,
{
let make = SyntaxFactory::without_mappings();
let orig = ctx.sema.original_range_opt(field_list.syntax())?;
let list_range = cover_edit_range(source, orig.range);
let l_curly = match list_range.start() {
NodeOrToken::Node(node) => node.first_token()?,
NodeOrToken::Token(t) => t.clone(),
};
let r_curly = match list_range.end() {
NodeOrToken::Node(node) => node.last_token()?,
NodeOrToken::Token(t) => t.clone(),
};
if l_curly.kind() == T!['{'] {
delete_whitespace(edit, l_curly.prev_token());
delete_whitespace(edit, l_curly.next_token());
edit.replace(l_curly, make.token(T!['(']));
}
if r_curly.kind() == T!['}'] {
delete_whitespace(edit, r_curly.prev_token());
edit.replace(r_curly, make.token(T![')']));
}
for name_ref in fields(&field_list) {
let Some(orig) = ctx.sema.original_range_opt(name_ref.syntax()) else { continue };
let name_range = cover_edit_range(source, orig.range);
if let Some(colon) = next_non_trivia_token(name_range.end().clone())
&& colon.kind() == T![:]
{
edit.delete(&colon);
edit.delete_all(name_range);
if let Some(next) = next_non_trivia_token(colon.clone())
&& next.kind() != T!['}']
{
// Avoid overlapping delete whitespace on `{ field: }`
delete_whitespace(edit, colon.next_token());
}
}
}
Some(())
}
fn edit_field_references(
ctx: &AssistContext<'_>,
edit: &mut SourceChangeBuilder,
builder: &mut SourceChangeBuilder,
fields: impl Iterator<Item = ast::RecordField>,
) {
let make = SyntaxFactory::without_mappings();
for (index, field) in fields.enumerate() {
let field = match ctx.sema.to_def(&field) {
Some(it) => it,
@@ -243,19 +298,46 @@ fn edit_field_references(
let def = Definition::Field(field);
let usages = def.usages(&ctx.sema).all();
for (file_id, refs) in usages {
edit.edit_file(file_id.file_id(ctx.db()));
let source = ctx.sema.parse(file_id);
let mut edit = builder.make_editor(source.syntax());
for r in refs {
if let Some(name_ref) = r.name.as_name_ref() {
// Only edit the field reference if it's part of a `.field` access
if name_ref.syntax().parent().and_then(ast::FieldExpr::cast).is_some() {
edit.replace(r.range, index.to_string());
edit.replace_all(
cover_edit_range(&source, r.range),
vec![make.name_ref(&index.to_string()).syntax().clone().into()],
);
}
}
}
builder.add_file_edits(file_id.file_id(ctx.db()), edit);
}
}
}
fn delete_whitespace(edit: &mut SyntaxEditor, whitespace: Option<impl Element>) {
let Some(whitespace) = whitespace else { return };
let NodeOrToken::Token(token) = whitespace.syntax_element() else { return };
if token.kind() == SyntaxKind::WHITESPACE && !token.text().contains('\n') {
edit.delete(token);
}
}
fn remove_trailing_comma(w: ast::WhereClause) -> SyntaxNode {
let w = w.syntax().clone_subtree();
let mut editor = SyntaxEditor::new(w.clone());
if let Some(last) = w.last_child_or_token()
&& last.kind() == T![,]
{
editor.delete(last);
}
editor.finish().new_root().clone()
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
@@ -677,6 +759,102 @@ struct Wrap<T>(T)
);
}
#[test]
fn convert_constructor_expr_uses_self() {
// regression test for #21595
check_assist(
convert_named_struct_to_tuple_struct,
r#"
struct $0Foo { field1: u32 }
impl Foo {
fn clone(&self) -> Self {
Self { field1: self.field1 }
}
}"#,
r#"
struct Foo(u32);
impl Foo {
fn clone(&self) -> Self {
Self(self.0)
}
}"#,
);
check_assist(
convert_named_struct_to_tuple_struct,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
struct $0Foo { field1: u32 }
impl Foo {
fn clone(&self) -> Self {
id!(Self { field1: self.field1 })
}
}"#,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
struct Foo(u32);
impl Foo {
fn clone(&self) -> Self {
id!(Self(self.0))
}
}"#,
);
}
#[test]
fn convert_pat_uses_self() {
// regression test for #21595
check_assist(
convert_named_struct_to_tuple_struct,
r#"
enum Foo {
$0Value { field: &'static Foo },
Nil,
}
fn foo(foo: &Foo) {
if let Foo::Value { field: Foo::Value { field } } = foo {}
}"#,
r#"
enum Foo {
Value(&'static Foo),
Nil,
}
fn foo(foo: &Foo) {
if let Foo::Value(Foo::Value(field)) = foo {}
}"#,
);
check_assist(
convert_named_struct_to_tuple_struct,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
enum Foo {
$0Value { field: &'static Foo },
Nil,
}
fn foo(foo: &Foo) {
if let id!(Foo::Value { field: Foo::Value { field } }) = foo {}
}"#,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
enum Foo {
Value(&'static Foo),
Nil,
}
fn foo(foo: &Foo) {
if let id!(Foo::Value(Foo::Value(field))) = foo {}
}"#,
);
}
#[test]
fn not_applicable_other_than_record_variant() {
check_assist_not_applicable(
@@ -1042,7 +1220,9 @@ macro_rules! id {
fn test() {
id! {
let s = Struct(42);
let s = Struct(
42,
);
let Struct(value) = s;
let Struct(inner) = s;
}
@@ -10,14 +10,14 @@
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make,
syntax_factory::SyntaxFactory,
},
};
use crate::{
AssistId,
assist_context::{AssistContext, Assists},
utils::{invert_boolean_expression_legacy, is_never_block},
utils::{invert_boolean_expression, is_never_block},
};
// Assist: convert_to_guarded_return
@@ -69,6 +69,7 @@ fn if_expr_to_guarded_return(
acc: &mut Assists,
ctx: &AssistContext<'_>,
) -> Option<()> {
let make = SyntaxFactory::without_mappings();
let else_block = match if_expr.else_branch() {
Some(ast::ElseBranch::Block(block_expr)) if is_never_block(&ctx.sema, &block_expr) => {
Some(block_expr)
@@ -88,7 +89,7 @@ fn if_expr_to_guarded_return(
return None;
}
let let_chains = flat_let_chain(cond);
let let_chains = flat_let_chain(cond, &make);
let then_branch = if_expr.then_branch()?;
let then_block = then_branch.stmt_list()?;
@@ -110,7 +111,8 @@ fn if_expr_to_guarded_return(
let early_expression = else_block
.or_else(|| {
early_expression(parent_container, &ctx.sema).map(ast::make::tail_only_block_expr)
early_expression(parent_container, &ctx.sema, &make)
.map(ast::make::tail_only_block_expr)
})?
.reset_indent();
@@ -133,6 +135,7 @@ fn if_expr_to_guarded_return(
"Convert to guarded return",
target,
|edit| {
let make = SyntaxFactory::without_mappings();
let if_indent_level = IndentLevel::from_node(if_expr.syntax());
let replacement = let_chains.into_iter().map(|expr| {
if let ast::Expr::LetExpr(let_expr) = &expr
@@ -140,15 +143,15 @@ fn if_expr_to_guarded_return(
{
// If-let.
let let_else_stmt =
make::let_else_stmt(pat, None, expr, early_expression.clone());
make.let_else_stmt(pat, None, expr, early_expression.clone());
let let_else_stmt = let_else_stmt.indent(if_indent_level);
let_else_stmt.syntax().clone()
} else {
// If.
let new_expr = {
let then_branch = clean_stmt_block(&early_expression);
let cond = invert_boolean_expression_legacy(expr);
make::expr_if(cond, then_branch, None).indent(if_indent_level)
let then_branch = clean_stmt_block(&early_expression, &make);
let cond = invert_boolean_expression(&make, expr);
make.expr_if(cond, then_branch, None).indent(if_indent_level)
};
new_expr.syntax().clone()
}
@@ -159,7 +162,7 @@ fn if_expr_to_guarded_return(
.enumerate()
.flat_map(|(i, node)| {
(i != 0)
.then(|| make::tokens::whitespace(newline).into())
.then(|| make.whitespace(newline).into())
.into_iter()
.chain(node.children_with_tokens())
})
@@ -201,12 +204,13 @@ fn let_stmt_to_guarded_return(
let happy_pattern = try_enum.happy_pattern(pat);
let target = let_stmt.syntax().text_range();
let make = SyntaxFactory::without_mappings();
let early_expression: ast::Expr = {
let parent_block =
let_stmt.syntax().parent()?.ancestors().find_map(ast::BlockExpr::cast)?;
let parent_container = parent_block.syntax().parent()?;
early_expression(parent_container, &ctx.sema)?
early_expression(parent_container, &ctx.sema, &make)?
};
acc.add(
@@ -215,9 +219,10 @@ fn let_stmt_to_guarded_return(
target,
|edit| {
let let_indent_level = IndentLevel::from_node(let_stmt.syntax());
let make = SyntaxFactory::without_mappings();
let replacement = {
let let_else_stmt = make::let_else_stmt(
let let_else_stmt = make.let_else_stmt(
happy_pattern,
let_stmt.ty(),
expr.reset_indent(),
@@ -228,6 +233,7 @@ fn let_stmt_to_guarded_return(
};
let mut editor = edit.make_editor(let_stmt.syntax());
editor.replace(let_stmt.syntax(), replacement);
editor.add_mappings(make.finish_with_mappings());
edit.add_file_edits(ctx.vfs_file_id(), editor);
},
)
@@ -236,38 +242,39 @@ fn let_stmt_to_guarded_return(
fn early_expression(
parent_container: SyntaxNode,
sema: &Semantics<'_, RootDatabase>,
make: &SyntaxFactory,
) -> Option<ast::Expr> {
let return_none_expr = || {
let none_expr = make::expr_path(make::ext::ident_path("None"));
make::expr_return(Some(none_expr))
let none_expr = make.expr_path(make.ident_path("None"));
make.expr_return(Some(none_expr))
};
if let Some(fn_) = ast::Fn::cast(parent_container.clone())
&& let Some(fn_def) = sema.to_def(&fn_)
&& let Some(TryEnum::Option) = TryEnum::from_ty(sema, &fn_def.ret_type(sema.db))
{
return Some(return_none_expr());
return Some(return_none_expr().into());
}
if let Some(body) = ast::ClosureExpr::cast(parent_container.clone()).and_then(|it| it.body())
&& let Some(ret_ty) = sema.type_of_expr(&body).map(TypeInfo::original)
&& let Some(TryEnum::Option) = TryEnum::from_ty(sema, &ret_ty)
{
return Some(return_none_expr());
return Some(return_none_expr().into());
}
Some(match parent_container.kind() {
WHILE_EXPR | LOOP_EXPR | FOR_EXPR => make::expr_continue(None),
FN | CLOSURE_EXPR => make::expr_return(None),
WHILE_EXPR | LOOP_EXPR | FOR_EXPR => make.expr_continue(None).into(),
FN | CLOSURE_EXPR => make.expr_return(None).into(),
_ => return None,
})
}
fn flat_let_chain(mut expr: ast::Expr) -> Vec<ast::Expr> {
fn flat_let_chain(mut expr: ast::Expr, make: &SyntaxFactory) -> Vec<ast::Expr> {
let mut chains = vec![];
let mut reduce_cond = |rhs| {
if !matches!(rhs, ast::Expr::LetExpr(_))
&& let Some(last) = chains.pop_if(|last| !matches!(last, ast::Expr::LetExpr(_)))
{
chains.push(make::expr_bin_op(rhs, ast::BinaryOp::LogicOp(ast::LogicOp::And), last));
chains.push(make.expr_bin_op(rhs, ast::BinaryOp::LogicOp(ast::LogicOp::And), last));
} else {
chains.push(rhs);
}
@@ -286,12 +293,12 @@ fn flat_let_chain(mut expr: ast::Expr) -> Vec<ast::Expr> {
chains
}
fn clean_stmt_block(block: &ast::BlockExpr) -> ast::BlockExpr {
fn clean_stmt_block(block: &ast::BlockExpr, make: &SyntaxFactory) -> ast::BlockExpr {
if block.statements().next().is_none()
&& let Some(tail_expr) = block.tail_expr()
&& block.modifier().is_none()
{
make::block_expr(once(make::expr_stmt(tail_expr).into()), None)
make.block_expr(once(make.expr_stmt(tail_expr).into()), None)
} else {
block.clone()
}
@@ -1,17 +1,21 @@
use either::Either;
use hir::FileRangeWrapper;
use ide_db::defs::{Definition, NameRefClass};
use std::ops::RangeInclusive;
use ide_db::{
defs::{Definition, NameRefClass},
search::FileReference,
};
use syntax::{
SyntaxElement, SyntaxKind, SyntaxNode, T, TextSize,
SyntaxKind, T,
ast::{
self, AstNode, HasAttrs, HasGenericParams, HasVisibility, syntax_factory::SyntaxFactory,
self, AstNode, HasArgList, HasAttrs, HasGenericParams, HasVisibility,
syntax_factory::SyntaxFactory,
},
match_ast,
syntax_editor::{Element, Position, SyntaxEditor},
};
use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
use crate::{
AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder, utils::cover_edit_range,
};
// Assist: convert_tuple_struct_to_named_struct
//
@@ -147,93 +151,121 @@ fn edit_struct_references(
};
let usages = strukt_def.usages(&ctx.sema).include_self_refs().all();
let edit_node = |node: SyntaxNode| -> Option<SyntaxNode> {
let make = SyntaxFactory::without_mappings();
match_ast! {
match node {
ast::TupleStructPat(tuple_struct_pat) => {
Some(make.record_pat_with_fields(
tuple_struct_pat.path()?,
generate_record_pat_list(&tuple_struct_pat, names),
).syntax().clone())
},
// for tuple struct creations like Foo(42)
ast::CallExpr(call_expr) => {
let path = call_expr.syntax().descendants().find_map(ast::PathExpr::cast).and_then(|expr| expr.path())?;
// this also includes method calls like Foo::new(42), we should skip them
if let Some(name_ref) = path.segment().and_then(|s| s.name_ref()) {
match NameRefClass::classify(&ctx.sema, &name_ref) {
Some(NameRefClass::Definition(Definition::SelfType(_), _)) => {},
Some(NameRefClass::Definition(def, _)) if def == strukt_def => {},
_ => return None,
};
}
let arg_list = call_expr.syntax().descendants().find_map(ast::ArgList::cast)?;
Some(
make.record_expr(
path,
ast::make::record_expr_field_list(arg_list.args().zip(names).map(
|(expr, name)| {
ast::make::record_expr_field(
ast::make::name_ref(&name.to_string()),
Some(expr),
)
},
)),
).syntax().clone()
)
},
_ => None,
}
}
};
for (file_id, refs) in usages {
let source = ctx.sema.parse(file_id);
let source = source.syntax();
let mut editor = edit.make_editor(source.syntax());
let mut editor = edit.make_editor(source);
for r in refs.iter().rev() {
if let Some((old_node, new_node)) = r
.name
.syntax()
.ancestors()
.find_map(|node| Some((node.clone(), edit_node(node.clone())?)))
{
if let Some(old_node) = ctx.sema.original_syntax_node_rooted(&old_node) {
editor.replace(old_node, new_node);
} else {
let FileRangeWrapper { file_id: _, range } = ctx.sema.original_range(&old_node);
let parent = source.covering_element(range);
match parent {
SyntaxElement::Token(token) => {
editor.replace(token, new_node.syntax_element());
}
SyntaxElement::Node(parent_node) => {
// replace the part of macro
// ```
// foo!(a, Test::A(0));
// ^^^^^^^^^^^^^^^ // parent_node
// ^^^^^^^^^^ // replace_range
// ```
let start = parent_node
.children_with_tokens()
.find(|t| t.text_range().contains(range.start()));
let end = parent_node
.children_with_tokens()
.find(|t| t.text_range().contains(range.end() - TextSize::new(1)));
if let (Some(start), Some(end)) = (start, end) {
let replace_range = RangeInclusive::new(start, end);
editor.replace_all(replace_range, vec![new_node.into()]);
}
}
for r in refs {
process_struct_name_reference(ctx, r, &mut editor, &source, &strukt_def, names);
}
edit.add_file_edits(file_id.file_id(ctx.db()), editor);
}
}
fn process_struct_name_reference(
ctx: &AssistContext<'_>,
r: FileReference,
editor: &mut SyntaxEditor,
source: &ast::SourceFile,
strukt_def: &Definition,
names: &[ast::Name],
) -> Option<()> {
let make = SyntaxFactory::without_mappings();
let name_ref = r.name.as_name_ref()?;
let path_segment = name_ref.syntax().parent().and_then(ast::PathSegment::cast)?;
let full_path = path_segment.syntax().parent().and_then(ast::Path::cast)?.top_path();
if full_path.segment()?.name_ref()? != *name_ref {
// `name_ref` isn't the last segment of the path, so `full_path` doesn't point to the
// struct we want to edit.
return None;
}
let parent = full_path.syntax().parent()?;
match_ast! {
match parent {
ast::TupleStructPat(tuple_struct_pat) => {
let range = ctx.sema.original_range_opt(tuple_struct_pat.syntax())?.range;
let new = make.record_pat_with_fields(
full_path,
generate_record_pat_list(&tuple_struct_pat, names),
);
editor.replace_all(cover_edit_range(source, range), vec![new.syntax().clone().into()]);
},
ast::PathExpr(path_expr) => {
let call_expr = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
// this also includes method calls like Foo::new(42), we should skip them
match NameRefClass::classify(&ctx.sema, name_ref) {
Some(NameRefClass::Definition(Definition::SelfType(_), _)) => {},
Some(NameRefClass::Definition(def, _)) if def == *strukt_def => {},
_ => return None,
}
let arg_list = call_expr.arg_list()?;
let mut first_insert = vec![];
for (expr, name) in arg_list.args().zip(names) {
let range = ctx.sema.original_range_opt(expr.syntax())?.range;
let place = cover_edit_range(source, range);
let elements = vec![
make.name_ref(&name.text()).syntax().clone().into(),
make.token(T![:]).into(),
make.whitespace(" ").into(),
];
if first_insert.is_empty() {
// XXX: SyntaxEditor cannot insert after deleted element
first_insert = elements;
} else {
editor.insert_all(Position::before(place.start()), elements);
}
}
}
process_delimiter(ctx, source, editor, &arg_list, first_insert);
},
_ => {}
}
edit.add_file_edits(file_id.file_id(ctx.db()), editor);
}
Some(())
}
fn process_delimiter(
ctx: &AssistContext<'_>,
source: &ast::SourceFile,
editor: &mut SyntaxEditor,
list: &impl AstNode,
first_insert: Vec<syntax::SyntaxElement>,
) {
let Some(range) = ctx.sema.original_range_opt(list.syntax()) else { return };
let place = cover_edit_range(source, range.range);
let l_paren = match place.start() {
syntax::NodeOrToken::Node(node) => node.first_token(),
syntax::NodeOrToken::Token(t) => Some(t.clone()),
};
let r_paren = match place.end() {
syntax::NodeOrToken::Node(node) => node.last_token(),
syntax::NodeOrToken::Token(t) => Some(t.clone()),
};
let make = SyntaxFactory::without_mappings();
if let Some(l_paren) = l_paren
&& l_paren.kind() == T!['(']
{
let mut open_delim = vec![
make.whitespace(" ").into(),
make.token(T!['{']).into(),
make.whitespace(" ").into(),
];
open_delim.extend(first_insert);
editor.replace_with_many(l_paren, open_delim);
}
if let Some(r_paren) = r_paren
&& r_paren.kind() == T![')']
{
editor.replace_with_many(
r_paren,
vec![make.whitespace(" ").into(), make.token(T!['}']).into()],
);
}
}
@@ -252,13 +284,15 @@ fn edit_field_references(
let usages = def.usages(&ctx.sema).all();
for (file_id, refs) in usages {
let source = ctx.sema.parse(file_id);
let source = source.syntax();
let mut editor = edit.make_editor(source);
let mut editor = edit.make_editor(source.syntax());
for r in refs {
if let Some(name_ref) = r.name.as_name_ref()
&& let Some(original) = ctx.sema.original_ast_node(name_ref.clone())
&& let Some(original) = ctx.sema.original_range_opt(name_ref.syntax())
{
editor.replace(original.syntax(), name.syntax());
editor.replace_all(
cover_edit_range(&source, original.range),
vec![name.syntax().clone().into()],
);
}
}
edit.add_file_edits(file_id.file_id(ctx.db()), editor);
@@ -739,6 +773,64 @@ struct Wrap<T>
"#,
);
}
#[test]
fn convert_expr_uses_self() {
check_assist(
convert_tuple_struct_to_named_struct,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
struct T$0(u8);
fn test(t: T) {
T(t.0);
id!(T(t.0));
}"#,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
struct T { field1: u8 }
fn test(t: T) {
T { field1: t.field1 };
id!(T { field1: t.field1 });
}"#,
);
}
#[test]
#[ignore = "FIXME overlap edits in nested uses self"]
fn convert_pat_uses_self() {
check_assist(
convert_tuple_struct_to_named_struct,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
enum T {
$0Value(&'static T),
Nil,
}
fn test(t: T) {
if let T::Value(T::Value(t)) = t {}
if let id!(T::Value(T::Value(t))) = t {}
}"#,
r#"
macro_rules! id {
($($t:tt)*) => { $($t)* }
}
enum T {
Value { field1: &'static T },
Nil,
}
fn test(t: T) {
if let T::Value { field1: T::Value { field1: t } } = t {}
if let id!(T::Value { field1: T::Value { field1: t } }) = t {}
}"#,
);
}
#[test]
fn not_applicable_other_than_tuple_variant() {
check_assist_not_applicable(
@@ -6,7 +6,7 @@
ast::{
self, HasLoopBody,
edit::{AstNodeEdit, IndentLevel},
make,
syntax_factory::SyntaxFactory,
},
syntax_editor::{Element, Position},
};
@@ -14,7 +14,7 @@
use crate::{
AssistId,
assist_context::{AssistContext, Assists},
utils::invert_boolean_expression_legacy,
utils::invert_boolean_expression,
};
// Assist: convert_while_to_loop
@@ -52,44 +52,47 @@ pub(crate) fn convert_while_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>)
"Convert while to loop",
target,
|builder| {
let make = SyntaxFactory::without_mappings();
let mut edit = builder.make_editor(while_expr.syntax());
let while_indent_level = IndentLevel::from_node(while_expr.syntax());
let break_block = make::block_expr(
iter::once(make::expr_stmt(make::expr_break(None, None)).into()),
None,
)
.indent(IndentLevel(1));
let break_block = make
.block_expr(
iter::once(make.expr_stmt(make.expr_break(None, None).into()).into()),
None,
)
.indent(IndentLevel(1));
edit.replace_all(
while_kw.syntax_element()..=while_cond.syntax().syntax_element(),
vec![make::token(T![loop]).syntax_element()],
vec![make.token(T![loop]).syntax_element()],
);
if is_pattern_cond(while_cond.clone()) {
let then_branch = while_body.reset_indent().indent(IndentLevel(1));
let if_expr = make::expr_if(while_cond, then_branch, Some(break_block.into()));
let stmts = iter::once(make::expr_stmt(if_expr.into()).into());
let block_expr = make::block_expr(stmts, None);
let if_expr = make.expr_if(while_cond, then_branch, Some(break_block.into()));
let stmts = iter::once(make.expr_stmt(if_expr.into()).into());
let block_expr = make.block_expr(stmts, None);
edit.replace(while_body.syntax(), block_expr.indent(while_indent_level).syntax());
} else {
let if_cond = invert_boolean_expression_legacy(while_cond);
let if_expr = make::expr_if(if_cond, break_block, None).indent(while_indent_level);
let if_cond = invert_boolean_expression(&make, while_cond);
let if_expr = make.expr_if(if_cond, break_block, None).indent(while_indent_level);
if !while_body.syntax().text().contains_char('\n') {
edit.insert(
Position::after(&l_curly),
make::tokens::whitespace(&format!("\n{while_indent_level}")),
make.whitespace(&format!("\n{while_indent_level}")),
);
}
edit.insert_all(
Position::after(&l_curly),
vec![
make::tokens::whitespace(&format!("\n{}", while_indent_level + 1)).into(),
make.whitespace(&format!("\n{}", while_indent_level + 1)).into(),
if_expr.syntax().syntax_element(),
],
);
};
edit.add_mappings(make.finish_with_mappings());
builder.add_file_edits(ctx.vfs_file_id(), edit);
},
)
@@ -381,23 +381,20 @@ fn build_usage_edit(
Some(field_expr) => Some({
let field_name: SmolStr = field_expr.name_ref()?.to_string().into();
let new_field_name = field_names.get(&field_name)?;
let new_expr = ast::make::expr_path(ast::make::ext::ident_path(new_field_name));
let new_expr = make.expr_path(make.ident_path(new_field_name));
// If struct binding is a reference, we might need to deref field usages
if data.is_ref {
let (replace_expr, ref_data) = determine_ref_and_parens(ctx, &field_expr);
(
replace_expr.syntax().clone_for_update(),
ref_data.wrap_expr(new_expr).syntax().clone_for_update(),
)
(replace_expr.syntax().clone(), ref_data.wrap_expr(new_expr, make).syntax().clone())
} else {
(field_expr.syntax().clone(), new_expr.syntax().clone_for_update())
(field_expr.syntax().clone(), new_expr.syntax().clone())
}
}),
None => Some((
usage.name.syntax().as_node().unwrap().clone(),
make.expr_macro(
ast::make::ext::ident_path("todo"),
make.ident_path("todo"),
make.token_tree(syntax::SyntaxKind::L_PAREN, []),
)
.syntax()
@@ -9,7 +9,6 @@
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make,
syntax_factory::SyntaxFactory,
},
};
@@ -68,41 +67,46 @@ pub(crate) fn desugar_try_expr(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
AssistId::refactor_rewrite("desugar_try_expr_match"),
"Replace try expression with match",
target,
|edit| {
|builder| {
let make = SyntaxFactory::with_mappings();
let mut editor = builder.make_editor(try_expr.syntax());
let sad_pat = match try_enum {
TryEnum::Option => make::path_pat(make::ext::ident_path("None")),
TryEnum::Result => make::tuple_struct_pat(
make::ext::ident_path("Err"),
iter::once(make::path_pat(make::ext::ident_path("err"))),
)
.into(),
TryEnum::Option => make.path_pat(make.ident_path("None")),
TryEnum::Result => make
.tuple_struct_pat(
make.ident_path("Err"),
iter::once(make.path_pat(make.ident_path("err"))),
)
.into(),
};
let sad_expr = match try_enum {
TryEnum::Option => {
make::expr_return(Some(make::expr_path(make::ext::ident_path("None"))))
}
TryEnum::Result => make::expr_return(Some(
make::expr_call(
make::expr_path(make::ext::ident_path("Err")),
make::arg_list(iter::once(make::expr_path(make::ext::ident_path("err")))),
TryEnum::Option => make.expr_return(Some(make.expr_path(make.ident_path("None")))),
TryEnum::Result => make.expr_return(Some(
make.expr_call(
make.expr_path(make.ident_path("Err")),
make.arg_list(iter::once(make.expr_path(make.ident_path("err")))),
)
.into(),
)),
};
let happy_arm = make::match_arm(
try_enum.happy_pattern(make::ident_pat(false, false, make::name("it")).into()),
let happy_arm = make.match_arm(
try_enum.happy_pattern(make.ident_pat(false, false, make.name("it")).into()),
None,
make::expr_path(make::ext::ident_path("it")),
make.expr_path(make.ident_path("it")),
);
let sad_arm = make::match_arm(sad_pat, None, sad_expr);
let sad_arm = make.match_arm(sad_pat, None, sad_expr.into());
let match_arm_list = make::match_arm_list([happy_arm, sad_arm]);
let match_arm_list = make.match_arm_list([happy_arm, sad_arm]);
let expr_match = make::expr_match(expr.clone(), match_arm_list)
let expr_match = make
.expr_match(expr.clone(), match_arm_list)
.indent(IndentLevel::from_node(try_expr.syntax()));
edit.replace_ast::<ast::Expr>(try_expr.clone().into(), expr_match.into());
editor.replace(try_expr.syntax(), expr_match.syntax());
editor.add_mappings(make.finish_with_mappings());
builder.add_file_edits(ctx.vfs_file_id(), editor);
},
);
@@ -8,7 +8,7 @@
AstNode, AstToken, NodeOrToken,
SyntaxKind::WHITESPACE,
SyntaxToken, T,
ast::{self, TokenTree, make, syntax_factory::SyntaxFactory},
ast::{self, TokenTree, syntax_factory::SyntaxFactory},
};
// Assist: extract_expressions_from_format_string
@@ -57,6 +57,7 @@ pub(crate) fn extract_expressions_from_format_string(
"Extract format expressions",
tt.syntax().text_range(),
|edit| {
let make = SyntaxFactory::without_mappings();
// Extract existing arguments in macro
let mut raw_tokens = tt.token_trees_and_tokens().skip(1).collect_vec();
let format_string_index = format_str_index(&raw_tokens, &fmt_string);
@@ -94,14 +95,14 @@ pub(crate) fn extract_expressions_from_format_string(
let mut new_tt_bits = raw_tokens;
let mut placeholder_indexes = vec![];
new_tt_bits.push(NodeOrToken::Token(make::tokens::literal(&new_fmt)));
new_tt_bits.push(NodeOrToken::Token(make.expr_literal(&new_fmt).token().clone()));
for arg in extracted_args {
if matches!(arg, Arg::Expr(_) | Arg::Placeholder) {
// insert ", " before each arg
new_tt_bits.extend_from_slice(&[
NodeOrToken::Token(make::token(T![,])),
NodeOrToken::Token(make::tokens::single_space()),
NodeOrToken::Token(make.token(T![,])),
NodeOrToken::Token(make.whitespace(" ")),
]);
}
@@ -109,7 +110,7 @@ pub(crate) fn extract_expressions_from_format_string(
Arg::Expr(s) => {
// insert arg
let expr = ast::Expr::parse(&s, ctx.edition()).syntax_node();
let mut expr_tt = utils::tt_from_syntax(expr);
let mut expr_tt = utils::tt_from_syntax(expr, &make);
new_tt_bits.append(&mut expr_tt);
}
Arg::Placeholder => {
@@ -120,7 +121,7 @@ pub(crate) fn extract_expressions_from_format_string(
}
None => {
placeholder_indexes.push(new_tt_bits.len());
new_tt_bits.push(NodeOrToken::Token(make::token(T![_])));
new_tt_bits.push(NodeOrToken::Token(make.token(T![_])));
}
}
}
@@ -129,7 +130,6 @@ pub(crate) fn extract_expressions_from_format_string(
}
// Insert new args
let make = SyntaxFactory::with_mappings();
let new_tt = make.token_tree(tt_delimiter, new_tt_bits);
let mut editor = edit.make_editor(tt.syntax());
editor.replace(tt.syntax(), new_tt.syntax());
@@ -9,7 +9,6 @@
ast::{
self, AstNode,
edit::{AstNodeEdit, IndentLevel},
make,
syntax_factory::SyntaxFactory,
},
syntax_editor::Position,
@@ -75,7 +74,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
.next()
.and_then(ast::Expr::cast)
{
expr.syntax().ancestors().find_map(valid_target_expr)?.syntax().clone()
expr.syntax().ancestors().find_map(valid_target_expr(ctx))?.syntax().clone()
} else {
return None;
}
@@ -96,7 +95,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
let to_extract = node
.descendants()
.take_while(|it| range.contains_range(it.text_range()))
.find_map(valid_target_expr)?;
.find_map(valid_target_expr(ctx))?;
let ty = ctx.sema.type_of_expr(&to_extract).map(TypeInfo::adjusted);
if matches!(&ty, Some(ty_info) if ty_info.is_unit()) {
@@ -176,7 +175,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
let mut editor = edit.make_editor(&expr_replace);
let pat_name = make.name(&var_name);
let name_expr = make.expr_path(make::ext::ident_path(&var_name));
let name_expr = make.expr_path(make.ident_path(&var_name));
if let Some(cap) = ctx.config.snippet_cap {
let tabstop = edit.make_tabstop_before(cap);
@@ -233,7 +232,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
Position::before(place),
vec![
new_stmt.syntax().clone().into(),
make::tokens::whitespace(&trailing_ws).into(),
make.whitespace(&trailing_ws).into(),
],
);
@@ -283,14 +282,19 @@ fn peel_parens(mut expr: ast::Expr) -> ast::Expr {
/// Check whether the node is a valid expression which can be extracted to a variable.
/// In general that's true for any expression, but in some cases that would produce invalid code.
fn valid_target_expr(node: SyntaxNode) -> Option<ast::Expr> {
match node.kind() {
SyntaxKind::PATH_EXPR | SyntaxKind::LOOP_EXPR | SyntaxKind::LET_EXPR => None,
fn valid_target_expr(ctx: &AssistContext<'_>) -> impl Fn(SyntaxNode) -> Option<ast::Expr> {
|node| match node.kind() {
SyntaxKind::LOOP_EXPR | SyntaxKind::LET_EXPR => None,
SyntaxKind::BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()),
SyntaxKind::RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
SyntaxKind::BLOCK_EXPR => {
ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from)
}
SyntaxKind::PATH_EXPR => {
let path_expr = ast::PathExpr::cast(node)?;
let path_resolution = ctx.sema.resolve_path(&path_expr.path()?)?;
like_const_value(ctx, path_resolution).then_some(path_expr.into())
}
_ => ast::Expr::cast(node),
}
}
@@ -455,6 +459,31 @@ fn from(to_extract: &ast::Expr, kind: &ExtractionKind) -> Option<Anchor> {
}
}
fn like_const_value(ctx: &AssistContext<'_>, path_resolution: hir::PathResolution) -> bool {
let db = ctx.db();
let adt_like_const_value = |adt: Option<hir::Adt>| matches!(adt, Some(hir::Adt::Struct(s)) if s.kind(db) == hir::StructKind::Unit);
match path_resolution {
hir::PathResolution::Def(def) => match def {
hir::ModuleDef::Adt(adt) => adt_like_const_value(Some(adt)),
hir::ModuleDef::Variant(variant) => variant.kind(db) == hir::StructKind::Unit,
hir::ModuleDef::TypeAlias(ty) => adt_like_const_value(ty.ty(db).as_adt()),
hir::ModuleDef::Const(_) | hir::ModuleDef::Static(_) => true,
hir::ModuleDef::Trait(_)
| hir::ModuleDef::BuiltinType(_)
| hir::ModuleDef::Macro(_)
| hir::ModuleDef::Module(_) => false,
hir::ModuleDef::Function(_) => false, // no extract named function
},
hir::PathResolution::SelfType(ty) => adt_like_const_value(ty.self_ty(db).as_adt()),
hir::PathResolution::ConstParam(_) => true,
hir::PathResolution::Local(_)
| hir::PathResolution::TypeParam(_)
| hir::PathResolution::BuiltinAttr(_)
| hir::PathResolution::ToolModule(_)
| hir::PathResolution::DeriveHelper(_) => false,
}
}
#[cfg(test)]
mod tests {
// NOTE: We use check_assist_by_label, but not check_assist_not_applicable_by_label
@@ -1747,6 +1776,27 @@ fn main() {
);
}
#[test]
fn extract_non_local_path_expr() {
check_assist_by_label(
extract_variable,
r#"
struct Foo;
fn foo() -> Foo {
$0Foo$0
}
"#,
r#"
struct Foo;
fn foo() -> Foo {
let $0foo = Foo;
foo
}
"#,
"Extract into variable",
);
}
#[test]
fn extract_var_for_return_not_applicable() {
check_assist_not_applicable(extract_variable, "fn foo() { $0return$0; } ");
@@ -4,7 +4,7 @@
ast::{
self, AstNode, HasGenericParams, HasName, HasVisibility as _,
edit::{AstNodeEdit, IndentLevel},
make,
syntax_factory::SyntaxFactory,
},
syntax_editor::Position,
};
@@ -100,7 +100,6 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
let Some(impl_def) = find_struct_impl(ctx, &adt, std::slice::from_ref(&name)) else {
continue;
};
let field = make::ext::field_from_idents(["self", &field_name])?;
acc.add_group(
&GroupLabel("Generate delegate methods…".to_owned()),
@@ -108,10 +107,14 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
format!("Generate delegate for `{field_name}.{name}()`",),
target,
|edit| {
let make = SyntaxFactory::without_mappings();
let field = make
.field_from_idents(["self", &field_name])
.expect("always be a valid expression");
// Create the function
let method_source = match ctx.sema.source(method) {
Some(source) => {
let v = source.value.clone_for_update();
let v = source.value;
let source_scope = ctx.sema.scope(v.syntax());
let target_scope = ctx.sema.scope(strukt.syntax());
if let (Some(s), Some(t)) = (source_scope, target_scope) {
@@ -132,42 +135,42 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
let is_unsafe = method_source.unsafe_token().is_some();
let is_gen = method_source.gen_token().is_some();
let fn_name = make::name(&name);
let fn_name = make.name(&name);
let type_params = method_source.generic_param_list();
let where_clause = method_source.where_clause();
let params =
method_source.param_list().unwrap_or_else(|| make::param_list(None, []));
method_source.param_list().unwrap_or_else(|| make.param_list(None, []));
// compute the `body`
let arg_list = method_source
.param_list()
.map(convert_param_list_to_arg_list)
.unwrap_or_else(|| make::arg_list([]));
.map(|v| convert_param_list_to_arg_list(v, &make))
.unwrap_or_else(|| make.arg_list([]));
let tail_expr =
make::expr_method_call(field, make::name_ref(&name), arg_list).into();
let tail_expr = make.expr_method_call(field, make.name_ref(&name), arg_list).into();
let tail_expr_finished =
if is_async { make::expr_await(tail_expr) } else { tail_expr };
let body = make::block_expr([], Some(tail_expr_finished));
if is_async { make.expr_await(tail_expr).into() } else { tail_expr };
let body = make.block_expr([], Some(tail_expr_finished));
let ret_type = method_source.ret_type();
let f = make::fn_(
None,
vis,
fn_name,
type_params,
where_clause,
params,
body,
ret_type,
is_async,
is_const,
is_unsafe,
is_gen,
)
.indent(IndentLevel(1));
let f = make
.fn_(
None,
vis,
fn_name,
type_params,
where_clause,
params,
body,
ret_type,
is_async,
is_const,
is_unsafe,
is_gen,
)
.indent(IndentLevel(1));
let item = ast::AssocItem::Fn(f.clone());
let mut editor = edit.make_editor(strukt.syntax());
@@ -179,7 +182,7 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
Some(item)
}
None => {
let assoc_item_list = make::assoc_item_list(Some(vec![item]));
let assoc_item_list = make.assoc_item_list(vec![item]);
editor.insert(
Position::last_child_of(impl_def.syntax()),
assoc_item_list.syntax(),
@@ -192,17 +195,16 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
let ty_params = strukt.generic_param_list();
let ty_args = ty_params.as_ref().map(|it| it.to_generic_args());
let where_clause = strukt.where_clause();
let assoc_item_list = make::assoc_item_list(Some(vec![item]));
let assoc_item_list = make.assoc_item_list(vec![item]);
let impl_def = make::impl_(
let impl_def = make.impl_(
None,
ty_params,
ty_args,
make::ty_path(make::ext::ident_path(name)),
syntax::ast::Type::PathType(make.ty_path(make.ident_path(name))),
where_clause,
Some(assoc_item_list),
)
.clone_for_update();
);
// Fixup impl_def indentation
let indent = strukt.indent_level();
@@ -213,7 +215,7 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
editor.insert_all(
Position::after(strukt.syntax()),
vec![
make::tokens::whitespace(&format!("\n\n{indent}")).into(),
make.whitespace(&format!("\n\n{indent}")).into(),
impl_def.syntax().clone().into(),
],
);
@@ -227,6 +229,7 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
let tabstop = edit.make_tabstop_before(cap);
editor.add_annotation(fn_.syntax(), tabstop);
}
editor.add_mappings(make.finish_with_mappings());
edit.add_file_edits(ctx.vfs_file_id(), editor);
},
)?;
@@ -782,7 +782,7 @@ fn func_assoc_item(
};
// Build argument list with self expression prepended
let other_args = convert_param_list_to_arg_list(l);
let other_args = convert_param_list_to_arg_list(l, &make);
let all_args: Vec<ast::Expr> =
std::iter::once(tail_expr_self).chain(other_args.args()).collect();
let args = make.arg_list(all_args);
@@ -790,13 +790,13 @@ fn func_assoc_item(
make.expr_call(make.expr_path(qualified_path), args).into()
}
None => make
.expr_call(make.expr_path(qualified_path), convert_param_list_to_arg_list(l))
.expr_call(make.expr_path(qualified_path), convert_param_list_to_arg_list(l, &make))
.into(),
},
None => make
.expr_call(
make.expr_path(qualified_path),
convert_param_list_to_arg_list(make.param_list(None, Vec::new())),
convert_param_list_to_arg_list(make.param_list(None, Vec::new()), &make),
)
.into(),
};
@@ -1,8 +1,10 @@
use crate::assist_context::{AssistContext, Assists};
use ide_db::assists::AssistId;
use syntax::{
AstNode, SyntaxKind, T,
ast::{self, HasGenericParams, HasName, HasVisibility, edit::AstNodeEdit, make},
AstNode, AstToken, SyntaxKind, T,
ast::{
self, HasDocComments, HasGenericParams, HasName, HasVisibility, edit::AstNodeEdit, make,
},
syntax_editor::{Position, SyntaxEditor},
};
@@ -45,7 +47,7 @@
// };
// }
//
// trait ${0:NewTrait}<const N: usize> {
// trait ${0:Create}<const N: usize> {
// // Used as an associated constant.
// const CONST_ASSOC: usize = N * 4;
//
@@ -54,7 +56,7 @@
// const_maker! {i32, 7}
// }
//
// impl<const N: usize> ${0:NewTrait}<N> for Foo<N> {
// impl<const N: usize> ${0:Create}<N> for Foo<N> {
// // Used as an associated constant.
// const CONST_ASSOC: usize = N * 4;
//
@@ -107,7 +109,7 @@ pub(crate) fn generate_trait_from_impl(acc: &mut Assists, ctx: &AssistContext<'_
};
let trait_ast = make::trait_(
false,
"NewTrait",
&trait_name(&impl_assoc_items).text(),
impl_ast.generic_param_list(),
impl_ast.where_clause(),
trait_items,
@@ -133,6 +135,7 @@ pub(crate) fn generate_trait_from_impl(acc: &mut Assists, ctx: &AssistContext<'_
let mut editor = builder.make_editor(impl_ast.syntax());
impl_assoc_items.assoc_items().for_each(|item| {
remove_items_visibility(&mut editor, &item);
remove_doc_comments(&mut editor, &item);
});
editor.insert_all(Position::before(impl_name.syntax()), elements);
@@ -160,6 +163,18 @@ pub(crate) fn generate_trait_from_impl(acc: &mut Assists, ctx: &AssistContext<'_
Some(())
}
fn trait_name(items: &ast::AssocItemList) -> ast::Name {
let mut fn_names = items
.assoc_items()
.filter_map(|x| if let ast::AssocItem::Fn(f) = x { f.name() } else { None });
fn_names
.next()
.and_then(|name| {
fn_names.next().is_none().then(|| make::name(&stdx::to_camel_case(&name.text())))
})
.unwrap_or_else(|| make::name("NewTrait"))
}
/// `E0449` Trait items always share the visibility of their trait
fn remove_items_visibility(editor: &mut SyntaxEditor, item: &ast::AssocItem) {
if let Some(has_vis) = ast::AnyHasVisibility::cast(item.syntax().clone()) {
@@ -175,6 +190,17 @@ fn remove_items_visibility(editor: &mut SyntaxEditor, item: &ast::AssocItem) {
}
}
fn remove_doc_comments(editor: &mut SyntaxEditor, item: &ast::AssocItem) {
for doc in item.doc_comments() {
if let Some(next) = doc.syntax().next_token()
&& next.kind() == SyntaxKind::WHITESPACE
{
editor.delete(next);
}
editor.delete(doc.syntax());
}
}
fn strip_body(editor: &mut SyntaxEditor, item: &ast::AssocItem) {
if let ast::AssocItem::Fn(f) = item
&& let Some(body) = f.body()
@@ -226,11 +252,47 @@ fn add(&mut self, x: f64) {
r#"
struct Foo(f64);
trait NewTrait {
trait Add {
fn add(&mut self, x: f64);
}
impl NewTrait for Foo {
impl Add for Foo {
fn add(&mut self, x: f64) {
self.0 += x;
}
}"#,
)
}
#[test]
fn test_remove_doc_comments() {
check_assist_no_snippet_cap(
generate_trait_from_impl,
r#"
struct Foo(f64);
impl F$0oo {
/// Add `x`
///
/// # Examples
#[cfg(true)]
fn add(&mut self, x: f64) {
self.0 += x;
}
}"#,
r#"
struct Foo(f64);
trait Add {
/// Add `x`
///
/// # Examples
#[cfg(true)]
fn add(&mut self, x: f64);
}
impl Add for Foo {
#[cfg(true)]
fn add(&mut self, x: f64) {
self.0 += x;
}
@@ -339,11 +401,11 @@ pub fn a_func() -> Option<()> {
r#"
struct Foo;
trait NewTrait {
trait AFunc {
fn a_func() -> Option<()>;
}
impl NewTrait for Foo {
impl AFunc for Foo {
fn a_func() -> Option<()> {
Some(())
}
@@ -373,17 +435,39 @@ fn foo() {}
}"#,
r#"
mod a {
trait NewTrait {
trait Foo {
fn foo();
}
impl NewTrait for S {
impl Foo for S {
fn foo() {}
}
}"#,
)
}
#[test]
fn test_multi_fn_impl_not_suggest_trait_name() {
check_assist_no_snippet_cap(
generate_trait_from_impl,
r#"
impl S$0 {
fn foo() {}
fn bar() {}
}"#,
r#"
trait NewTrait {
fn foo();
fn bar();
}
impl NewTrait for S {
fn foo() {}
fn bar() {}
}"#,
)
}
#[test]
fn test_snippet_cap_is_some() {
check_assist(
@@ -1,3 +1,4 @@
use either::{Either, for_both};
use hir::{PathResolution, Semantics};
use ide_db::{
EditionedFileId, RootDatabase,
@@ -5,8 +6,9 @@
search::{FileReference, FileReferenceNode, UsageSearchResult},
};
use syntax::{
SyntaxElement, TextRange,
Direction, TextRange,
ast::{self, AstNode, AstToken, HasName, syntax_factory::SyntaxFactory},
syntax_editor::{Element, SyntaxEditor},
};
use crate::{
@@ -36,12 +38,15 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_>)
let InlineData { let_stmt, delete_let, references, target } =
if let Some(path_expr) = ctx.find_node_at_offset::<ast::PathExpr>() {
inline_usage(&ctx.sema, path_expr, range, file_id)
} else if let Some(let_stmt) = ctx.find_node_at_offset::<ast::LetStmt>() {
} else if let Some(let_stmt) = ctx.find_node_at_offset() {
inline_let(&ctx.sema, let_stmt, range, file_id)
} else {
None
}?;
let initializer_expr = let_stmt.initializer()?;
let initializer_expr = match &let_stmt {
either::Either::Left(it) => it.initializer()?,
either::Either::Right(it) => it.expr()?,
};
let wrap_in_parens = references
.into_iter()
@@ -81,13 +86,15 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_>)
let mut editor = builder.make_editor(&target);
if delete_let {
editor.delete(let_stmt.syntax());
if let Some(whitespace) = let_stmt
.syntax()
.next_sibling_or_token()
.and_then(SyntaxElement::into_token)
.and_then(ast::Whitespace::cast)
if let Some(bin_expr) = let_stmt.syntax().parent().and_then(ast::BinExpr::cast)
&& let Some(op_token) = bin_expr.op_token()
{
editor.delete(whitespace.syntax());
editor.delete(&op_token);
remove_whitespace(op_token, Direction::Prev, &mut editor);
remove_whitespace(let_stmt.syntax(), Direction::Prev, &mut editor);
} else {
remove_whitespace(let_stmt.syntax(), Direction::Next, &mut editor);
}
}
@@ -116,7 +123,7 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_>)
}
struct InlineData {
let_stmt: ast::LetStmt,
let_stmt: Either<ast::LetStmt, ast::LetExpr>,
delete_let: bool,
target: ast::NameOrNameRef,
references: Vec<FileReference>,
@@ -124,11 +131,11 @@ struct InlineData {
fn inline_let(
sema: &Semantics<'_, RootDatabase>,
let_stmt: ast::LetStmt,
let_stmt: Either<ast::LetStmt, ast::LetExpr>,
range: TextRange,
file_id: EditionedFileId,
) -> Option<InlineData> {
let bind_pat = match let_stmt.pat()? {
let bind_pat = match for_both!(&let_stmt, it => it.pat())? {
ast::Pat::IdentPat(pat) => pat,
_ => return None,
};
@@ -187,7 +194,7 @@ fn inline_usage(
let bind_pat = source.as_ident_pat()?;
let let_stmt = ast::LetStmt::cast(bind_pat.syntax().parent()?)?;
let let_stmt = AstNode::cast(bind_pat.syntax().parent()?)?;
let UsageSearchResult { mut references } = Definition::Local(local).usages(sema).all();
let mut references = references.remove(&file_id)?;
@@ -197,6 +204,23 @@ fn inline_usage(
Some(InlineData { let_stmt, delete_let, target: ast::NameOrNameRef::NameRef(name), references })
}
fn remove_whitespace(elem: impl Element, dir: Direction, editor: &mut SyntaxEditor) {
let token = match elem.syntax_element() {
syntax::NodeOrToken::Node(node) => match dir {
Direction::Next => node.last_token(),
Direction::Prev => node.first_token(),
},
syntax::NodeOrToken::Token(t) => Some(t),
};
let next_token = match dir {
Direction::Next => token.and_then(|it| it.next_token()),
Direction::Prev => token.and_then(|it| it.prev_token()),
};
if let Some(whitespace) = next_token.and_then(ast::Whitespace::cast) {
editor.delete(whitespace.syntax());
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
@@ -404,6 +428,38 @@ fn foo() {
);
}
#[test]
fn test_inline_let_expr() {
check_assist(
inline_local_variable,
r"
fn bar(a: usize) {}
fn foo() {
if let a$0 = 1
&& true
{
a + 1;
if a > 10 {}
while a > 10 {}
let b = a * 10;
bar(a);
}
}",
r"
fn bar(a: usize) {}
fn foo() {
if true
{
1 + 1;
if 1 > 10 {}
while 1 > 10 {}
let b = 1 * 10;
bar(1);
}
}",
);
}
#[test]
fn test_not_inline_mut_variable() {
cov_mark::check!(test_not_inline_mut_variable);
@@ -816,6 +872,70 @@ fn f() {
);
}
#[test]
fn let_expr_works_on_local_usage() {
check_assist(
inline_local_variable,
r#"
fn f() {
if let xyz = 0
&& true
{
xyz$0;
}
}
"#,
r#"
fn f() {
if true
{
0;
}
}
"#,
);
check_assist(
inline_local_variable,
r#"
fn f() {
if let xyz = true
&& xyz$0
{
}
}
"#,
r#"
fn f() {
if true
{
}
}
"#,
);
check_assist(
inline_local_variable,
r#"
fn f() {
if true
&& let xyz = 0
{
xyz$0;
}
}
"#,
r#"
fn f() {
if true
{
0;
}
}
"#,
);
}
#[test]
fn does_not_remove_let_when_multiple_usages() {
check_assist(
@@ -12,7 +12,7 @@
use syntax::ast::syntax_factory::SyntaxFactory;
use syntax::syntax_editor::SyntaxEditor;
use syntax::{
AstNode, NodeOrToken, SyntaxNode,
AstNode, NodeOrToken, SyntaxKind, SyntaxNode, T,
ast::{self, HasGenericParams, HasName},
};
@@ -322,12 +322,42 @@ fn create_replacement(
if let Some(old_lifetime) = ast::Lifetime::cast(syntax.clone()) {
if let Some(new_lifetime) = lifetime_map.0.get(&old_lifetime.to_string()) {
if new_lifetime.text() == "'_" {
removals.push(NodeOrToken::Node(syntax.clone()));
// Check if this lifetime is inside a LifetimeArg (in angle brackets)
if let Some(lifetime_arg) =
old_lifetime.syntax().parent().and_then(ast::LifetimeArg::cast)
{
// Remove LifetimeArg and associated comma/whitespace
let lifetime_arg_syntax = lifetime_arg.syntax();
removals.push(NodeOrToken::Node(lifetime_arg_syntax.clone()));
if let Some(ws) = syntax.next_sibling_or_token() {
removals.push(ws.clone());
// Remove comma and whitespace (look forward then backward)
let comma_and_ws: Vec<_> = lifetime_arg_syntax
.siblings_with_tokens(syntax::Direction::Next)
.skip(1)
.take_while(|it| it.as_token().is_some())
.take_while_inclusive(|it| it.kind() == T![,])
.collect();
if comma_and_ws.iter().any(|it| it.kind() == T![,]) {
removals.extend(comma_and_ws);
} else {
// No comma after, try before
let comma_and_ws: Vec<_> = lifetime_arg_syntax
.siblings_with_tokens(syntax::Direction::Prev)
.skip(1)
.take_while(|it| it.as_token().is_some())
.take_while_inclusive(|it| it.kind() == T![,])
.collect();
removals.extend(comma_and_ws);
}
continue;
}
removals.push(NodeOrToken::Node(syntax.clone()));
if let Some(ws) = syntax.next_sibling_or_token()
&& ws.kind() == SyntaxKind::WHITESPACE
{
removals.push(ws);
}
continue;
}
@@ -349,6 +379,34 @@ fn create_replacement(
}
}
// Deduplicate removals to avoid intersecting changes
removals.sort_by_key(|n| n.text_range().start());
removals.dedup();
// Remove GenericArgList entirely if all its args are being removed (avoids empty angle brackets)
let generic_arg_lists_to_check: Vec<_> =
updated_concrete_type.descendants().filter_map(ast::GenericArgList::cast).collect();
for generic_arg_list in generic_arg_lists_to_check {
let will_be_empty = generic_arg_list.generic_args().all(|arg| match arg {
ast::GenericArg::LifetimeArg(lt_arg) => removals.iter().any(|removal| {
if let NodeOrToken::Node(node) = removal { node == lt_arg.syntax() } else { false }
}),
_ => false,
});
if will_be_empty && generic_arg_list.generic_args().next().is_some() {
removals.retain(|removal| {
if let NodeOrToken::Node(node) = removal {
!node.ancestors().any(|anc| anc == *generic_arg_list.syntax())
} else {
true
}
});
removals.push(NodeOrToken::Node(generic_arg_list.syntax().clone()));
}
}
for (old, new) in replacements {
editor.replace(old, new);
}
@@ -946,6 +1004,48 @@ trait Tr {
);
}
#[test]
fn inline_types_with_lifetime() {
check_assist(
inline_type_alias_uses,
r#"
struct A<'a, 'b>(pub &'a mut &'b mut ());
type $0T<'a, 'b> = A<'a, 'b>;
fn foo(_: T) {}
"#,
r#"
struct A<'a, 'b>(pub &'a mut &'b mut ());
fn foo(_: A) {}
"#,
);
}
#[test]
fn mixed_lifetime_and_type_args() {
check_assist(
inline_type_alias,
r#"
type Foo<'a, T> = Bar<'a, T>;
struct Bar<'a, T>(&'a T);
fn main() {
let a: $0Foo<u32>;
}
"#,
r#"
type Foo<'a, T> = Bar<'a, T>;
struct Bar<'a, T>(&'a T);
fn main() {
let a: Bar<u32>;
}
"#,
);
}
mod inline_type_alias_uses {
use crate::{handlers::inline_type_alias::inline_type_alias_uses, tests::check_assist};
@@ -1,13 +1,13 @@
use ide_db::syntax_helpers::node_ext::is_pattern_cond;
use syntax::{
T,
ast::{self, AstNode},
ast::{self, AstNode, syntax_factory::SyntaxFactory},
};
use crate::{
AssistId,
assist_context::{AssistContext, Assists},
utils::invert_boolean_expression_legacy,
utils::invert_boolean_expression,
};
// Assist: invert_if
@@ -50,7 +50,8 @@ pub(crate) fn invert_if(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()
};
acc.add(AssistId::refactor_rewrite("invert_if"), "Invert if", if_range, |edit| {
let flip_cond = invert_boolean_expression_legacy(cond.clone());
let make = SyntaxFactory::without_mappings();
let flip_cond = invert_boolean_expression(&make, cond.clone());
edit.replace_ast(cond, flip_cond);
let else_node = else_block.syntax();
@@ -49,8 +49,9 @@ pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext<'_>) -> Optio
SyntaxElement::Node(n) => n,
SyntaxElement::Token(t) => t.parent()?,
};
let mut selected_nodes =
parent_node.children().filter(|it| selection_range.contains_range(it.text_range()));
let mut selected_nodes = parent_node.children().filter(|it| {
selection_range.intersect(it.text_range()).is_some_and(|it| !it.is_empty())
});
let first_selected = selected_nodes.next()?;
let edits = match_ast! {
@@ -677,6 +678,25 @@ fn merge_selection_uses() {
);
}
#[test]
fn merge_partial_selection_uses() {
cov_mark::check!(merge_with_selected_use_item_neighbors);
check_assist(
merge_imports,
r"
use std::fmt::Error;
$0use std::fmt::Display;
use std::fmt::Debug;
use std::fmt::Write;
use$0 std::fmt::Result;
",
r"
use std::fmt::Error;
use std::fmt::{Debug, Display, Result, Write};
",
);
}
#[test]
fn merge_selection_use_trees() {
cov_mark::check!(merge_with_selected_use_tree_neighbors);
@@ -3,7 +3,7 @@
SyntaxKind::WHITESPACE,
TextRange,
ast::{
AstNode, BlockExpr, ElseBranch, Expr, IfExpr, MatchArm, Pat, edit::AstNodeEdit, make,
AstNode, BlockExpr, ElseBranch, Expr, IfExpr, MatchArm, Pat, edit::AstNodeEdit,
prec::ExprPrecedence, syntax_factory::SyntaxFactory,
},
syntax_editor::Element,
@@ -53,14 +53,15 @@ pub(crate) fn move_guard_to_arm_body(acc: &mut Assists, ctx: &AssistContext<'_>)
let space_after_arrow = match_arm.fat_arrow_token()?.next_sibling_or_token();
let arm_expr = match_arm.expr()?;
let make = SyntaxFactory::without_mappings();
let if_branch = chain([&match_arm], &rest_arms)
.rfold(None, |else_branch, arm| {
if let Some(guard) = arm.guard() {
let then_branch = crate::utils::wrap_block(&arm.expr()?);
let then_branch = crate::utils::wrap_block(&arm.expr()?, &make);
let guard_condition = guard.condition()?.reset_indent();
Some(make::expr_if(guard_condition, then_branch, else_branch).into())
Some(make.expr_if(guard_condition, then_branch, else_branch).into())
} else {
arm.expr().map(|it| crate::utils::wrap_block(&it).into())
arm.expr().map(|it| crate::utils::wrap_block(&it, &make).into())
}
})?
.indent(arm_expr.indent_level());
@@ -84,7 +85,7 @@ pub(crate) fn move_guard_to_arm_body(acc: &mut Assists, ctx: &AssistContext<'_>)
if let Some(element) = space_after_arrow
&& element.kind() == WHITESPACE
{
edit.replace(element, make::tokens::single_space());
edit.replace(element, make.whitespace(" "));
}
edit.delete(guard.syntax());
@@ -1,6 +1,6 @@
use hir::{AsAssocItem, AssocItem, AssocItemContainer, ItemInNs, ModuleDef, db::HirDatabase};
use ide_db::assists::AssistId;
use syntax::{AstNode, ast};
use syntax::{AstNode, ast, ast::syntax_factory::SyntaxFactory};
use crate::{
assist_context::{AssistContext, Assists},
@@ -52,19 +52,25 @@ pub(crate) fn qualify_method_call(acc: &mut Assists, ctx: &AssistContext<'_>) ->
cfg,
)?;
let qualify_candidate = QualifyCandidate::ImplMethod(ctx.sema.db, call, resolved_call);
let qualify_candidate = QualifyCandidate::ImplMethod(ctx.sema.db, call.clone(), resolved_call);
acc.add(
AssistId::refactor_rewrite("qualify_method_call"),
format!("Qualify `{ident}` method call"),
range,
|builder| {
let make = SyntaxFactory::with_mappings();
let mut editor = builder.make_editor(call.syntax());
qualify_candidate.qualify(
|replace_with: String| builder.replace(range, replace_with),
|_| {},
&mut editor,
&make,
&receiver_path,
item_in_ns,
current_edition,
)
);
editor.add_mappings(make.finish_with_mappings());
builder.add_file_edits(ctx.vfs_file_id(), editor);
},
);
Some(())
@@ -11,7 +11,8 @@
use syntax::ast::HasGenericArgs;
use syntax::{
AstNode, ast,
ast::{HasArgList, make},
ast::{HasArgList, syntax_factory::SyntaxFactory},
syntax_editor::SyntaxEditor,
};
use crate::{
@@ -54,25 +55,25 @@ pub(crate) fn qualify_path(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
let qualify_candidate = match candidate {
ImportCandidate::Path(candidate) if !candidate.qualifier.is_empty() => {
cov_mark::hit!(qualify_path_qualifier_start);
let path = ast::Path::cast(syntax_under_caret)?;
let path = ast::Path::cast(syntax_under_caret.clone())?;
let (prev_segment, segment) = (path.qualifier()?.segment()?, path.segment()?);
QualifyCandidate::QualifierStart(segment, prev_segment.generic_arg_list())
}
ImportCandidate::Path(_) => {
cov_mark::hit!(qualify_path_unqualified_name);
let path = ast::Path::cast(syntax_under_caret)?;
let path = ast::Path::cast(syntax_under_caret.clone())?;
let generics = path.segment()?.generic_arg_list();
QualifyCandidate::UnqualifiedName(generics)
}
ImportCandidate::TraitAssocItem(_) => {
cov_mark::hit!(qualify_path_trait_assoc_item);
let path = ast::Path::cast(syntax_under_caret)?;
let path = ast::Path::cast(syntax_under_caret.clone())?;
let (qualifier, segment) = (path.qualifier()?, path.segment()?);
QualifyCandidate::TraitAssocItem(qualifier, segment)
}
ImportCandidate::TraitMethod(_) => {
cov_mark::hit!(qualify_path_trait_method);
let mcall_expr = ast::MethodCallExpr::cast(syntax_under_caret)?;
let mcall_expr = ast::MethodCallExpr::cast(syntax_under_caret.clone())?;
QualifyCandidate::TraitMethod(ctx.sema.db, mcall_expr)
}
};
@@ -101,12 +102,18 @@ pub(crate) fn qualify_path(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
label(ctx.db(), candidate, &import, current_edition),
range,
|builder| {
let make = SyntaxFactory::with_mappings();
let mut editor = builder.make_editor(&syntax_under_caret);
qualify_candidate.qualify(
|replace_with: String| builder.replace(range, replace_with),
&mut editor,
&make,
&import.import_path,
import.item_to_import,
current_edition,
)
);
editor.add_mappings(make.finish_with_mappings());
builder.add_file_edits(ctx.vfs_file_id(), editor);
},
);
}
@@ -124,6 +131,8 @@ impl QualifyCandidate<'_> {
pub(crate) fn qualify(
&self,
mut replacer: impl FnMut(String),
editor: &mut SyntaxEditor,
make: &SyntaxFactory,
import: &hir::ModPath,
item: hir::ItemInNs,
edition: Edition,
@@ -142,10 +151,10 @@ pub(crate) fn qualify(
replacer(format!("<{qualifier} as {import}>::{segment}"));
}
QualifyCandidate::TraitMethod(db, mcall_expr) => {
Self::qualify_trait_method(db, mcall_expr, replacer, import, item);
Self::qualify_trait_method(db, mcall_expr, editor, make, import, item);
}
QualifyCandidate::ImplMethod(db, mcall_expr, hir_fn) => {
Self::qualify_fn_call(db, mcall_expr, replacer, import, hir_fn);
Self::qualify_fn_call(db, mcall_expr, editor, make, import, hir_fn);
}
}
}
@@ -153,7 +162,8 @@ pub(crate) fn qualify(
fn qualify_fn_call(
db: &RootDatabase,
mcall_expr: &ast::MethodCallExpr,
mut replacer: impl FnMut(String),
editor: &mut SyntaxEditor,
make: &SyntaxFactory,
import: ast::Path,
hir_fn: &hir::Function,
) -> Option<()> {
@@ -165,15 +175,17 @@ fn qualify_fn_call(
if let Some(self_access) = hir_fn.self_param(db).map(|sp| sp.access(db)) {
let receiver = match self_access {
hir::Access::Shared => make::expr_ref(receiver, false),
hir::Access::Exclusive => make::expr_ref(receiver, true),
hir::Access::Shared => make.expr_ref(receiver, false),
hir::Access::Exclusive => make.expr_ref(receiver, true),
hir::Access::Owned => receiver,
};
let arg_list = match arg_list {
Some(args) => make::arg_list(iter::once(receiver).chain(args)),
None => make::arg_list(iter::once(receiver)),
Some(args) => make.arg_list(iter::once(receiver).chain(args)),
None => make.arg_list(iter::once(receiver)),
};
replacer(format!("{import}::{method_name}{generics}{arg_list}"));
let call_path = make.path_from_text(&format!("{import}::{method_name}{generics}"));
let call_expr = make.expr_call(make.expr_path(call_path), arg_list);
editor.replace(mcall_expr.syntax(), call_expr.syntax());
}
Some(())
}
@@ -181,14 +193,15 @@ fn qualify_fn_call(
fn qualify_trait_method(
db: &RootDatabase,
mcall_expr: &ast::MethodCallExpr,
replacer: impl FnMut(String),
editor: &mut SyntaxEditor,
make: &SyntaxFactory,
import: ast::Path,
item: hir::ItemInNs,
) -> Option<()> {
let trait_method_name = mcall_expr.name_ref()?;
let trait_ = item_as_trait(db, item)?;
let method = find_trait_method(db, trait_, &trait_method_name)?;
Self::qualify_fn_call(db, mcall_expr, replacer, import, &method)
Self::qualify_fn_call(db, mcall_expr, editor, make, import, &method)
}
}
@@ -1,8 +1,11 @@
use either::Either;
use ide_db::syntax_helpers::suggest_name;
use syntax::ast::{self, AstNode, HasArgList, syntax_factory::SyntaxFactory};
use syntax::ast::{self, AstNode, HasArgList, prec::ExprPrecedence, syntax_factory::SyntaxFactory};
use crate::{AssistContext, AssistId, Assists, utils::cover_let_chain};
use crate::{
AssistContext, AssistId, Assists,
utils::{cover_let_chain, wrap_paren, wrap_paren_in_call},
};
// Assist: replace_is_some_with_if_let_some
//
@@ -39,6 +42,7 @@ pub(crate) fn replace_is_method_with_if_let_method(
match method_kind {
"is_some" | "is_ok" => {
let receiver = call_expr.receiver()?;
let make = SyntaxFactory::with_mappings();
let mut name_generator = suggest_name::NameGenerator::new_from_scope_locals(
ctx.sema.scope(has_cond.syntax()),
@@ -48,7 +52,7 @@ pub(crate) fn replace_is_method_with_if_let_method(
} else {
name_generator.for_variable(&receiver, &ctx.sema)
};
let (pat, predicate) = method_predicate(&call_expr).unzip();
let (pat, predicate) = method_predicate(&call_expr, &var_name, &make);
let (assist_id, message, text) = if method_kind == "is_some" {
("replace_is_some_with_if_let_some", "Replace `is_some` with `let Some`", "Some")
@@ -61,13 +65,9 @@ pub(crate) fn replace_is_method_with_if_let_method(
message,
call_expr.syntax().text_range(),
|edit| {
let make = SyntaxFactory::with_mappings();
let mut editor = edit.make_editor(call_expr.syntax());
let var_pat = pat.unwrap_or_else(|| {
make.ident_pat(false, false, make.name(&var_name)).into()
});
let pat = make.tuple_struct_pat(make.ident_path(text), [var_pat]).into();
let pat = make.tuple_struct_pat(make.ident_path(text), [pat]).into();
let let_expr = make.expr_let(pat, receiver);
if let Some(cap) = ctx.config.snippet_cap
@@ -81,6 +81,7 @@ pub(crate) fn replace_is_method_with_if_let_method(
let new_expr = if let Some(predicate) = predicate {
let op = ast::BinaryOp::LogicOp(ast::LogicOp::And);
let predicate = wrap_paren(predicate, &make, ExprPrecedence::LAnd);
make.expr_bin(let_expr.into(), op, predicate).into()
} else {
ast::Expr::from(let_expr)
@@ -96,14 +97,23 @@ pub(crate) fn replace_is_method_with_if_let_method(
}
}
fn method_predicate(call_expr: &ast::MethodCallExpr) -> Option<(ast::Pat, ast::Expr)> {
let argument = call_expr.arg_list()?.args().next()?;
match argument {
ast::Expr::ClosureExpr(it) => {
let pat = it.param_list()?.params().next()?.pat()?;
Some((pat, it.body()?))
}
_ => None,
fn method_predicate(
call_expr: &ast::MethodCallExpr,
name: &str,
make: &SyntaxFactory,
) -> (ast::Pat, Option<ast::Expr>) {
let argument = call_expr.arg_list().and_then(|it| it.args().next());
if let Some(ast::Expr::ClosureExpr(it)) = argument.clone()
&& let Some(pat) = it.param_list().and_then(|it| it.params().next()?.pat())
{
(pat, it.body())
} else {
let pat = make.ident_pat(false, false, make.name(name));
let expr = argument.map(|expr| {
let arg_list = make.arg_list([make.expr_path(make.ident_path(name))]);
make.expr_call(wrap_paren_in_call(expr, make), arg_list).into()
});
(pat.into(), expr)
}
}
@@ -232,6 +242,54 @@ fn main() {
let x = Some(1);
if let Some(it) = x && it != 3 {}
}
"#,
);
check_assist(
replace_is_method_with_if_let_method,
r#"
fn main() {
let x = Some(1);
if x.is_som$0e_and(|it| it != 3 || it > 10) {}
}
"#,
r#"
fn main() {
let x = Some(1);
if let Some(it) = x && (it != 3 || it > 10) {}
}
"#,
);
check_assist(
replace_is_method_with_if_let_method,
r#"
fn main() {
let x = Some(1);
if x.is_som$0e_and(predicate) {}
}
"#,
r#"
fn main() {
let x = Some(1);
if let Some(x1) = x && predicate(x1) {}
}
"#,
);
check_assist(
replace_is_method_with_if_let_method,
r#"
fn main() {
let x = Some(1);
if x.is_som$0e_and(func.f) {}
}
"#,
r#"
fn main() {
let x = Some(1);
if let Some(x1) = x && (func.f)(x1) {}
}
"#,
);
}
@@ -86,8 +86,8 @@ pub(crate) fn replace_let_with_if_let(acc: &mut Assists, ctx: &AssistContext<'_>
}
fn let_expr_needs_paren(expr: &ast::Expr) -> bool {
let fake_expr_let =
ast::make::expr_let(ast::make::tuple_pat(None).into(), ast::make::ext::expr_unit());
let make = SyntaxFactory::without_mappings();
let fake_expr_let = make.expr_let(make.tuple_pat(None).into(), make.expr_unit());
let Some(fake_expr) = fake_expr_let.expr() else {
stdx::never!();
return false;
@@ -2,10 +2,10 @@
use ide_db::{RootDatabase, assists::AssistId, defs::Definition};
use syntax::{
AstNode,
ast::{self, Expr, HasArgList, make},
ast::{self, Expr, HasArgList, make, syntax_factory::SyntaxFactory},
};
use crate::{AssistContext, Assists};
use crate::{AssistContext, Assists, utils::wrap_paren_in_call};
// Assist: replace_with_lazy_method
//
@@ -177,11 +177,7 @@ fn into_call(param: &Expr, sema: &Semantics<'_, RootDatabase>) -> Expr {
}
})()
.unwrap_or_else(|| {
let callable = if needs_parens_in_call(param) {
make::expr_paren(param.clone()).into()
} else {
param.clone()
};
let callable = wrap_paren_in_call(param.clone(), &SyntaxFactory::without_mappings());
make::expr_call(callable, make::arg_list(Vec::new())).into()
})
}
@@ -200,12 +196,6 @@ fn ends_is(name: &str, end: &str) -> bool {
name.strip_suffix(end).is_some_and(|s| s.is_empty() || s.ends_with('_'))
}
fn needs_parens_in_call(param: &Expr) -> bool {
let call = make::expr_call(make::ext::expr_unit(), make::arg_list(Vec::new()));
let callable = call.expr().expect("invalid make call");
param.needs_parens_in_place_of(call.syntax(), callable.syntax())
}
#[cfg(test)]
mod tests {
use crate::tests::check_assist;
@@ -45,6 +45,7 @@ pub(crate) fn unwrap_block(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
ast::LoopExpr(it) => it.syntax().clone(),
ast::WhileExpr(it) => it.syntax().clone(),
ast::MatchArm(it) => it.parent_match().syntax().clone(),
ast::LetElse(it) => it.syntax().parent()?,
ast::LetStmt(it) => {
replacement = wrap_let(&it, replacement);
prefer_container = Some(it.syntax().clone());
@@ -556,6 +557,40 @@ fn main() {
);
}
#[test]
fn simple_let_else() {
check_assist(
unwrap_block,
r#"
fn main() {
let Some(2) = None else {$0
return;
};
}
"#,
r#"
fn main() {
return;
}
"#,
);
check_assist(
unwrap_block,
r#"
fn main() {
let Some(2) = None else {$0
return
};
}
"#,
r#"
fn main() {
return
}
"#,
);
}
#[test]
fn unwrap_match_arm() {
check_assist(
@@ -1,3 +1,6 @@
use std::iter;
use either::Either;
use syntax::{
AstNode, T,
ast::{self, edit::AstNodeEdit},
@@ -24,11 +27,16 @@
// ```
pub(crate) fn unwrap_tuple(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let let_kw = ctx.find_token_syntax_at_offset(T![let])?;
let let_stmt = let_kw.parent().and_then(ast::LetStmt::cast)?;
let indent_level = let_stmt.indent_level().0 as usize;
let pat = let_stmt.pat()?;
let ty = let_stmt.ty();
let init = let_stmt.initializer()?;
let let_stmt = let_kw.parent().and_then(Either::<ast::LetStmt, ast::LetExpr>::cast)?;
let mut indent_level = let_stmt.indent_level();
let pat = either::for_both!(&let_stmt, it => it.pat())?;
let (ty, init, prefix, suffix) = match &let_stmt {
Either::Left(let_stmt) => (let_stmt.ty(), let_stmt.initializer()?, "", ";"),
Either::Right(let_expr) => {
indent_level += 1;
(None, let_expr.expr()?, "&& ", "")
}
};
// This only applies for tuple patterns, types, and initializers.
let tuple_pat = match pat {
@@ -60,25 +68,19 @@ pub(crate) fn unwrap_tuple(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
"Unwrap tuple",
let_kw.text_range(),
|edit| {
let indents = " ".repeat(indent_level);
let mut decls = String::new();
// If there is an ascribed type, insert that type for each declaration,
// otherwise, omit that type.
if let Some(tys) = tuple_ty {
let mut zipped_decls = String::new();
for (pat, ty, expr) in
itertools::izip!(tuple_pat.fields(), tys.fields(), tuple_init.fields())
{
zipped_decls.push_str(&format!("{indents}let {pat}: {ty} = {expr};\n"))
}
edit.replace(parent.text_range(), zipped_decls.trim());
} else {
let mut zipped_decls = String::new();
for (pat, expr) in itertools::izip!(tuple_pat.fields(), tuple_init.fields()) {
zipped_decls.push_str(&format!("{indents}let {pat} = {expr};\n"));
}
edit.replace(parent.text_range(), zipped_decls.trim());
let tys =
tuple_ty.into_iter().flat_map(|it| it.fields().map(Some)).chain(iter::repeat(None));
for (pat, ty, expr) in itertools::izip!(tuple_pat.fields(), tys, tuple_init.fields()) {
let ty = ty.map_or_else(String::new, |ty| format!(": {ty}"));
decls.push_str(&format!("{prefix}let {pat}{ty} = {expr}{suffix}\n{indent_level}"))
}
let s = decls.trim();
edit.replace(parent.text_range(), s.strip_prefix(prefix).unwrap_or(s));
},
)
}
@@ -123,6 +125,28 @@ fn main() {
);
}
#[test]
fn unwrap_tuples_in_let_expr() {
check_assist(
unwrap_tuple,
r#"
fn main() {
if $0let (foo, bar) = ("Foo", "Bar") {
code();
}
}
"#,
r#"
fn main() {
if let foo = "Foo"
&& let bar = "Bar" {
code();
}
}
"#,
);
}
#[test]
fn unwrap_tuple_with_types() {
check_assist(
@@ -2,7 +2,7 @@
use itertools::Itertools;
use syntax::{
NodeOrToken, SyntaxToken, T, TextRange, algo,
ast::{self, AstNode, make, syntax_factory::SyntaxFactory},
ast::{self, AstNode, edit::AstNodeEdit, make, syntax_factory::SyntaxFactory},
};
use crate::{AssistContext, AssistId, Assists};
@@ -27,7 +27,7 @@
enum WrapUnwrapOption {
WrapDerive { derive: TextRange, attr: ast::Attr },
WrapAttr(ast::Attr),
WrapAttr(Vec<ast::Attr>),
}
/// Attempts to get the derive attribute from a derive attribute list
@@ -102,9 +102,9 @@ fn attempt_get_derive(attr: ast::Attr, ident: SyntaxToken) -> WrapUnwrapOption {
if ident.parent().and_then(ast::TokenTree::cast).is_none()
|| !attr.simple_name().map(|v| v.eq("derive")).unwrap_or_default()
{
WrapUnwrapOption::WrapAttr(attr)
WrapUnwrapOption::WrapAttr(vec![attr])
} else {
attempt_attr().unwrap_or(WrapUnwrapOption::WrapAttr(attr))
attempt_attr().unwrap_or_else(|| WrapUnwrapOption::WrapAttr(vec![attr]))
}
}
pub(crate) fn wrap_unwrap_cfg_attr(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
@@ -118,13 +118,27 @@ pub(crate) fn wrap_unwrap_cfg_attr(acc: &mut Assists, ctx: &AssistContext<'_>) -
Some(attempt_get_derive(attr, ident))
}
(Some(attr), _) => Some(WrapUnwrapOption::WrapAttr(attr)),
(Some(attr), _) => Some(WrapUnwrapOption::WrapAttr(vec![attr])),
_ => None,
}
} else {
let covering_element = ctx.covering_element();
match covering_element {
NodeOrToken::Node(node) => ast::Attr::cast(node).map(WrapUnwrapOption::WrapAttr),
NodeOrToken::Node(node) => {
if let Some(attr) = ast::Attr::cast(node.clone()) {
Some(WrapUnwrapOption::WrapAttr(vec![attr]))
} else {
let attrs = node
.children()
.filter(|it| it.text_range().intersect(ctx.selection_trimmed()).is_some())
.map(ast::Attr::cast)
.collect::<Option<Vec<_>>>()?;
if attrs.is_empty() {
return None;
}
Some(WrapUnwrapOption::WrapAttr(attrs))
}
}
NodeOrToken::Token(ident) if ident.kind() == syntax::T![ident] => {
let attr = ident.parent_ancestors().find_map(ast::Attr::cast)?;
Some(attempt_get_derive(attr, ident))
@@ -133,10 +147,12 @@ pub(crate) fn wrap_unwrap_cfg_attr(acc: &mut Assists, ctx: &AssistContext<'_>) -
}
}?;
match option {
WrapUnwrapOption::WrapAttr(attr) if attr.simple_name().as_deref() == Some("cfg_attr") => {
unwrap_cfg_attr(acc, attr)
}
WrapUnwrapOption::WrapAttr(attr) => wrap_cfg_attr(acc, ctx, attr),
WrapUnwrapOption::WrapAttr(attrs) => match &attrs[..] {
[attr] if attr.simple_name().as_deref() == Some("cfg_attr") => {
unwrap_cfg_attr(acc, attrs.into_iter().next().unwrap())
}
_ => wrap_cfg_attrs(acc, ctx, attrs),
},
WrapUnwrapOption::WrapDerive { derive, attr } => wrap_derive(acc, ctx, attr, derive),
}
}
@@ -220,40 +236,51 @@ fn wrap_derive(
);
Some(())
}
fn wrap_cfg_attr(acc: &mut Assists, ctx: &AssistContext<'_>, attr: ast::Attr) -> Option<()> {
let range = attr.syntax().text_range();
let path = attr.path()?;
fn wrap_cfg_attrs(acc: &mut Assists, ctx: &AssistContext<'_>, attrs: Vec<ast::Attr>) -> Option<()> {
let (first_attr, last_attr) = (attrs.first()?, attrs.last()?);
let range = first_attr.syntax().text_range().cover(last_attr.syntax().text_range());
let path_attrs =
attrs.iter().map(|attr| Some((attr.path()?, attr.clone()))).collect::<Option<Vec<_>>>()?;
let handle_source_change = |edit: &mut SourceChangeBuilder| {
let make = SyntaxFactory::with_mappings();
let mut editor = edit.make_editor(attr.syntax());
let mut raw_tokens =
vec![NodeOrToken::Token(make.token(T![,])), NodeOrToken::Token(make.whitespace(" "))];
path.syntax().descendants_with_tokens().for_each(|it| {
if let NodeOrToken::Token(token) = it {
raw_tokens.push(NodeOrToken::Token(token));
}
});
if let Some(meta) = attr.meta() {
if let (Some(eq), Some(expr)) = (meta.eq_token(), meta.expr()) {
raw_tokens.push(NodeOrToken::Token(make.whitespace(" ")));
raw_tokens.push(NodeOrToken::Token(eq));
raw_tokens.push(NodeOrToken::Token(make.whitespace(" ")));
let mut editor = edit.make_editor(first_attr.syntax());
let mut raw_tokens = vec![];
for (path, attr) in path_attrs {
raw_tokens.extend([
NodeOrToken::Token(make.token(T![,])),
NodeOrToken::Token(make.whitespace(" ")),
]);
path.syntax().descendants_with_tokens().for_each(|it| {
if let NodeOrToken::Token(token) = it {
raw_tokens.push(NodeOrToken::Token(token));
}
});
if let Some(meta) = attr.meta() {
if let (Some(eq), Some(expr)) = (meta.eq_token(), meta.expr()) {
raw_tokens.push(NodeOrToken::Token(make.whitespace(" ")));
raw_tokens.push(NodeOrToken::Token(eq));
raw_tokens.push(NodeOrToken::Token(make.whitespace(" ")));
expr.syntax().descendants_with_tokens().for_each(|it| {
if let NodeOrToken::Token(token) = it {
raw_tokens.push(NodeOrToken::Token(token));
}
});
} else if let Some(tt) = meta.token_tree() {
raw_tokens.extend(tt.token_trees_and_tokens());
expr.syntax().descendants_with_tokens().for_each(|it| {
if let NodeOrToken::Token(token) = it {
raw_tokens.push(NodeOrToken::Token(token));
}
});
} else if let Some(tt) = meta.token_tree() {
raw_tokens.extend(tt.token_trees_and_tokens());
}
}
}
let meta =
make.meta_token_tree(make.ident_path("cfg_attr"), make.token_tree(T!['('], raw_tokens));
let cfg_attr =
if attr.excl_token().is_some() { make.attr_inner(meta) } else { make.attr_outer(meta) };
let cfg_attr = if first_attr.excl_token().is_some() {
make.attr_inner(meta)
} else {
make.attr_outer(meta)
};
editor.replace(attr.syntax(), cfg_attr.syntax());
let syntax_range = first_attr.syntax().clone().into()..=last_attr.syntax().clone().into();
editor.replace_all(syntax_range, vec![cfg_attr.syntax().clone().into()]);
if let Some(snippet_cap) = ctx.config.snippet_cap
&& let Some(first_meta) =
@@ -332,7 +359,8 @@ fn unwrap_cfg_attr(acc: &mut Assists, attr: ast::Attr) -> Option<()> {
return None;
}
let handle_source_change = |f: &mut SourceChangeBuilder| {
let inner_attrs = inner_attrs.iter().map(|it| it.to_string()).join("\n");
let inner_attrs =
inner_attrs.iter().map(|it| it.to_string()).join(&format!("\n{}", attr.indent_level()));
f.replace(range, inner_attrs);
};
acc.add(
@@ -414,6 +442,42 @@ pub struct Test {
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
pub struct Test {
#[other_attr]
$0#[foo]
#[bar]$0
#[other_attr]
test: u32,
}
"#,
r#"
pub struct Test {
#[other_attr]
#[cfg_attr($0, foo, bar)]
#[other_attr]
test: u32,
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
pub struct Test {
#[cfg_attr(debug_assertions$0, foo, bar)]
test: u32,
}
"#,
r#"
pub struct Test {
#[foo]
#[bar]
test: u32,
}
"#,
);
}
#[test]
fn to_from_eq_attr() {
@@ -2282,7 +2282,7 @@ macro_rules! const_maker {
};
}
trait ${0:NewTrait}<const N: usize> {
trait ${0:Create}<const N: usize> {
// Used as an associated constant.
const CONST_ASSOC: usize = N * 4;
@@ -2291,7 +2291,7 @@ trait ${0:NewTrait}<const N: usize> {
const_maker! {i32, 7}
}
impl<const N: usize> ${0:NewTrait}<N> for Foo<N> {
impl<const N: usize> ${0:Create}<N> for Foo<N> {
// Used as an associated constant.
const CONST_ASSOC: usize = N * 4;
@@ -25,6 +25,7 @@
edit::{AstNodeEdit, IndentLevel},
edit_in_place::AttrsOwnerEdit,
make,
prec::ExprPrecedence,
syntax_factory::SyntaxFactory,
},
syntax_editor::{Element, Removable, SyntaxEditor},
@@ -86,17 +87,31 @@ pub fn extract_trivial_expression(block_expr: &ast::BlockExpr) -> Option<ast::Ex
None
}
pub(crate) fn wrap_block(expr: &ast::Expr) -> ast::BlockExpr {
pub(crate) fn wrap_block(expr: &ast::Expr, make: &SyntaxFactory) -> ast::BlockExpr {
if let ast::Expr::BlockExpr(block) = expr
&& let Some(first) = block.syntax().first_token()
&& first.kind() == T!['{']
{
block.reset_indent()
} else {
make::block_expr(None, Some(expr.reset_indent().indent(1.into())))
make.block_expr(None, Some(expr.reset_indent().indent(1.into())))
}
}
pub(crate) fn wrap_paren(expr: ast::Expr, make: &SyntaxFactory, prec: ExprPrecedence) -> ast::Expr {
if expr.precedence().needs_parentheses_in(prec) { make.expr_paren(expr).into() } else { expr }
}
pub(crate) fn wrap_paren_in_call(expr: ast::Expr, make: &SyntaxFactory) -> ast::Expr {
if needs_parens_in_call(&expr) { make.expr_paren(expr).into() } else { expr }
}
fn needs_parens_in_call(param: &ast::Expr) -> bool {
let call = make::expr_call(make::ext::expr_unit(), make::arg_list(Vec::new()));
let callable = call.expr().expect("invalid make call");
param.needs_parens_in_place_of(call.syntax(), callable.syntax())
}
/// This is a method with a heuristics to support test methods annotated with custom test annotations, such as
/// `#[test_case(...)]`, `#[tokio::test]` and similar.
/// Also a regular `#[test]` annotation is supported.
@@ -275,11 +290,6 @@ pub(crate) fn invert_boolean_expression(make: &SyntaxFactory, expr: ast::Expr) -
invert_special_case(make, &expr).unwrap_or_else(|| make.expr_prefix(T![!], expr).into())
}
// FIXME: Migrate usages of this function to the above function and remove this.
pub(crate) fn invert_boolean_expression_legacy(expr: ast::Expr) -> ast::Expr {
invert_special_case_legacy(&expr).unwrap_or_else(|| make::expr_prefix(T![!], expr).into())
}
fn invert_special_case(make: &SyntaxFactory, expr: &ast::Expr) -> Option<ast::Expr> {
match expr {
ast::Expr::BinExpr(bin) => {
@@ -343,62 +353,11 @@ fn invert_special_case(make: &SyntaxFactory, expr: &ast::Expr) -> Option<ast::Ex
}
}
fn invert_special_case_legacy(expr: &ast::Expr) -> Option<ast::Expr> {
match expr {
ast::Expr::BinExpr(bin) => {
let bin = bin.clone_subtree();
let op_token = bin.op_token()?;
let rev_token = match op_token.kind() {
T![==] => T![!=],
T![!=] => T![==],
T![<] => T![>=],
T![<=] => T![>],
T![>] => T![<=],
T![>=] => T![<],
// Parenthesize other expressions before prefixing `!`
_ => {
return Some(
make::expr_prefix(T![!], make::expr_paren(expr.clone()).into()).into(),
);
}
};
let mut bin_editor = SyntaxEditor::new(bin.syntax().clone());
bin_editor.replace(op_token, make::token(rev_token));
ast::Expr::cast(bin_editor.finish().new_root().clone())
}
ast::Expr::MethodCallExpr(mce) => {
let receiver = mce.receiver()?;
let method = mce.name_ref()?;
let arg_list = mce.arg_list()?;
let method = match method.text().as_str() {
"is_some" => "is_none",
"is_none" => "is_some",
"is_ok" => "is_err",
"is_err" => "is_ok",
_ => return None,
};
Some(make::expr_method_call(receiver, make::name_ref(method), arg_list).into())
}
ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::UnaryOp::Not => match pe.expr()? {
ast::Expr::ParenExpr(parexpr) => parexpr.expr(),
_ => pe.expr(),
},
ast::Expr::Literal(lit) => match lit.kind() {
ast::LiteralKind::Bool(b) => match b {
true => Some(ast::Expr::Literal(make::expr_literal("false"))),
false => Some(ast::Expr::Literal(make::expr_literal("true"))),
},
_ => None,
},
_ => None,
}
}
pub(crate) fn insert_attributes(
before: impl Element,
edit: &mut SyntaxEditor,
attrs: impl IntoIterator<Item = ast::Attr>,
make: &SyntaxFactory,
) {
let mut attrs = attrs.into_iter().peekable();
if attrs.peek().is_none() {
@@ -410,9 +369,7 @@ pub(crate) fn insert_attributes(
edit.insert_all(
syntax::syntax_editor::Position::before(elem),
attrs
.flat_map(|attr| {
[attr.syntax().clone().into(), make::tokens::whitespace(&whitespace).into()]
})
.flat_map(|attr| [attr.syntax().clone().into(), make.whitespace(&whitespace).into()])
.collect(),
);
}
@@ -1095,18 +1052,21 @@ pub(crate) fn trimmed_text_range(source_file: &SourceFile, initial_range: TextRa
/// Convert a list of function params to a list of arguments that can be passed
/// into a function call.
pub(crate) fn convert_param_list_to_arg_list(list: ast::ParamList) -> ast::ArgList {
pub(crate) fn convert_param_list_to_arg_list(
list: ast::ParamList,
make: &SyntaxFactory,
) -> ast::ArgList {
let mut args = vec![];
for param in list.params() {
if let Some(ast::Pat::IdentPat(pat)) = param.pat()
&& let Some(name) = pat.name()
{
let name = name.to_string();
let expr = make::expr_path(make::ext::ident_path(&name));
let expr = make.expr_path(make.ident_path(&name));
args.push(expr);
}
}
make::arg_list(args)
make.arg_list(args)
}
/// Calculate the number of hashes required for a raw string containing `s`
@@ -1191,7 +1151,10 @@ pub(crate) fn replace_record_field_expr(
/// Creates a token tree list from a syntax node, creating the needed delimited sub token trees.
/// Assumes that the input syntax node is a valid syntax tree.
pub(crate) fn tt_from_syntax(node: SyntaxNode) -> Vec<NodeOrToken<ast::TokenTree, SyntaxToken>> {
pub(crate) fn tt_from_syntax(
node: SyntaxNode,
make: &SyntaxFactory,
) -> Vec<NodeOrToken<ast::TokenTree, SyntaxToken>> {
let mut tt_stack = vec![(None, vec![])];
for element in node.descendants_with_tokens() {
@@ -1219,7 +1182,7 @@ pub(crate) fn tt_from_syntax(node: SyntaxNode) -> Vec<NodeOrToken<ast::TokenTree
"mismatched opening and closing delimiters"
);
let sub_tt = make::token_tree(delimiter.expect("unbalanced delimiters"), tt);
let sub_tt = make.token_tree(delimiter.expect("unbalanced delimiters"), tt);
parent_tt.push(NodeOrToken::Node(sub_tt));
}
_ => {
@@ -1254,6 +1217,20 @@ pub(crate) fn cover_let_chain(mut expr: ast::Expr, range: TextRange) -> Option<a
}
}
pub(crate) fn cover_edit_range(
source: &impl AstNode,
range: TextRange,
) -> std::ops::RangeInclusive<syntax::SyntaxElement> {
let node = match source.syntax().covering_element(range) {
NodeOrToken::Node(node) => node,
NodeOrToken::Token(t) => t.parent().unwrap(),
};
let mut iter = node.children_with_tokens().filter(|it| range.contains_range(it.text_range()));
let first = iter.next().unwrap_or(node.into());
let last = iter.last().unwrap_or_else(|| first.clone());
first..=last
}
pub(crate) fn is_selected(
it: &impl AstNode,
selection: syntax::TextRange,
@@ -5,7 +5,7 @@
//! based on the parent of the existing expression.
use syntax::{
AstNode, T,
ast::{self, FieldExpr, MethodCallExpr, make, syntax_factory::SyntaxFactory},
ast::{self, FieldExpr, MethodCallExpr, syntax_factory::SyntaxFactory},
};
use crate::AssistContext;
@@ -119,13 +119,13 @@ pub(crate) struct RefData {
impl RefData {
/// Derefs `expr` and wraps it in parens if necessary
pub(crate) fn wrap_expr(&self, mut expr: ast::Expr) -> ast::Expr {
pub(crate) fn wrap_expr(&self, mut expr: ast::Expr, make: &SyntaxFactory) -> ast::Expr {
if self.needs_deref {
expr = make::expr_prefix(T![*], expr).into();
expr = make.expr_prefix(T![*], expr).into();
}
if self.needs_parentheses {
expr = make::expr_paren(expr).into();
expr = make.expr_paren(expr).into();
}
expr
@@ -30,14 +30,27 @@ pub(crate) fn complete_fn_param(
_ => return None,
};
let qualifier = param_qualifier(param);
let comma_wrapper = comma_wrapper(ctx);
let mut add_new_item_to_acc = |label: &str| {
let mk_item = |label: &str, range: TextRange| {
CompletionItem::new(CompletionItemKind::Binding, range, label, ctx.edition)
let label = label.strip_prefix(qualifier.as_str()).unwrap_or(label);
let insert = if label.starts_with('#') {
// FIXME: `#[attr] it: i32` -> `#[attr] mut it: i32`
label.to_smolstr()
} else {
format_smolstr!("{qualifier}{label}")
};
let mk_item = |insert_text: &str, range: TextRange| {
let mut item =
CompletionItem::new(CompletionItemKind::Binding, range, label, ctx.edition);
if insert_text != label {
item.insert_text(insert_text);
}
item
};
let item = match &comma_wrapper {
Some((fmt, range)) => mk_item(&fmt(label), *range),
None => mk_item(label, ctx.source_range()),
Some((fmt, range)) => mk_item(&fmt(&insert), *range),
None => mk_item(&insert, ctx.source_range()),
};
// Completion lookup is omitted intentionally here.
// See the full discussion: https://github.com/rust-lang/rust-analyzer/issues/12073
@@ -75,9 +88,6 @@ fn fill_fn_params(
let mut file_params = FxHashMap::default();
let mut extract_params = |f: ast::Fn| {
if !is_simple_param(current_param) {
return;
}
f.param_list().into_iter().flat_map(|it| it.params()).for_each(|param| {
if let Some(pat) = param.pat() {
let whole_param = param.to_smolstr();
@@ -88,6 +98,9 @@ fn fill_fn_params(
};
for node in ctx.token.parent_ancestors() {
if !is_simple_param(current_param) {
break;
}
match_ast! {
match node {
ast::SourceFile(it) => it.items().filter_map(|item| match item {
@@ -214,3 +227,16 @@ fn is_simple_param(param: &ast::Param) -> bool {
.pat()
.is_none_or(|pat| matches!(pat, ast::Pat::IdentPat(ident_pat) if ident_pat.pat().is_none()))
}
fn param_qualifier(param: &ast::Param) -> SmolStr {
let mut b = syntax::SmolStrBuilder::new();
if let Some(ast::Pat::IdentPat(pat)) = param.pat() {
if pat.ref_token().is_some() {
b.push_str("ref ");
}
if pat.mut_token().is_some() {
b.push_str("mut ");
}
}
b.finish()
}
@@ -16,7 +16,7 @@
use stdx::never;
use syntax::{
SmolStr,
SyntaxKind::{EXPR_STMT, STMT_LIST},
SyntaxKind::{CLOSURE_EXPR, EXPR_STMT, MATCH_ARM, STMT_LIST},
T, TextRange, TextSize, ToSmolStr,
ast::{self, AstNode, AstToken},
format_smolstr, match_ast,
@@ -66,6 +66,12 @@ pub(crate) fn complete_postfix(
Some(it) => it,
None => return,
};
let semi =
if expr_ctx.in_block_expr && ctx.token.next_token().is_none_or(|it| it.kind() != T![;]) {
";"
} else {
""
};
let cfg = ctx.config.find_path_config(ctx.is_nightly);
@@ -151,12 +157,12 @@ pub(crate) fn complete_postfix(
.add_to(acc, ctx.db);
}
_ if matches!(parent.kind(), STMT_LIST | EXPR_STMT) => {
postfix_snippet("let", "let", &format!("let $0 = {receiver_text};"))
postfix_snippet("let", "let", &format!("let $0 = {receiver_text}{semi}"))
.add_to(acc, ctx.db);
postfix_snippet("letm", "let mut", &format!("let mut $0 = {receiver_text};"))
postfix_snippet("letm", "let mut", &format!("let mut $0 = {receiver_text}{semi}"))
.add_to(acc, ctx.db);
}
_ if ast::MatchArm::can_cast(parent.kind()) => {
_ if matches!(parent.kind(), MATCH_ARM | CLOSURE_EXPR) => {
postfix_snippet(
"let",
"let",
@@ -307,26 +313,12 @@ pub(crate) fn complete_postfix(
add_format_like_completions(acc, ctx, &dot_receiver_including_refs, cap, &literal_text);
}
postfix_snippet(
"return",
"return expr",
&format!(
"return {receiver_text}{semi}",
semi = if expr_ctx.in_block_expr { ";" } else { "" }
),
)
.add_to(acc, ctx.db);
postfix_snippet("return", "return expr", &format!("return {receiver_text}{semi}"))
.add_to(acc, ctx.db);
if let Some(BreakableKind::Block | BreakableKind::Loop) = expr_ctx.in_breakable {
postfix_snippet(
"break",
"break expr",
&format!(
"break {receiver_text}{semi}",
semi = if expr_ctx.in_block_expr { ";" } else { "" }
),
)
.add_to(acc, ctx.db);
postfix_snippet("break", "break expr", &format!("break {receiver_text}{semi}"))
.add_to(acc, ctx.db);
}
}
@@ -371,12 +363,20 @@ fn get_receiver_text(
range.range = TextRange::at(range.range.start(), range.range.len() - TextSize::of('.'))
}
let file_text = sema.db.file_text(range.file_id.file_id(sema.db));
let mut text = file_text.text(sema.db)[range.range].to_owned();
let text = file_text.text(sema.db);
let indent_spaces = indent_of_tail_line(&text[TextRange::up_to(range.range.start())]);
let mut text = stdx::dedent_by(indent_spaces, &text[range.range]);
// The receiver texts should be interpreted as-is, as they are expected to be
// normal Rust expressions.
escape_snippet_bits(&mut text);
text
return text;
fn indent_of_tail_line(text: &str) -> usize {
let tail_line = text.rsplit_once('\n').map_or(text, |(_, s)| s);
let trimmed = tail_line.trim_start_matches(' ');
tail_line.len() - trimmed.len()
}
}
/// Escapes `\` and `$` so that they don't get interpreted as snippet-specific constructs.
@@ -402,6 +402,10 @@ fn receiver_accessor(receiver: &ast::Expr) -> ast::Expr {
.unwrap_or_else(|| receiver.clone())
}
/// Given an `initial_element`, tries to expand it to include deref(s), and then references.
/// Returns the expanded expressions, and the added prefix as a string
///
/// For example, if called with the `42` in `&&mut *42`, would return `(&&mut *42, "&&mut *")`.
fn include_references(initial_element: &ast::Expr) -> (ast::Expr, String) {
let mut resulting_element = initial_element.clone();
let mut prefix = String::new();
@@ -410,11 +414,8 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, String) {
while let Some(parent_deref_element) =
resulting_element.syntax().parent().and_then(ast::PrefixExpr::cast)
&& parent_deref_element.op_kind() == Some(ast::UnaryOp::Deref)
{
if parent_deref_element.op_kind() != Some(ast::UnaryOp::Deref) {
break;
}
found_ref_or_deref = true;
resulting_element = ast::Expr::from(parent_deref_element);
@@ -663,6 +664,22 @@ fn main() {
#[test]
fn let_middle_block() {
check_edit(
"let",
r#"
fn main() {
baz.l$0
res
}
"#,
r#"
fn main() {
let $0 = baz;
res
}
"#,
);
check(
r#"
fn main() {
@@ -719,6 +736,20 @@ fn main() {
#[test]
fn let_tail_block() {
check_edit(
"let",
r#"
fn main() {
baz.l$0
}
"#,
r#"
fn main() {
let $0 = baz;
}
"#,
);
check(
r#"
fn main() {
@@ -772,6 +803,23 @@ fn main() {
);
}
#[test]
fn let_before_semicolon() {
check_edit(
"let",
r#"
fn main() {
baz.l$0;
}
"#,
r#"
fn main() {
let $0 = baz;
}
"#,
);
}
#[test]
fn option_iflet() {
check_edit(
@@ -965,6 +1013,28 @@ fn main() {
);
}
#[test]
fn closure_let_block() {
check_edit(
"let",
r#"
fn main() {
let bar = 2;
let f = || bar.$0;
}
"#,
r#"
fn main() {
let bar = 2;
let f = || {
let $1 = bar;
$0
};
}
"#,
);
}
#[test]
fn option_letelse() {
check_edit(
@@ -1040,6 +1110,7 @@ fn main() {
#[test]
fn postfix_completion_for_references() {
check_edit("dbg", r#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#);
check_edit("dbg", r#"fn main() { &&*"hello".$0 }"#, r#"fn main() { dbg!(&&*"hello") }"#);
check_edit("refm", r#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#);
check_edit(
"ifl",
@@ -1198,9 +1269,9 @@ fn main() {
fn main() {
ControlFlow::Break(match true {
true => "\${1:placeholder}",
false => "\\\$",
})
true => "\${1:placeholder}",
false => "\\\$",
})
}
"#,
);
@@ -1440,4 +1511,31 @@ fn foo() {
"#,
);
}
#[test]
fn snippet_dedent() {
check_edit(
"let",
r#"
//- minicore: option
fn foo(x: Option<i32>, y: Option<i32>) {
let _f = || {
x
.and(y)
.map(|it| it+2)
.$0
};
}
"#,
r#"
fn foo(x: Option<i32>, y: Option<i32>) {
let _f = || {
let $0 = x
.and(y)
.map(|it| it+2);
};
}
"#,
);
}
}
@@ -778,6 +778,16 @@ fn expected_type_and_name<'db>(
let ty = sema.type_of_pat(&ast::Pat::from(it)).map(TypeInfo::original);
(ty, None)
},
ast::TupleStructPat(it) => {
let fields = it.path().and_then(|path| match sema.resolve_path(&path)? {
hir::PathResolution::Def(hir::ModuleDef::Adt(adt)) => Some(adt.as_struct()?.fields(sema.db)),
hir::PathResolution::Def(hir::ModuleDef::Variant(variant)) => Some(variant.fields(sema.db)),
_ => None,
});
let nr = it.fields().take_while(|it| it.syntax().text_range().end() <= token.text_range().start()).count();
let ty = fields.and_then(|fields| Some(fields.get(nr)?.ty(sema.db).to_type(sema.db)));
(ty, None)
},
ast::Fn(it) => {
cov_mark::hit!(expected_type_fn_ret_with_leading_char);
cov_mark::hit!(expected_type_fn_ret_without_leading_char);
@@ -944,10 +954,10 @@ fn classify_name_ref<'db>(
let field_expr_handle = |receiver, node| {
let receiver = find_opt_node_in_file(original_file, receiver);
let receiver_is_ambiguous_float_literal = match &receiver {
Some(ast::Expr::Literal(l)) => matches! {
l.kind(),
ast::LiteralKind::FloatNumber { .. } if l.syntax().last_token().is_some_and(|it| it.text().ends_with('.'))
},
Some(ast::Expr::Literal(l)) => {
matches!(l.kind(), ast::LiteralKind::FloatNumber { .. })
&& l.syntax().last_token().is_some_and(|it| it.text().ends_with('.'))
}
_ => false,
};
@@ -287,6 +287,50 @@ fn foo() -> Foo {
);
}
#[test]
fn expected_type_tuple_struct_pat() {
check_expected_type_and_name(
r#"
//- minicore: option
struct Foo(Option<i32>);
fn foo(x: Foo) -> Foo {
match x { Foo($0) => () }
}
"#,
expect![[r#"ty: Option<i32>, name: ?"#]],
);
check_expected_type_and_name(
r#"
struct Foo(i32, u32, f32);
fn foo(x: Foo) -> Foo {
match x { Foo($0) => () }
}
"#,
expect![[r#"ty: i32, name: ?"#]],
);
check_expected_type_and_name(
r#"
struct Foo(i32, u32, f32);
fn foo(x: Foo) -> Foo {
match x { Foo(num,$0) => () }
}
"#,
expect![[r#"ty: u32, name: ?"#]],
);
check_expected_type_and_name(
r#"
struct Foo(i32, u32, f32);
fn foo(x: Foo) -> Foo {
match x { Foo(num,$0,float) => () }
}
"#,
expect![[r#"ty: u32, name: ?"#]],
);
}
#[test]
fn expected_type_if_let_without_leading_char() {
cov_mark::check!(expected_type_if_let_without_leading_char);
@@ -678,7 +678,7 @@ fn main() {
fn complete_fn_param() {
// has mut kw
check_edit(
"mut bar: u32",
"bar: u32",
r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), mut ba$0)
@@ -689,10 +689,35 @@ fn g(foo: (), mut bar: u32)
"#,
);
// has type param
// has unmatched mut kw
check_edit(
"bar: u32",
r#"
fn f(foo: (), bar: u32) {}
fn g(foo: (), mut ba$0)
"#,
r#"
fn f(foo: (), bar: u32) {}
fn g(foo: (), mut bar: u32)
"#,
);
check_edit(
"mut bar: u32",
r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), ba$0)
"#,
r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), mut bar: u32)
"#,
);
// has type param
check_edit(
"bar: u32",
r#"
fn g(foo: (), mut ba$0: u32)
fn f(foo: (), mut bar: u32) {}
"#,
@@ -707,7 +732,7 @@ fn f(foo: (), mut bar: u32) {}
fn complete_fn_mut_param_add_comma() {
// add leading and trailing comma
check_edit(
", mut bar: u32,",
"bar: u32",
r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: ()mut ba$0 baz: ())
@@ -746,7 +771,7 @@ fn g(foo: (), #[baz = "qux"] mut bar: u32)
);
check_edit(
r#", #[baz = "qux"] mut bar: u32"#,
r#"#[baz = "qux"] mut bar: u32"#,
r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: ()#[baz = "qux"] mut ba$0)
@@ -43,7 +43,7 @@ fn bar(file_id: usize) {}
fn baz(file$0 id: u32) {}
"#,
expect![[r#"
bn file_id: usize,
bn file_id: usize
kw mut
kw ref
"#]],
@@ -97,25 +97,37 @@ fn missing_record_expr_field_fixes(
make::ty(&new_field_type.display_source_code(sema.db, module.into(), true).ok()?),
);
let last_field = record_fields.fields().last()?;
let last_field_syntax = last_field.syntax();
let indent = IndentLevel::from_node(last_field_syntax);
let (indent, offset, postfix, needs_comma) =
if let Some(last_field) = record_fields.fields().last() {
let indent = IndentLevel::from_node(last_field.syntax());
let offset = last_field.syntax().text_range().end();
let needs_comma = !last_field.to_string().ends_with(',');
(indent, offset, String::new(), needs_comma)
} else {
let indent = IndentLevel::from_node(record_fields.syntax());
let offset = record_fields.l_curly_token()?.text_range().end();
let postfix = if record_fields.syntax().text().contains_char('\n') {
",".into()
} else {
format!(",\n{indent}")
};
(indent + 1, offset, postfix, false)
};
let mut new_field = new_field.to_string();
// FIXME: check submodule instead of FileId
if usage_file_id != def_file_id && !matches!(def_id, hir::VariantDef::Variant(_)) {
new_field = format!("pub(crate) {new_field}");
}
new_field = format!("\n{indent}{new_field}");
new_field = format!("\n{indent}{new_field}{postfix}");
let needs_comma = !last_field_syntax.to_string().ends_with(',');
if needs_comma {
new_field = format!(",{new_field}");
}
let source_change = SourceChange::from_text_edit(
def_file_id.file_id(sema.db),
TextEdit::insert(last_field_syntax.text_range().end(), new_field),
TextEdit::insert(offset, new_field),
);
return Some(vec![fix(
@@ -334,6 +346,44 @@ struct Foo {
)
}
#[test]
fn test_add_field_from_usage_with_empty_struct() {
check_fix(
r"
fn main() {
Foo { bar$0: false };
}
struct Foo {}
",
r"
fn main() {
Foo { bar: false };
}
struct Foo {
bar: bool,
}
",
);
check_fix(
r"
fn main() {
Foo { bar$0: false };
}
struct Foo {
}
",
r"
fn main() {
Foo { bar: false };
}
struct Foo {
bar: bool,
}
",
);
}
#[test]
fn test_add_field_in_other_file_from_usage() {
check_fix(
@@ -1,4 +1,11 @@
use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext};
use either::Either;
use hir::Semantics;
use ide_db::text_edit::TextEdit;
use ide_db::ty_filter::TryEnum;
use ide_db::{RootDatabase, source_change::SourceChange};
use syntax::{AstNode, ast};
use crate::{Assist, Diagnostic, DiagnosticCode, DiagnosticsContext, fix};
// Diagnostic: non-exhaustive-let
//
@@ -15,11 +22,74 @@ pub(crate) fn non_exhaustive_let(
d.pat.map(Into::into),
)
.stable()
.with_fixes(fixes(&ctx.sema, d))
}
fn fixes(sema: &Semantics<'_, RootDatabase>, d: &hir::NonExhaustiveLet) -> Option<Vec<Assist>> {
let root = sema.parse_or_expand(d.pat.file_id);
let pat = d.pat.value.to_node(&root);
let let_stmt = ast::LetStmt::cast(pat.syntax().parent()?)?;
let early_node =
sema.ancestors_with_macros(let_stmt.syntax().clone()).find_map(AstNode::cast)?;
let early_text = early_text(sema, &early_node);
if let_stmt.let_else().is_some() {
return None;
}
let hir::FileRangeWrapper { file_id, range } = sema.original_range_opt(let_stmt.syntax())?;
let insert_offset = if let Some(semicolon) = let_stmt.semicolon_token()
&& let Some(token) = sema.parse(file_id).syntax().token_at_offset(range.end()).left_biased()
&& token.kind() == semicolon.kind()
{
token.text_range().start()
} else {
range.end()
};
let semicolon = if let_stmt.semicolon_token().is_none() { ";" } else { "" };
let else_block = format!(" else {{ {early_text} }}{semicolon}");
let file_id = file_id.file_id(sema.db);
let source_change =
SourceChange::from_text_edit(file_id, TextEdit::insert(insert_offset, else_block));
let target = sema.original_range(let_stmt.syntax()).range;
Some(vec![fix("add_let_else_block", "Add let-else block", source_change, target)])
}
fn early_text(
sema: &Semantics<'_, RootDatabase>,
early_node: &Either<ast::AnyHasLoopBody, Either<ast::Fn, ast::ClosureExpr>>,
) -> &'static str {
match early_node {
Either::Left(_any_loop) => "continue",
Either::Right(Either::Left(fn_)) => sema
.to_def(fn_)
.map(|fn_def| fn_def.ret_type(sema.db))
.map(|ty| return_text(&ty, sema))
.unwrap_or("return"),
Either::Right(Either::Right(closure)) => closure
.body()
.and_then(|expr| sema.type_of_expr(&expr))
.map(|ty| return_text(&ty.adjusted(), sema))
.unwrap_or("return"),
}
}
fn return_text(ty: &hir::Type<'_>, sema: &Semantics<'_, RootDatabase>) -> &'static str {
if ty.is_unit() {
"return"
} else if let Some(try_enum) = TryEnum::from_ty(sema, ty) {
match try_enum {
TryEnum::Option => "return None",
TryEnum::Result => "return Err($0)",
}
} else {
"return $0"
}
}
#[cfg(test)]
mod tests {
use crate::tests::check_diagnostics;
use crate::tests::{check_diagnostics, check_fix};
#[test]
fn option_nonexhaustive() {
@@ -28,7 +98,7 @@ fn option_nonexhaustive() {
//- minicore: option
fn main() {
let None = Some(5);
//^^^^ error: non-exhaustive pattern: `Some(_)` not covered
//^^^^ 💡 error: non-exhaustive pattern: `Some(_)` not covered
}
"#,
);
@@ -54,7 +124,7 @@ fn option_nonexhaustive_inside_blocks() {
fn main() {
'_a: {
let None = Some(5);
//^^^^ error: non-exhaustive pattern: `Some(_)` not covered
//^^^^ 💡 error: non-exhaustive pattern: `Some(_)` not covered
}
}
"#,
@@ -66,7 +136,7 @@ fn main() {
fn main() {
let _ = async {
let None = Some(5);
//^^^^ error: non-exhaustive pattern: `Some(_)` not covered
//^^^^ 💡 error: non-exhaustive pattern: `Some(_)` not covered
};
}
"#,
@@ -78,7 +148,7 @@ fn main() {
fn main() {
unsafe {
let None = Some(5);
//^^^^ error: non-exhaustive pattern: `Some(_)` not covered
//^^^^ 💡 error: non-exhaustive pattern: `Some(_)` not covered
}
}
"#,
@@ -101,7 +171,7 @@ fn test(x: Result<i32, !>) {
//- minicore: result
fn test(x: Result<i32, &'static !>) {
let Ok(_y) = x;
//^^^^^^ error: non-exhaustive pattern: `Err(_)` not covered
//^^^^^^ 💡 error: non-exhaustive pattern: `Err(_)` not covered
}
"#,
);
@@ -132,6 +202,136 @@ fn foo(v: Enum<()>) {
);
}
#[test]
fn fix_return_in_loop() {
check_fix(
r#"
//- minicore: option
fn foo() {
while cond {
let None$0 = Some(5);
}
}
"#,
r#"
fn foo() {
while cond {
let None = Some(5) else { continue };
}
}
"#,
);
}
#[test]
fn fix_return_in_fn() {
check_fix(
r#"
//- minicore: option
fn foo() {
let None$0 = Some(5);
}
"#,
r#"
fn foo() {
let None = Some(5) else { return };
}
"#,
);
}
#[test]
fn fix_return_in_macro_expanded() {
check_fix(
r#"
//- minicore: option
macro_rules! identity { ($($t:tt)*) => { $($t)* }; }
fn foo() {
identity! {
let None$0 = Some(5);
}
}
"#,
r#"
macro_rules! identity { ($($t:tt)*) => { $($t)* }; }
fn foo() {
identity! {
let None = Some(5) else { return };
}
}
"#,
);
}
#[test]
fn fix_return_in_incomplete_let() {
check_fix(
r#"
//- minicore: option
fn foo() {
let None$0 = Some(5)
}
"#,
r#"
fn foo() {
let None = Some(5) else { return };
}
"#,
);
}
#[test]
fn fix_return_in_closure() {
check_fix(
r#"
//- minicore: option
fn foo() -> Option<()> {
let _f = || {
let None$0 = Some(5);
};
}
"#,
r#"
fn foo() -> Option<()> {
let _f = || {
let None = Some(5) else { return };
};
}
"#,
);
}
#[test]
fn fix_return_try_in_fn() {
check_fix(
r#"
//- minicore: option
fn foo() -> Option<()> {
let None$0 = Some(5);
}
"#,
r#"
fn foo() -> Option<()> {
let None = Some(5) else { return None };
}
"#,
);
check_fix(
r#"
//- minicore: option, result
fn foo() -> Result<(), i32> {
let None$0 = Some(5);
}
"#,
r#"
fn foo() -> Result<(), i32> {
let None = Some(5) else { return Err($0) };
}
"#,
);
}
#[test]
fn regression_20259() {
check_diagnostics(
@@ -48,7 +48,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &RemoveUnnecessaryElse) -> Option<Vec<
let mut indent = IndentLevel::from_node(if_expr.syntax());
let has_parent_if_expr = if_expr.syntax().parent().and_then(ast::IfExpr::cast).is_some();
if has_parent_if_expr {
indent = indent + 1;
indent += 1;
}
let else_replacement = match if_expr.else_branch()? {
ast::ElseBranch::Block(block) => block
@@ -42,6 +42,7 @@ pub struct LoadCargoConfig {
pub load_out_dirs_from_check: bool,
pub with_proc_macro_server: ProcMacroServerChoice,
pub prefill_caches: bool,
pub num_worker_threads: usize,
pub proc_macro_processes: usize,
}
@@ -197,7 +198,7 @@ pub fn load_workspace_into_db(
);
if load_config.prefill_caches {
prime_caches::parallel_prime_caches(db, 1, &|_| ());
prime_caches::parallel_prime_caches(db, load_config.num_worker_threads, &|_| ());
}
Ok((vfs, proc_macro_server.and_then(Result::ok)))
@@ -744,16 +745,26 @@ mod tests {
#[test]
fn test_loading_rust_analyzer() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
let cargo_toml_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("Cargo.toml");
let cargo_toml_path = AbsPathBuf::assert_utf8(cargo_toml_path);
let manifest = ProjectManifest::from_manifest_file(cargo_toml_path).unwrap();
let cargo_config = CargoConfig { set_test: true, ..CargoConfig::default() };
let load_cargo_config = LoadCargoConfig {
load_out_dirs_from_check: false,
with_proc_macro_server: ProcMacroServerChoice::None,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let workspace = ProjectWorkspace::load(manifest, &cargo_config, &|_| {}).unwrap();
let (db, _vfs, _proc_macro) =
load_workspace_at(path, &cargo_config, &load_cargo_config, &|_| {}).unwrap();
load_workspace(workspace, &cargo_config.extra_env, &load_cargo_config).unwrap();
let n_crates = db.all_crates().len();
// RA has quite a few crates, but the exact count doesn't matter
@@ -365,9 +365,27 @@ pub enum RunnableKind {
/// May include {test_id} which will get the test clicked on by the user.
TestOne,
/// Run tests matching a pattern (in RA, usually a path::to::module::of::tests)
/// May include {label} which will get the label from the `build` section of a crate.
/// May include {test_pattern} which will get the test module clicked on by the user.
TestMod,
/// Run a single doctest
/// May include {label} which will get the label from the `build` section of a crate.
/// May include {test_id} which will get the doctest clicked on by the user.
DocTestOne,
/// Run a single benchmark
/// May include {label} which will get the label from the `build` section of a crate.
/// May include {bench_id} which will get the benchmark clicked on by the user.
BenchOne,
/// Template for checking a target, emitting rustc JSON diagnostics.
/// May include {label} which will get the label from the `build` section of a crate.
Flycheck,
/// For forwards-compatibility, i.e. old rust-analyzer binary with newer workspace discovery tools
Unknown,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
@@ -380,6 +398,8 @@ pub struct ProjectJsonData {
crates: Vec<CrateData>,
#[serde(default)]
runnables: Vec<RunnableData>,
//
// New fields should be Option or #[serde(default)]. This applies to most of this datastructure.
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default)]
@@ -453,32 +473,40 @@ enum EditionData {
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct BuildData {
struct BuildData {
label: String,
build_file: Utf8PathBuf,
target_kind: TargetKindData,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnableData {
pub program: String,
pub args: Vec<String>,
pub cwd: Utf8PathBuf,
pub kind: RunnableKindData,
struct RunnableData {
program: String,
args: Vec<String>,
cwd: Utf8PathBuf,
kind: RunnableKindData,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RunnableKindData {
enum RunnableKindData {
Flycheck,
Check,
Run,
TestOne,
TestMod,
DocTestOne,
BenchOne,
/// For forwards-compatibility, i.e. old rust-analyzer binary with newer workspace discovery tools
#[allow(unused)]
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum TargetKindData {
enum TargetKindData {
Bin,
/// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
Lib,
@@ -541,7 +569,11 @@ fn from(data: RunnableKindData) -> Self {
RunnableKindData::Check => RunnableKind::Check,
RunnableKindData::Run => RunnableKind::Run,
RunnableKindData::TestOne => RunnableKind::TestOne,
RunnableKindData::TestMod => RunnableKind::TestMod,
RunnableKindData::DocTestOne => RunnableKind::DocTestOne,
RunnableKindData::BenchOne => RunnableKind::BenchOne,
RunnableKindData::Flycheck => RunnableKind::Flycheck,
RunnableKindData::Unknown => RunnableKind::Unknown,
}
}
}
@@ -192,6 +192,12 @@ fn rust_project_hello_world_project_model() {
);
}
#[test]
fn rust_project_labeled_project_model() {
// This just needs to parse.
_ = load_rust_project("labeled-project.json");
}
#[test]
fn rust_project_cfg_groups() {
let (crate_graph, _proc_macros) = load_rust_project("cfg-groups.json");
@@ -0,0 +1,37 @@
{
"sysroot_src": null,
"crates": [
{
"display_name": "hello_world",
"root_module": "$ROOT$src/lib.rs",
"edition": "2018",
"deps": [],
"is_workspace_member": true,
"build": {
"label": "//:hello_world",
"build_file": "$ROOT$BUILD",
"target_kind": "bin"
}
}
],
"runnables": [
{
"kind": "run",
"program": "bazel",
"args": ["run", "{label}"],
"cwd": "$ROOT$"
},
{
"kind": "flycheck",
"program": "$ROOT$custom-flychecker.sh",
"args": ["{label}"],
"cwd": "$ROOT$"
},
{
"kind": "we-ignore-unknown-runnable-kinds-for-forwards-compatibility",
"program": "abc",
"args": ["{label}"],
"cwd": "$ROOT$"
}
]
}
@@ -91,6 +91,7 @@ pub fn run(self, verbosity: Verbosity) -> anyhow::Result<()> {
}
},
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
@@ -41,6 +41,7 @@ fn run_(self) -> anyhow::Result<()> {
load_out_dirs_from_check: !self.disable_build_scripts,
with_proc_macro_server,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let (db, _vfs, _proc_macro) =
@@ -191,6 +191,9 @@
/// Exclude code from vendored libraries from the resulting index.
optional --exclude-vendored-libraries
/// The number of worker threads for cache priming. Defaults to the number of physical cores.
optional --num-threads num_threads: usize
}
}
}
@@ -338,6 +341,7 @@ pub struct Scip {
pub output: Option<PathBuf>,
pub config_path: Option<PathBuf>,
pub exclude_vendored_libraries: bool,
pub num_threads: Option<usize>,
}
impl RustAnalyzer {
@@ -293,6 +293,7 @@ pub fn run(
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let path = AbsPathBuf::assert_utf8(env::current_dir()?.join(self.path));
@@ -38,6 +38,7 @@ pub fn run(self) -> anyhow::Result<()> {
// we want to ensure that this command, not `load_workspace_at`,
// is responsible for that work.
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: config.proc_macro_num_processes(),
};
@@ -23,6 +23,7 @@ pub fn run(self) -> Result<()> {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let (ref db, _vfs, _proc_macro) =
@@ -103,6 +103,7 @@ fn new() -> Result<Self> {
load_out_dirs_from_check: false,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let (db, _vfs, _proc_macro) =
@@ -52,6 +52,7 @@ pub fn run(self) -> anyhow::Result<()> {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: true,
num_worker_threads: self.num_threads.unwrap_or_else(num_cpus::get_physical),
proc_macro_processes: config.proc_macro_num_processes(),
};
let cargo_config = config.cargo(None);
@@ -20,6 +20,7 @@ pub fn run(self) -> anyhow::Result<()> {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let (ref db, vfs, _proc_macro) = load_workspace_at(
@@ -57,6 +58,7 @@ pub fn run(self) -> anyhow::Result<()> {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
let (ref db, _vfs, _proc_macro) = load_workspace_at(
@@ -44,6 +44,7 @@ fn run_(self) -> anyhow::Result<()> {
load_out_dirs_from_check: !self.disable_build_scripts,
with_proc_macro_server,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: config.proc_macro_num_processes(),
};
let (db, vfs, _proc_macro) =
@@ -948,18 +948,18 @@ pub enum MaxSubstitutionLength {
/// Override the command used for bench runnables.
/// The first element of the array should be the program to execute (for example, `cargo`).
///
/// Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically
/// Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically
/// replace the package name, target option (such as `--bin` or `--example`), the target name and
/// the test name (name of test function or test mod path).
/// the arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).
runnables_bench_overrideCommand: Option<Vec<String>> = None,
/// Command to be executed instead of 'cargo' for runnables.
runnables_command: Option<String> = None,
/// Override the command used for bench runnables.
/// The first element of the array should be the program to execute (for example, `cargo`).
///
/// Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically
/// Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically
/// replace the package name, target option (such as `--bin` or `--example`), the target name and
/// the test name (name of test function or test mod path).
/// the arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).
runnables_doctest_overrideCommand: Option<Vec<String>> = None,
/// Additional arguments to be passed to cargo for runnables such as
/// tests or binaries. For example, it may be `--release`.
@@ -977,9 +977,9 @@ pub enum MaxSubstitutionLength {
/// Override the command used for test runnables.
/// The first element of the array should be the program to execute (for example, `cargo`).
///
/// Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically
/// Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically
/// replace the package name, target option (such as `--bin` or `--example`), the target name and
/// the test name (name of test function or test mod path).
/// the arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).
runnables_test_overrideCommand: Option<Vec<String>> = None,
/// Path to the Cargo.toml of the rust compiler workspace, for usage in rustc_private
@@ -53,6 +53,7 @@ fn integrated_highlighting_benchmark() {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 1,
};
@@ -122,6 +123,7 @@ fn integrated_completion_benchmark() {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: true,
num_worker_threads: 1,
proc_macro_processes: 1,
};
@@ -324,6 +326,7 @@ fn integrated_diagnostics_benchmark() {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: true,
num_worker_threads: 1,
proc_macro_processes: 1,
};
@@ -303,6 +303,15 @@ fn next_event(
.map(Some)
}
fn trigger_garbage_collection(&mut self) {
if cfg!(test) {
// Slow tests run the main loop in multiple threads, but GC isn't thread safe.
return;
}
self.analysis_host.trigger_garbage_collection();
}
fn handle_event(&mut self, event: Event) {
let loop_start = Instant::now();
let _p = tracing::info_span!("GlobalState::handle_event", event = %event).entered();
@@ -383,7 +392,7 @@ fn handle_event(&mut self, event: Event) {
));
}
PrimeCachesProgress::End { cancelled } => {
self.analysis_host.trigger_garbage_collection();
self.trigger_garbage_collection();
self.prime_caches_queue.op_completed(());
if cancelled {
self.prime_caches_queue
@@ -542,7 +551,7 @@ fn handle_event(&mut self, event: Event) {
&& self.fmt_pool.handle.is_empty()
&& current_revision != self.last_gc_revision
{
self.analysis_host.trigger_garbage_collection();
self.trigger_garbage_collection();
self.last_gc_revision = current_revision;
}
}
@@ -1178,6 +1187,8 @@ fn handle_flycheck_msg(&mut self, message: FlycheckMessage, cargo_finished: &mut
} => self.diagnostics.clear_check_older_than_for_package(id, package_id, generation),
FlycheckMessage::Progress { id, progress } => {
let format_with_id = |user_facing_command: String| {
// When we're running multiple flychecks, we have to include a disambiguator in
// the title, or the editor complains. Note that this is a user-facing string.
if self.flycheck.len() == 1 {
user_facing_command
} else {
@@ -6,7 +6,7 @@
use cfg::{CfgAtom, CfgExpr};
use hir::sym;
use ide::{Cancellable, Crate, FileId, RunnableKind, TestId};
use project_model::project_json::Runnable;
use project_model::project_json::{self, Runnable};
use project_model::{CargoFeatures, ManifestPath, TargetKind};
use rustc_hash::FxHashSet;
use triomphe::Arc;
@@ -72,48 +72,51 @@ pub(crate) struct ProjectJsonTargetSpec {
}
impl ProjectJsonTargetSpec {
fn find_replace_runnable(
&self,
kind: project_json::RunnableKind,
replacer: &dyn Fn(&Self, &str) -> String,
) -> Option<Runnable> {
for runnable in &self.shell_runnables {
if runnable.kind == kind {
let mut runnable = runnable.clone();
let replaced_args: Vec<_> =
runnable.args.iter().map(|arg| replacer(self, arg)).collect();
runnable.args = replaced_args;
return Some(runnable);
}
}
None
}
pub(crate) fn runnable_args(&self, kind: &RunnableKind) -> Option<Runnable> {
match kind {
RunnableKind::Bin => {
for runnable in &self.shell_runnables {
if matches!(runnable.kind, project_model::project_json::RunnableKind::Run) {
let mut runnable = runnable.clone();
let replaced_args: Vec<_> = runnable
.args
.iter()
.map(|arg| arg.replace("{label}", &self.label))
.collect();
runnable.args = replaced_args;
return Some(runnable);
}
}
None
}
RunnableKind::Bin => self
.find_replace_runnable(project_json::RunnableKind::Run, &|this, arg| {
arg.replace("{label}", &this.label)
}),
RunnableKind::Test { test_id, .. } => {
for runnable in &self.shell_runnables {
if matches!(runnable.kind, project_model::project_json::RunnableKind::TestOne) {
let mut runnable = runnable.clone();
let replaced_args: Vec<_> = runnable
.args
.iter()
.map(|arg| arg.replace("{test_id}", &test_id.to_string()))
.map(|arg| arg.replace("{label}", &self.label))
.collect();
runnable.args = replaced_args;
return Some(runnable);
}
}
None
self.find_replace_runnable(project_json::RunnableKind::Run, &|this, arg| {
arg.replace("{label}", &this.label).replace("{test_id}", &test_id.to_string())
})
}
RunnableKind::TestMod { path } => self
.find_replace_runnable(project_json::RunnableKind::TestMod, &|this, arg| {
arg.replace("{label}", &this.label).replace("{test_pattern}", path)
}),
RunnableKind::Bench { test_id } => {
self.find_replace_runnable(project_json::RunnableKind::BenchOne, &|this, arg| {
arg.replace("{label}", &this.label).replace("{bench_id}", &test_id.to_string())
})
}
RunnableKind::DocTest { test_id } => {
self.find_replace_runnable(project_json::RunnableKind::DocTestOne, &|this, arg| {
arg.replace("{label}", &this.label).replace("{test_id}", &test_id.to_string())
})
}
RunnableKind::TestMod { .. } => None,
RunnableKind::Bench { .. } => None,
RunnableKind::DocTest { .. } => None,
}
}
}
@@ -129,38 +132,21 @@ pub(crate) fn runnable_args(
let extra_test_binary_args = config.extra_test_binary_args;
let mut cargo_args = Vec::new();
let mut executable_args = Vec::new();
let executable_args = Self::executable_args_for(kind, extra_test_binary_args);
match kind {
RunnableKind::Test { test_id, attr } => {
RunnableKind::Test { .. } => {
cargo_args.push(config.test_command);
executable_args.push(test_id.to_string());
if let TestId::Path(_) = test_id {
executable_args.push("--exact".to_owned());
}
executable_args.extend(extra_test_binary_args);
if attr.ignore {
executable_args.push("--ignored".to_owned());
}
}
RunnableKind::TestMod { path } => {
RunnableKind::TestMod { .. } => {
cargo_args.push(config.test_command);
executable_args.push(path.clone());
executable_args.extend(extra_test_binary_args);
}
RunnableKind::Bench { test_id } => {
RunnableKind::Bench { .. } => {
cargo_args.push(config.bench_command);
executable_args.push(test_id.to_string());
if let TestId::Path(_) = test_id {
executable_args.push("--exact".to_owned());
}
executable_args.extend(extra_test_binary_args);
}
RunnableKind::DocTest { test_id } => {
RunnableKind::DocTest { .. } => {
cargo_args.push("test".to_owned());
cargo_args.push("--doc".to_owned());
executable_args.push(test_id.to_string());
executable_args.extend(extra_test_binary_args);
}
RunnableKind::Bin => {
let subcommand = match spec {
@@ -253,16 +239,70 @@ pub(crate) fn override_command(
TargetKind::BuildScript | TargetKind::Other => "",
};
let target = |kind, target| match kind {
TargetKind::Bin | TargetKind::Test | TargetKind::Bench | TargetKind::Example => target,
_ => "",
};
let replace_placeholders = |arg: String| match &spec {
Some(spec) => arg
.replace("${package}", &spec.package)
.replace("${target_arg}", target_arg(spec.target_kind))
.replace("${target}", &spec.target)
.replace("${target}", target(spec.target_kind, &spec.target))
.replace("${test_name}", &test_name),
_ => arg,
};
args.map(|args| args.into_iter().map(replace_placeholders).collect())
let extra_test_binary_args = config.extra_test_binary_args;
let executable_args = Self::executable_args_for(kind, extra_test_binary_args);
args.map(|mut args| {
let exec_args_idx = args.iter().position(|a| a == "${executable_args}");
if let Some(idx) = exec_args_idx {
args.splice(idx..idx + 1, executable_args);
}
args.into_iter().map(replace_placeholders).filter(|a| !a.trim().is_empty()).collect()
})
}
fn executable_args_for(
kind: &RunnableKind,
extra_test_binary_args: impl IntoIterator<Item = String>,
) -> Vec<String> {
let mut executable_args = Vec::new();
match kind {
RunnableKind::Test { test_id, attr } => {
executable_args.push(test_id.to_string());
if let TestId::Path(_) = test_id {
executable_args.push("--exact".to_owned());
}
executable_args.extend(extra_test_binary_args);
if attr.ignore {
executable_args.push("--ignored".to_owned());
}
}
RunnableKind::TestMod { path } => {
executable_args.push(path.clone());
executable_args.extend(extra_test_binary_args);
}
RunnableKind::Bench { test_id } => {
executable_args.push(test_id.to_string());
if let TestId::Path(_) = test_id {
executable_args.push("--exact".to_owned());
}
executable_args.extend(extra_test_binary_args);
}
RunnableKind::DocTest { test_id } => {
executable_args.push(test_id.to_string());
executable_args.extend(extra_test_binary_args);
}
RunnableKind::Bin => {}
}
executable_args
}
pub(crate) fn push_to(self, buf: &mut Vec<String>, kind: &RunnableKind) {
+51 -6
View File
@@ -221,12 +221,7 @@ pub fn trim_indent(mut text: &str) -> String {
if text.starts_with('\n') {
text = &text[1..];
}
let indent = text
.lines()
.filter(|it| !it.trim().is_empty())
.map(|it| it.len() - it.trim_start().len())
.min()
.unwrap_or(0);
let indent = indent_of(text);
text.split_inclusive('\n')
.map(
|line| {
@@ -236,6 +231,25 @@ pub fn trim_indent(mut text: &str) -> String {
.collect()
}
#[must_use]
fn indent_of(text: &str) -> usize {
text.lines()
.filter(|it| !it.trim().is_empty())
.map(|it| it.len() - it.trim_start().len())
.min()
.unwrap_or(0)
}
#[must_use]
pub fn dedent_by(spaces: usize, text: &str) -> String {
text.split_inclusive('\n')
.map(|line| {
let trimmed = line.trim_start_matches(' ');
if line.len() - trimmed.len() <= spaces { trimmed } else { &line[spaces..] }
})
.collect()
}
pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
where
F: FnMut(&T) -> Ordering,
@@ -366,6 +380,37 @@ fn main() {
);
}
#[test]
fn test_dedent() {
assert_eq!(dedent_by(0, ""), "");
assert_eq!(dedent_by(1, ""), "");
assert_eq!(dedent_by(2, ""), "");
assert_eq!(dedent_by(0, "foo"), "foo");
assert_eq!(dedent_by(2, "foo"), "foo");
assert_eq!(dedent_by(2, " foo"), "foo");
assert_eq!(dedent_by(2, " foo"), " foo");
assert_eq!(dedent_by(2, " foo\nbar"), " foo\nbar");
assert_eq!(dedent_by(2, "foo\n bar"), "foo\n bar");
assert_eq!(dedent_by(2, "foo\n\n bar"), "foo\n\n bar");
assert_eq!(dedent_by(2, "foo\n.\n bar"), "foo\n.\n bar");
assert_eq!(dedent_by(2, "foo\n .\n bar"), "foo\n.\n bar");
assert_eq!(dedent_by(2, "foo\n .\n bar"), "foo\n .\n bar");
}
#[test]
fn test_indent_of() {
assert_eq!(indent_of(""), 0);
assert_eq!(indent_of(" "), 0);
assert_eq!(indent_of(" x"), 1);
assert_eq!(indent_of(" x\n"), 1);
assert_eq!(indent_of(" x\ny"), 0);
assert_eq!(indent_of(" x\n y"), 1);
assert_eq!(indent_of(" x\n y"), 1);
assert_eq!(indent_of(" x\n y"), 2);
assert_eq!(indent_of(" x\n y\n"), 2);
assert_eq!(indent_of(" x\n\n y\n"), 2);
}
#[test]
fn test_replace() {
#[track_caller]
@@ -132,3 +132,19 @@ pub fn previous_non_trivia_token(e: impl Into<SyntaxElement>) -> Option<SyntaxTo
}
None
}
pub fn next_non_trivia_token(e: impl Into<SyntaxElement>) -> Option<SyntaxToken> {
let mut token = match e.into() {
SyntaxElement::Node(n) => n.last_token()?,
SyntaxElement::Token(t) => t,
}
.next_token();
while let Some(inner) = token {
if !inner.kind().is_trivia() {
return Some(inner);
} else {
token = inner.next_token();
}
}
None
}
@@ -43,6 +43,12 @@ fn add(self, rhs: u8) -> IndentLevel {
}
}
impl ops::AddAssign<u8> for IndentLevel {
fn add_assign(&mut self, rhs: u8) {
self.0 += rhs;
}
}
impl IndentLevel {
pub fn single() -> IndentLevel {
IndentLevel(0)
@@ -5,7 +5,7 @@
AstNode, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken,
ast::{
self, HasArgList, HasAttrs, HasGenericArgs, HasGenericParams, HasLoopBody, HasName,
HasTypeBounds, HasVisibility, Param, RangeItem, make,
HasTypeBounds, HasVisibility, Lifetime, Param, RangeItem, make,
},
syntax_editor::SyntaxMappingBuilder,
};
@@ -21,6 +21,14 @@ pub fn name_ref(&self, name: &str) -> ast::NameRef {
make::name_ref(name).clone_for_update()
}
pub fn name_ref_self_ty(&self) -> ast::NameRef {
make::name_ref_self_ty().clone_for_update()
}
pub fn expr_todo(&self) -> ast::Expr {
make::ext::expr_todo().clone_for_update()
}
pub fn lifetime(&self, text: &str) -> ast::Lifetime {
make::lifetime(text).clone_for_update()
}
@@ -96,24 +104,47 @@ pub fn struct_(
generic_param_list: Option<ast::GenericParamList>,
field_list: ast::FieldList,
) -> ast::Struct {
make::struct_(visibility, strukt_name, generic_param_list, field_list).clone_for_update()
}
let ast = make::struct_(
visibility.clone(),
strukt_name.clone(),
generic_param_list.clone(),
field_list.clone(),
)
.clone_for_update();
pub fn enum_(
&self,
attrs: impl IntoIterator<Item = ast::Attr>,
visibility: Option<ast::Visibility>,
enum_name: ast::Name,
generic_param_list: Option<ast::GenericParamList>,
where_clause: Option<ast::WhereClause>,
variant_list: ast::VariantList,
) -> ast::Enum {
make::enum_(attrs, visibility, enum_name, generic_param_list, where_clause, variant_list)
.clone_for_update()
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
if let Some(visibility) = visibility {
builder.map_node(
visibility.syntax().clone(),
ast.visibility().unwrap().syntax().clone(),
);
}
builder.map_node(strukt_name.syntax().clone(), ast.name().unwrap().syntax().clone());
if let Some(generic_param_list) = generic_param_list {
builder.map_node(
generic_param_list.syntax().clone(),
ast.generic_param_list().unwrap().syntax().clone(),
);
}
builder
.map_node(field_list.syntax().clone(), ast.field_list().unwrap().syntax().clone());
builder.finish(&mut mapping);
}
ast
}
pub fn unnamed_param(&self, ty: ast::Type) -> ast::Param {
make::unnamed_param(ty).clone_for_update()
let ast = make::unnamed_param(ty.clone()).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_node(ty.syntax().clone(), ast.ty().unwrap().syntax().clone());
builder.finish(&mut mapping);
}
ast
}
pub fn ty_fn_ptr<I: Iterator<Item = Param>>(
@@ -123,7 +154,27 @@ pub fn ty_fn_ptr<I: Iterator<Item = Param>>(
params: I,
ret_type: Option<ast::RetType>,
) -> ast::FnPtrType {
make::ty_fn_ptr(is_unsafe, abi, params, ret_type).clone_for_update()
let (params, params_input) = iterator_input(params);
let ast = make::ty_fn_ptr(is_unsafe, abi.clone(), params.into_iter(), ret_type.clone())
.clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
if let Some(abi) = abi {
builder.map_node(abi.syntax().clone(), ast.abi().unwrap().syntax().clone());
}
builder.map_children(
params_input,
ast.param_list().unwrap().params().map(|p| p.syntax().clone()),
);
if let Some(ret_type) = ret_type {
builder
.map_node(ret_type.syntax().clone(), ast.ret_type().unwrap().syntax().clone());
}
builder.finish(&mut mapping);
}
ast
}
pub fn where_pred(
@@ -131,18 +182,61 @@ pub fn where_pred(
path: Either<ast::Lifetime, ast::Type>,
bounds: impl IntoIterator<Item = ast::TypeBound>,
) -> ast::WherePred {
make::where_pred(path, bounds).clone_for_update()
let (bounds, bounds_input) = iterator_input(bounds);
let ast = make::where_pred(path.clone(), bounds).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
match &path {
Either::Left(lifetime) => {
builder.map_node(
lifetime.syntax().clone(),
ast.lifetime().unwrap().syntax().clone(),
);
}
Either::Right(ty) => {
builder.map_node(ty.syntax().clone(), ast.ty().unwrap().syntax().clone());
}
}
if let Some(type_bound_list) = ast.type_bound_list() {
builder.map_children(
bounds_input,
type_bound_list.bounds().map(|b| b.syntax().clone()),
);
}
builder.finish(&mut mapping);
}
ast
}
pub fn where_clause(
&self,
predicates: impl IntoIterator<Item = ast::WherePred>,
) -> ast::WhereClause {
make::where_clause(predicates).clone_for_update()
let (predicates, input) = iterator_input(predicates);
let ast = make::where_clause(predicates).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_children(input, ast.predicates().map(|p| p.syntax().clone()));
builder.finish(&mut mapping);
}
ast
}
pub fn impl_trait_type(&self, bounds: ast::TypeBoundList) -> ast::ImplTraitType {
make::impl_trait_type(bounds).clone_for_update()
let ast = make::impl_trait_type(bounds.clone()).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder
.map_node(bounds.syntax().clone(), ast.type_bound_list().unwrap().syntax().clone());
builder.finish(&mut mapping);
}
ast
}
pub fn expr_field(&self, receiver: ast::Expr, field: &str) -> ast::FieldExpr {
@@ -340,15 +434,53 @@ pub fn generic_ty_path_segment(
name_ref: ast::NameRef,
generic_args: impl IntoIterator<Item = ast::GenericArg>,
) -> ast::PathSegment {
make::generic_ty_path_segment(name_ref, generic_args).clone_for_update()
let (generic_args, input) = iterator_input(generic_args);
let ast = make::generic_ty_path_segment(name_ref.clone(), generic_args).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_node(name_ref.syntax().clone(), ast.name_ref().unwrap().syntax().clone());
builder.map_children(
input,
ast.generic_arg_list().unwrap().generic_args().map(|a| a.syntax().clone()),
);
builder.finish(&mut mapping);
}
ast
}
pub fn tail_only_block_expr(&self, tail_expr: ast::Expr) -> ast::BlockExpr {
make::tail_only_block_expr(tail_expr)
let ast = make::tail_only_block_expr(tail_expr.clone()).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let stmt_list = ast.stmt_list().unwrap();
let mut builder = SyntaxMappingBuilder::new(stmt_list.syntax().clone());
builder.map_node(
tail_expr.syntax().clone(),
stmt_list.tail_expr().unwrap().syntax().clone(),
);
builder.finish(&mut mapping);
}
ast
}
pub fn expr_bin_op(&self, lhs: ast::Expr, op: ast::BinaryOp, rhs: ast::Expr) -> ast::Expr {
make::expr_bin_op(lhs, op, rhs)
let ast::Expr::BinExpr(ast) =
make::expr_bin_op(lhs.clone(), op, rhs.clone()).clone_for_update()
else {
unreachable!()
};
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_node(lhs.syntax().clone(), ast.lhs().unwrap().syntax().clone());
builder.map_node(rhs.syntax().clone(), ast.rhs().unwrap().syntax().clone());
builder.finish(&mut mapping);
}
ast.into()
}
pub fn ty_placeholder(&self) -> ast::Type {
@@ -385,7 +517,23 @@ pub fn use_(
visibility: Option<ast::Visibility>,
use_tree: ast::UseTree,
) -> ast::Use {
make::use_(attrs, visibility, use_tree).clone_for_update()
let (attrs, attrs_input) = iterator_input(attrs);
let ast = make::use_(attrs, visibility.clone(), use_tree.clone()).clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_children(attrs_input, ast.attrs().map(|attr| attr.syntax().clone()));
if let Some(visibility) = visibility {
builder.map_node(
visibility.syntax().clone(),
ast.visibility().unwrap().syntax().clone(),
);
}
builder.map_node(use_tree.syntax().clone(), ast.use_tree().unwrap().syntax().clone());
builder.finish(&mut mapping);
}
ast
}
pub fn use_tree(
@@ -395,7 +543,25 @@ pub fn use_tree(
alias: Option<ast::Rename>,
add_star: bool,
) -> ast::UseTree {
make::use_tree(path, use_tree_list, alias, add_star).clone_for_update()
let ast = make::use_tree(path.clone(), use_tree_list.clone(), alias.clone(), add_star)
.clone_for_update();
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_node(path.syntax().clone(), ast.path().unwrap().syntax().clone());
if let Some(use_tree_list) = use_tree_list {
builder.map_node(
use_tree_list.syntax().clone(),
ast.use_tree_list().unwrap().syntax().clone(),
);
}
if let Some(alias) = alias {
builder.map_node(alias.syntax().clone(), ast.rename().unwrap().syntax().clone());
}
builder.finish(&mut mapping);
}
ast
}
pub fn path_unqualified(&self, segment: ast::PathSegment) -> ast::Path {
@@ -896,10 +1062,6 @@ pub fn expr_underscore(&self) -> ast::UnderscoreExpr {
unreachable!()
};
if let Some(mut mapping) = self.mappings() {
SyntaxMappingBuilder::new(ast.syntax().clone()).finish(&mut mapping);
}
ast
}
@@ -1765,6 +1927,65 @@ pub fn ty_ref(&self, ty: ast::Type, is_mut: bool) -> ast::Type {
}
ast
}
pub fn field_from_idents<'a>(
&self,
parts: impl std::iter::IntoIterator<Item = &'a str>,
) -> Option<ast::Expr> {
make::ext::field_from_idents(parts)
}
pub fn expr_await(&self, expr: ast::Expr) -> ast::AwaitExpr {
let ast::Expr::AwaitExpr(ast) = make::expr_await(expr.clone()).clone_for_update() else {
unreachable!()
};
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
builder.map_node(expr.syntax().clone(), ast.expr().unwrap().syntax().clone());
builder.finish(&mut mapping);
}
ast
}
pub fn expr_break(&self, label: Option<Lifetime>, expr: Option<ast::Expr>) -> ast::BreakExpr {
let ast::Expr::BreakExpr(ast) =
make::expr_break(label.clone(), expr.clone()).clone_for_update()
else {
unreachable!()
};
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
if let Some(label) = label {
builder.map_node(label.syntax().clone(), ast.lifetime().unwrap().syntax().clone());
}
if let Some(expr) = expr {
builder.map_node(expr.syntax().clone(), ast.expr().unwrap().syntax().clone());
}
builder.finish(&mut mapping);
}
ast
}
pub fn expr_continue(&self, label: Option<Lifetime>) -> ast::ContinueExpr {
let ast::Expr::ContinueExpr(ast) = make::expr_continue(label.clone()).clone_for_update()
else {
unreachable!()
};
if let Some(mut mapping) = self.mappings() {
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
if let Some(label) = label {
builder.map_node(label.syntax().clone(), ast.lifetime().unwrap().syntax().clone());
}
builder.finish(&mut mapping);
}
ast
}
}
// `ext` constructors
@@ -1380,9 +1380,9 @@ Default: `null`
Override the command used for bench runnables.
The first element of the array should be the program to execute (for example, `cargo`).
Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically
Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically
replace the package name, target option (such as `--bin` or `--example`), the target name and
the test name (name of test function or test mod path).
the arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).
## rust-analyzer.runnables.command {#runnables.command}
@@ -1399,9 +1399,9 @@ Default: `null`
Override the command used for bench runnables.
The first element of the array should be the program to execute (for example, `cargo`).
Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically
Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically
replace the package name, target option (such as `--bin` or `--example`), the target name and
the test name (name of test function or test mod path).
the arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).
## rust-analyzer.runnables.extraArgs {#runnables.extraArgs}
@@ -1444,9 +1444,9 @@ Default: `null`
Override the command used for test runnables.
The first element of the array should be the program to execute (for example, `cargo`).
Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically
Use the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically
replace the package name, target option (such as `--bin` or `--example`), the target name and
the test name (name of test function or test mod path).
the arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).
## rust-analyzer.rustc.source {#rustc.source}
+6 -6
View File
@@ -3697,9 +3697,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -6719,9 +6719,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2865,7 +2865,7 @@
"title": "Runnables",
"properties": {
"rust-analyzer.runnables.bench.overrideCommand": {
"markdownDescription": "Override the command used for bench runnables.\nThe first element of the array should be the program to execute (for example, `cargo`).\n\nUse the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically\nreplace the package name, target option (such as `--bin` or `--example`), the target name and\nthe test name (name of test function or test mod path).",
"markdownDescription": "Override the command used for bench runnables.\nThe first element of the array should be the program to execute (for example, `cargo`).\n\nUse the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically\nreplace the package name, target option (such as `--bin` or `--example`), the target name and\nthe arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).",
"default": null,
"type": [
"null",
@@ -2894,7 +2894,7 @@
"title": "Runnables",
"properties": {
"rust-analyzer.runnables.doctest.overrideCommand": {
"markdownDescription": "Override the command used for bench runnables.\nThe first element of the array should be the program to execute (for example, `cargo`).\n\nUse the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically\nreplace the package name, target option (such as `--bin` or `--example`), the target name and\nthe test name (name of test function or test mod path).",
"markdownDescription": "Override the command used for bench runnables.\nThe first element of the array should be the program to execute (for example, `cargo`).\n\nUse the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically\nreplace the package name, target option (such as `--bin` or `--example`), the target name and\nthe arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).",
"default": null,
"type": [
"null",
@@ -2948,7 +2948,7 @@
"title": "Runnables",
"properties": {
"rust-analyzer.runnables.test.overrideCommand": {
"markdownDescription": "Override the command used for test runnables.\nThe first element of the array should be the program to execute (for example, `cargo`).\n\nUse the placeholders `${package}`, `${target_arg}`, `${target}`, `${test_name}` to dynamically\nreplace the package name, target option (such as `--bin` or `--example`), the target name and\nthe test name (name of test function or test mod path).",
"markdownDescription": "Override the command used for test runnables.\nThe first element of the array should be the program to execute (for example, `cargo`).\n\nUse the placeholders `${package}`, `${target_arg}`, `${target}`, `${executable_args}` to dynamically\nreplace the package name, target option (such as `--bin` or `--example`), the target name and\nthe arguments passed to test binary args (includes `rust-analyzer.runnables.extraTestBinaryArgs`).",
"default": null,
"type": [
"null",