Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--README.md2
-rw-r--r--crates/hir-ty/src/next_solver/ir_print.rs29
-rw-r--r--crates/hir-ty/src/target_feature.rs1
-rw-r--r--crates/hir/src/diagnostics.rs14
-rw-r--r--crates/ide-assists/src/handlers/add_braces.rs65
-rw-r--r--crates/ide-assists/src/handlers/add_label_to_loop.rs93
-rw-r--r--crates/ide-assists/src/handlers/add_lifetime_to_type.rs115
-rw-r--r--crates/ide-assists/src/handlers/add_missing_match_arms.rs132
-rw-r--r--crates/ide-assists/src/handlers/apply_demorgan.rs2
-rw-r--r--crates/ide-assists/src/handlers/convert_bool_to_enum.rs2
-rw-r--r--crates/ide-assists/src/handlers/convert_closure_to_fn.rs28
-rw-r--r--crates/ide-assists/src/handlers/convert_for_to_while_let.rs13
-rw-r--r--crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs48
-rw-r--r--crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs298
-rw-r--r--crates/ide-assists/src/handlers/convert_to_guarded_return.rs49
-rw-r--r--crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs274
-rw-r--r--crates/ide-assists/src/handlers/convert_while_to_loop.rs33
-rw-r--r--crates/ide-assists/src/handlers/destructure_struct_binding.rs11
-rw-r--r--crates/ide-assists/src/handlers/desugar_try_expr.rs48
-rw-r--r--crates/ide-assists/src/handlers/extract_expressions_from_format_string.rs14
-rw-r--r--crates/ide-assists/src/handlers/extract_variable.rs66
-rw-r--r--crates/ide-assists/src/handlers/generate_delegate_methods.rs69
-rw-r--r--crates/ide-assists/src/handlers/generate_delegate_trait.rs6
-rw-r--r--crates/ide-assists/src/handlers/generate_trait_from_impl.rs106
-rw-r--r--crates/ide-assists/src/handlers/inline_local_variable.rs146
-rw-r--r--crates/ide-assists/src/handlers/inline_type_alias.rs110
-rw-r--r--crates/ide-assists/src/handlers/invert_if.rs7
-rw-r--r--crates/ide-assists/src/handlers/merge_imports.rs24
-rw-r--r--crates/ide-assists/src/handlers/move_guard.rs11
-rw-r--r--crates/ide-assists/src/handlers/qualify_method_call.rs14
-rw-r--r--crates/ide-assists/src/handlers/qualify_path.rs45
-rw-r--r--crates/ide-assists/src/handlers/replace_is_method_with_if_let_method.rs90
-rw-r--r--crates/ide-assists/src/handlers/replace_let_with_if_let.rs4
-rw-r--r--crates/ide-assists/src/handlers/replace_method_eager_lazy.rs16
-rw-r--r--crates/ide-assists/src/handlers/unwrap_block.rs35
-rw-r--r--crates/ide-assists/src/handlers/unwrap_tuple.rs64
-rw-r--r--crates/ide-assists/src/handlers/wrap_unwrap_cfg_attr.rs138
-rw-r--r--crates/ide-assists/src/tests/generated.rs4
-rw-r--r--crates/ide-assists/src/utils.rs111
-rw-r--r--crates/ide-assists/src/utils/ref_field_expr.rs8
-rw-r--r--crates/ide-completion/src/completions/fn_param.rs40
-rw-r--r--crates/ide-completion/src/completions/postfix.rs160
-rw-r--r--crates/ide-completion/src/context/analysis.rs18
-rw-r--r--crates/ide-completion/src/context/tests.rs44
-rw-r--r--crates/ide-completion/src/render/function.rs33
-rw-r--r--crates/ide-completion/src/tests/fn_param.rs2
-rw-r--r--crates/ide-diagnostics/src/handlers/no_such_field.rs62
-rw-r--r--crates/ide-diagnostics/src/handlers/non_exhaustive_let.rs214
-rw-r--r--crates/ide-diagnostics/src/handlers/remove_unnecessary_else.rs2
-rw-r--r--crates/load-cargo/src/lib.rs17
-rw-r--r--crates/project-model/src/project_json.rs48
-rw-r--r--crates/project-model/src/tests.rs6
-rw-r--r--crates/project-model/test_data/labeled-project.json37
-rw-r--r--crates/rust-analyzer/src/cli/analysis_stats.rs1
-rw-r--r--crates/rust-analyzer/src/cli/diagnostics.rs1
-rw-r--r--crates/rust-analyzer/src/cli/flags.rs4
-rw-r--r--crates/rust-analyzer/src/cli/lsif.rs1
-rw-r--r--crates/rust-analyzer/src/cli/prime_caches.rs1
-rw-r--r--crates/rust-analyzer/src/cli/run_tests.rs1
-rw-r--r--crates/rust-analyzer/src/cli/rustc_tests.rs1
-rw-r--r--crates/rust-analyzer/src/cli/scip.rs1
-rw-r--r--crates/rust-analyzer/src/cli/ssr.rs2
-rw-r--r--crates/rust-analyzer/src/cli/unresolved_references.rs1
-rw-r--r--crates/rust-analyzer/src/config.rs12
-rw-r--r--crates/rust-analyzer/src/integrated_benchmarks.rs3
-rw-r--r--crates/rust-analyzer/src/main_loop.rs15
-rw-r--r--crates/rust-analyzer/src/target_spec.rs166
-rw-r--r--crates/stdx/src/lib.rs57
-rw-r--r--crates/syntax/src/algo.rs16
-rw-r--r--crates/syntax/src/ast/edit.rs6
-rw-r--r--crates/syntax/src/ast/syntax_factory/constructors.rs277
-rw-r--r--docs/book/src/configuration_generated.md12
-rw-r--r--editors/code/package-lock.json12
-rw-r--r--editors/code/package.json6
74 files changed, 2814 insertions, 810 deletions
diff --git a/README.md b/README.md
index 6d2a673286..06d63a2d23 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/crates/hir-ty/src/next_solver/ir_print.rs b/crates/hir-ty/src/next_solver/ir_print.rs
index 998aab5a3f..65931549db 100644
--- a/crates/hir-ty/src/next_solver/ir_print.rs
+++ b/crates/hir-ty/src/next_solver/ir_print.rs
@@ -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 @@ impl<'db> IrPrint<ty::TraitPredicate<Self>> for DbInterner<'db> {
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 @@ impl<'db> IrPrint<rustc_type_ir::HostEffectPredicate<Self>> for DbInterner<'db>
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 @@ impl<'db> IrPrint<ty::NormalizesTo<Self>> for DbInterner<'db> {
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 @@ impl<'db> IrPrint<ty::SubtypePredicate<Self>> for DbInterner<'db> {
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 @@ impl<'db> IrPrint<ty::CoercePredicate<Self>> for DbInterner<'db> {
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 @@ impl<'db> IrPrint<ty::FnSig<Self>> for DbInterner<'db> {
}
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 @@ impl<'db> IrPrint<rustc_type_ir::PatternKind<DbInterner<'db>>> for DbInterner<'d
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"),
+ }
}
}
diff --git a/crates/hir-ty/src/target_feature.rs b/crates/hir-ty/src/target_feature.rs
index 2bd675ba12..29a933f922 100644
--- a/crates/hir-ty/src/target_feature.rs
+++ b/crates/hir-ty/src/target_feature.rs
@@ -217,6 +217,7 @@ const TARGET_FEATURE_IMPLICATIONS_RAW: &[(&str, &[&str])] = &[
("relaxed-simd", &["simd128"]),
// BPF
("alu32", &[]),
+ ("allows-misaligned-mem-access", &[]),
// CSKY
("10e60", &["7e10"]),
("2e3", &["e2"]),
diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs
index 050777a480..7f672a697c 100644
--- a/crates/hir/src/diagnostics.rs
+++ b/crates/hir/src/diagnostics.rs
@@ -51,20 +51,6 @@ macro_rules! diagnostics {
)*
};
}
-// 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,
diff --git a/crates/ide-assists/src/handlers/add_braces.rs b/crates/ide-assists/src/handlers/add_braces.rs
index 99ee50fa58..da1322de4b 100644
--- a/crates/ide-assists/src/handlers/add_braces.rs
+++ b/crates/ide-assists/src/handlers/add_braces.rs
@@ -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,
@@ -176,6 +177,70 @@ fn foo() {
}
"#,
);
+
+ 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
+};
+"#,
+ );
}
#[test]
diff --git a/crates/ide-assists/src/handlers/add_label_to_loop.rs b/crates/ide-assists/src/handlers/add_label_to_loop.rs
index b84ad24cfc..6a408e5254 100644
--- a/crates/ide-assists/src/handlers/add_label_to_loop.rs
+++ b/crates/ide-assists/src/handlers/add_label_to_loop.rs
@@ -3,11 +3,7 @@ use ide_db::{
};
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 @@ use crate::{AssistContext, AssistId, Assists};
// }
// ```
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 {
@@ -124,6 +128,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(
add_label_to_loop,
@@ -194,4 +240,29 @@ fn main() {
}"#,
);
}
+
+ #[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;
+ }
+}"#,
+ );
+ }
}
diff --git a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs b/crates/ide-assists/src/handlers/add_lifetime_to_type.rs
index 27dbdcf2c4..265ee3d2d4 100644
--- a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs
+++ b/crates/ide-assists/src/handlers/add_lifetime_to_type.rs
@@ -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 @@ use crate::{AssistContext, AssistId, Assists};
// ```
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};
@@ -165,6 +168,24 @@ mod tests {
}
#[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(
add_lifetime_to_type,
diff --git a/crates/ide-assists/src/handlers/add_missing_match_arms.rs b/crates/ide-assists/src/handlers/add_missing_match_arms.rs
index c0ce057d77..8124fecff6 100644
--- a/crates/ide-assists/src/handlers/add_missing_match_arms.rs
+++ b/crates/ide-assists/src/handlers/add_missing_match_arms.rs
@@ -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#"
@@ -1097,6 +1110,103 @@ fn main() {
}
"#,
);
+
+ 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
+ }
+}
+"#,
+ );
}
#[test]
diff --git a/crates/ide-assists/src/handlers/apply_demorgan.rs b/crates/ide-assists/src/handlers/apply_demorgan.rs
index 80d0a6da12..4ee4970248 100644
--- a/crates/ide-assists/src/handlers/apply_demorgan.rs
+++ b/crates/ide-assists/src/handlers/apply_demorgan.rs
@@ -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());
diff --git a/crates/ide-assists/src/handlers/convert_bool_to_enum.rs b/crates/ide-assists/src/handlers/convert_bool_to_enum.rs
index 9347798100..e88778a62e 100644
--- a/crates/ide-assists/src/handlers/convert_bool_to_enum.rs
+++ b/crates/ide-assists/src/handlers/convert_bool_to_enum.rs
@@ -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"),
diff --git a/crates/ide-assists/src/handlers/convert_closure_to_fn.rs b/crates/ide-assists/src/handlers/convert_closure_to_fn.rs
index 5a223e1130..9f9ced98d7 100644
--- a/crates/ide-assists/src/handlers/convert_closure_to_fn.rs
+++ b/crates/ide-assists/src/handlers/convert_closure_to_fn.rs
@@ -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()
};
@@ -971,6 +971,32 @@ fn foo() {
}
"#,
);
+ check_assist(
+ convert_closure_to_fn,
+ r#"
+//- minicore: copy
+fn foo() {
+ {
+ let closure = |$0| match () {
+ () => {},
+ };
+ closure();
+ }
+}
+"#,
+ r#"
+fn foo() {
+ {
+ fn closure() {
+ match () {
+ () => {},
+ }
+ }
+ closure();
+ }
+}
+"#,
+ );
}
#[test]
diff --git a/crates/ide-assists/src/handlers/convert_for_to_while_let.rs b/crates/ide-assists/src/handlers/convert_for_to_while_let.rs
index d64e9ceda2..d409374c16 100644
--- a/crates/ide-assists/src/handlers/convert_for_to_while_let.rs
+++ b/crates/ide-assists/src/handlers/convert_for_to_while_let.rs
@@ -2,7 +2,7 @@ use hir::{Name, sym};
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([]),
);
diff --git a/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs b/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs
index 6a74d21451..66ccd2a4e4 100644
--- a/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs
+++ b/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs
@@ -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)]
diff --git a/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs b/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs
index e518c39dab..42fceb8533 100644
--- a/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs
+++ b/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs
@@ -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};
@@ -678,6 +760,102 @@ where
}
#[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(
convert_named_struct_to_tuple_struct,
@@ -1042,7 +1220,9 @@ struct Struct(i32);
fn test() {
id! {
- let s = Struct(42);
+ let s = Struct(
+ 42,
+ );
let Struct(value) = s;
let Struct(inner) = s;
}
diff --git a/crates/ide-assists/src/handlers/convert_to_guarded_return.rs b/crates/ide-assists/src/handlers/convert_to_guarded_return.rs
index dc51bf4b5b..db45916792 100644
--- a/crates/ide-assists/src/handlers/convert_to_guarded_return.rs
+++ b/crates/ide-assists/src/handlers/convert_to_guarded_return.rs
@@ -10,14 +10,14 @@ use syntax::{
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()
}
diff --git a/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs b/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs
index f8b9bb68db..f1eae83866 100644
--- a/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs
+++ b/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs
@@ -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,
- };
- }
+ for (file_id, refs) in usages {
+ let source = ctx.sema.parse(file_id);
+ let mut editor = edit.make_editor(source.syntax());
- 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 r in refs {
+ process_struct_name_reference(ctx, r, &mut editor, &source, &strukt_def, names);
}
- };
- for (file_id, refs) in usages {
- let source = ctx.sema.parse(file_id);
- let source = 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()]);
- }
- }
+ 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 @@ where
"#,
);
}
+
+ #[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(
diff --git a/crates/ide-assists/src/handlers/convert_while_to_loop.rs b/crates/ide-assists/src/handlers/convert_while_to_loop.rs
index 9fd8b4b315..f8215d6723 100644
--- a/crates/ide-assists/src/handlers/convert_while_to_loop.rs
+++ b/crates/ide-assists/src/handlers/convert_while_to_loop.rs
@@ -6,7 +6,7 @@ use syntax::{
ast::{
self, HasLoopBody,
edit::{AstNodeEdit, IndentLevel},
- make,
+ syntax_factory::SyntaxFactory,
},
syntax_editor::{Element, Position},
};
@@ -14,7 +14,7 @@ use syntax::{
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);
},
)
diff --git a/crates/ide-assists/src/handlers/destructure_struct_binding.rs b/crates/ide-assists/src/handlers/destructure_struct_binding.rs
index 4c4cee1d78..0f5ef0548c 100644
--- a/crates/ide-assists/src/handlers/destructure_struct_binding.rs
+++ b/crates/ide-assists/src/handlers/destructure_struct_binding.rs
@@ -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()
diff --git a/crates/ide-assists/src/handlers/desugar_try_expr.rs b/crates/ide-assists/src/handlers/desugar_try_expr.rs
index 9976e34e73..4022030159 100644
--- a/crates/ide-assists/src/handlers/desugar_try_expr.rs
+++ b/crates/ide-assists/src/handlers/desugar_try_expr.rs
@@ -9,7 +9,6 @@ use syntax::{
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);
},
);
diff --git a/crates/ide-assists/src/handlers/extract_expressions_from_format_string.rs b/crates/ide-assists/src/handlers/extract_expressions_from_format_string.rs
index 61af2de6ec..35e8baa18a 100644
--- a/crates/ide-assists/src/handlers/extract_expressions_from_format_string.rs
+++ b/crates/ide-assists/src/handlers/extract_expressions_from_format_string.rs
@@ -8,7 +8,7 @@ use syntax::{
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());
diff --git a/crates/ide-assists/src/handlers/extract_variable.rs b/crates/ide-assists/src/handlers/extract_variable.rs
index 7c60184142..7071106970 100644
--- a/crates/ide-assists/src/handlers/extract_variable.rs
+++ b/crates/ide-assists/src/handlers/extract_variable.rs
@@ -9,7 +9,6 @@ use syntax::{
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 @@ impl 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
@@ -1748,6 +1777,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; } ");
}
diff --git a/crates/ide-assists/src/handlers/generate_delegate_methods.rs b/crates/ide-assists/src/handlers/generate_delegate_methods.rs
index c1eb1a74ec..63033c7d5e 100644
--- a/crates/ide-assists/src/handlers/generate_delegate_methods.rs
+++ b/crates/ide-assists/src/handlers/generate_delegate_methods.rs
@@ -4,7 +4,7 @@ use syntax::{
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);
},
)?;
diff --git a/crates/ide-assists/src/handlers/generate_delegate_trait.rs b/crates/ide-assists/src/handlers/generate_delegate_trait.rs
index 921f04f2a5..f703e4dc4a 100644
--- a/crates/ide-assists/src/handlers/generate_delegate_trait.rs
+++ b/crates/ide-assists/src/handlers/generate_delegate_trait.rs
@@ -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(),
};
diff --git a/crates/ide-assists/src/handlers/generate_trait_from_impl.rs b/crates/ide-assists/src/handlers/generate_trait_from_impl.rs
index 8bc4d50cf6..1286abe356 100644
--- a/crates/ide-assists/src/handlers/generate_trait_from_impl.rs
+++ b/crates/ide-assists/src/handlers/generate_trait_from_impl.rs
@@ -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 @@ use syntax::{
// };
// }
//
-// 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 @@ use syntax::{
// 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 @@ impl F$0oo {
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 @@ impl F$0oo {
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,11 +435,11 @@ mod a {
}"#,
r#"
mod a {
- trait NewTrait {
+ trait Foo {
fn foo();
}
- impl NewTrait for S {
+ impl Foo for S {
fn foo() {}
}
}"#,
@@ -385,6 +447,28 @@ mod a {
}
#[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(
generate_trait_from_impl,
diff --git a/crates/ide-assists/src/handlers/inline_local_variable.rs b/crates/ide-assists/src/handlers/inline_local_variable.rs
index 5d4bdc6ec7..f55ef4229e 100644
--- a/crates/ide-assists/src/handlers/inline_local_variable.rs
+++ b/crates/ide-assists/src/handlers/inline_local_variable.rs
@@ -1,3 +1,4 @@
+use either::{Either, for_both};
use hir::{PathResolution, Semantics};
use ide_db::{
EditionedFileId, RootDatabase,
@@ -5,8 +6,9 @@ use ide_db::{
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};
@@ -405,6 +429,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);
check_assist_not_applicable(
@@ -817,6 +873,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(
inline_local_variable,
diff --git a/crates/ide-assists/src/handlers/inline_type_alias.rs b/crates/ide-assists/src/handlers/inline_type_alias.rs
index c7a48f3261..f3ebe61078 100644
--- a/crates/ide-assists/src/handlers/inline_type_alias.rs
+++ b/crates/ide-assists/src/handlers/inline_type_alias.rs
@@ -12,7 +12,7 @@ use itertools::Itertools;
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() == "'_" {
+ // 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()));
+
+ // 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() {
- removals.push(ws.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};
diff --git a/crates/ide-assists/src/handlers/invert_if.rs b/crates/ide-assists/src/handlers/invert_if.rs
index bf82d8df9b..c8cb7bb60f 100644
--- a/crates/ide-assists/src/handlers/invert_if.rs
+++ b/crates/ide-assists/src/handlers/invert_if.rs
@@ -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();
diff --git a/crates/ide-assists/src/handlers/merge_imports.rs b/crates/ide-assists/src/handlers/merge_imports.rs
index 9ba73d23dd..42bc05811f 100644
--- a/crates/ide-assists/src/handlers/merge_imports.rs
+++ b/crates/ide-assists/src/handlers/merge_imports.rs
@@ -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! {
@@ -678,6 +679,25 @@ use std::fmt::Result;
}
#[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);
check_assist(
diff --git a/crates/ide-assists/src/handlers/move_guard.rs b/crates/ide-assists/src/handlers/move_guard.rs
index b4c347ff36..80587372e5 100644
--- a/crates/ide-assists/src/handlers/move_guard.rs
+++ b/crates/ide-assists/src/handlers/move_guard.rs
@@ -3,7 +3,7 @@ use syntax::{
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());
diff --git a/crates/ide-assists/src/handlers/qualify_method_call.rs b/crates/ide-assists/src/handlers/qualify_method_call.rs
index 495a84d62b..8b9e6570e9 100644
--- a/crates/ide-assists/src/handlers/qualify_method_call.rs
+++ b/crates/ide-assists/src/handlers/qualify_method_call.rs
@@ -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(())
diff --git a/crates/ide-assists/src/handlers/qualify_path.rs b/crates/ide-assists/src/handlers/qualify_path.rs
index b3cf296965..c059f758c4 100644
--- a/crates/ide-assists/src/handlers/qualify_path.rs
+++ b/crates/ide-assists/src/handlers/qualify_path.rs
@@ -11,7 +11,8 @@ use syntax::Edition;
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 @@ impl QualifyCandidate<'_> {
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 @@ impl QualifyCandidate<'_> {
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 @@ impl QualifyCandidate<'_> {
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 @@ impl QualifyCandidate<'_> {
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)
}
}
diff --git a/crates/ide-assists/src/handlers/replace_is_method_with_if_let_method.rs b/crates/ide-assists/src/handlers/replace_is_method_with_if_let_method.rs
index d22e951b5d..38d8c38ef2 100644
--- a/crates/ide-assists/src/handlers/replace_is_method_with_if_let_method.rs
+++ b/crates/ide-assists/src/handlers/replace_is_method_with_if_let_method.rs
@@ -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)
}
}
@@ -234,6 +244,54 @@ fn main() {
}
"#,
);
+
+ 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) {}
+}
+"#,
+ );
}
#[test]
diff --git a/crates/ide-assists/src/handlers/replace_let_with_if_let.rs b/crates/ide-assists/src/handlers/replace_let_with_if_let.rs
index 5587f1b59c..6ff5f0bbd3 100644
--- a/crates/ide-assists/src/handlers/replace_let_with_if_let.rs
+++ b/crates/ide-assists/src/handlers/replace_let_with_if_let.rs
@@ -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;
diff --git a/crates/ide-assists/src/handlers/replace_method_eager_lazy.rs b/crates/ide-assists/src/handlers/replace_method_eager_lazy.rs
index 6ca3e26ca0..6e4dd8cb73 100644
--- a/crates/ide-assists/src/handlers/replace_method_eager_lazy.rs
+++ b/crates/ide-assists/src/handlers/replace_method_eager_lazy.rs
@@ -2,10 +2,10 @@ use hir::Semantics;
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;
diff --git a/crates/ide-assists/src/handlers/unwrap_block.rs b/crates/ide-assists/src/handlers/unwrap_block.rs
index e4f5e3523b..e029d7884f 100644
--- a/crates/ide-assists/src/handlers/unwrap_block.rs
+++ b/crates/ide-assists/src/handlers/unwrap_block.rs
@@ -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());
@@ -557,6 +558,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(
unwrap_block,
diff --git a/crates/ide-assists/src/handlers/unwrap_tuple.rs b/crates/ide-assists/src/handlers/unwrap_tuple.rs
index 46f3e85e12..e03274bbb3 100644
--- a/crates/ide-assists/src/handlers/unwrap_tuple.rs
+++ b/crates/ide-assists/src/handlers/unwrap_tuple.rs
@@ -1,3 +1,6 @@
+use std::iter;
+
+use either::Either;
use syntax::{
AstNode, T,
ast::{self, edit::AstNodeEdit},
@@ -24,11 +27,16 @@ use crate::{AssistContext, AssistId, Assists};
// ```
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));
},
)
}
@@ -124,6 +126,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(
unwrap_tuple,
diff --git a/crates/ide-assists/src/handlers/wrap_unwrap_cfg_attr.rs b/crates/ide-assists/src/handlers/wrap_unwrap_cfg_attr.rs
index 7d5740b748..36df4af31d 100644
--- a/crates/ide-assists/src/handlers/wrap_unwrap_cfg_attr.rs
+++ b/crates/ide-assists/src/handlers/wrap_unwrap_cfg_attr.rs
@@ -2,7 +2,7 @@ use ide_db::source_change::SourceChangeBuilder;
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 @@ use crate::{AssistContext, AssistId, Assists};
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 @@ mod tests {
}
"#,
);
+ 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() {
diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs
index 3040509000..8ba46799d5 100644
--- a/crates/ide-assists/src/tests/generated.rs
+++ b/crates/ide-assists/src/tests/generated.rs
@@ -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;
diff --git a/crates/ide-assists/src/utils.rs b/crates/ide-assists/src/utils.rs
index b4055e77cc..0657e7243a 100644
--- a/crates/ide-assists/src/utils.rs
+++ b/crates/ide-assists/src/utils.rs
@@ -25,6 +25,7 @@ use syntax::{
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,
diff --git a/crates/ide-assists/src/utils/ref_field_expr.rs b/crates/ide-assists/src/utils/ref_field_expr.rs
index df8ad41112..fc9bf210e4 100644
--- a/crates/ide-assists/src/utils/ref_field_expr.rs
+++ b/crates/ide-assists/src/utils/ref_field_expr.rs
@@ -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
diff --git a/crates/ide-completion/src/completions/fn_param.rs b/crates/ide-completion/src/completions/fn_param.rs
index 96dac66b8a..bd0b69215c 100644
--- a/crates/ide-completion/src/completions/fn_param.rs
+++ b/crates/ide-completion/src/completions/fn_param.rs
@@ -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()
+}
diff --git a/crates/ide-completion/src/completions/postfix.rs b/crates/ide-completion/src/completions/postfix.rs
index ea53aef40c..5b91e7c456 100644
--- a/crates/ide-completion/src/completions/postfix.rs
+++ b/crates/ide-completion/src/completions/postfix.rs
@@ -16,7 +16,7 @@ use itertools::Itertools;
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() {
@@ -773,6 +804,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(
"ifl",
@@ -966,6 +1014,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(
"lete",
@@ -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 @@ use core::ops::ControlFlow;
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);
+ };
+}
+"#,
+ );
+ }
}
diff --git a/crates/ide-completion/src/context/analysis.rs b/crates/ide-completion/src/context/analysis.rs
index 4b0cc0c7cd..bf899539a2 100644
--- a/crates/ide-completion/src/context/analysis.rs
+++ b/crates/ide-completion/src/context/analysis.rs
@@ -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,
};
diff --git a/crates/ide-completion/src/context/tests.rs b/crates/ide-completion/src/context/tests.rs
index e97d9720e3..94d904932a 100644
--- a/crates/ide-completion/src/context/tests.rs
+++ b/crates/ide-completion/src/context/tests.rs
@@ -288,6 +288,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);
check_expected_type_and_name(
diff --git a/crates/ide-completion/src/render/function.rs b/crates/ide-completion/src/render/function.rs
index 475e00dfcf..dfa30841e7 100644
--- a/crates/ide-completion/src/render/function.rs
+++ b/crates/ide-completion/src/render/function.rs
@@ -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)
diff --git a/crates/ide-completion/src/tests/fn_param.rs b/crates/ide-completion/src/tests/fn_param.rs
index d6d73da3f1..aaa225642c 100644
--- a/crates/ide-completion/src/tests/fn_param.rs
+++ b/crates/ide-completion/src/tests/fn_param.rs
@@ -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
"#]],
diff --git a/crates/ide-diagnostics/src/handlers/no_such_field.rs b/crates/ide-diagnostics/src/handlers/no_such_field.rs
index bcfe3a8aa5..619bb2307c 100644
--- a/crates/ide-diagnostics/src/handlers/no_such_field.rs
+++ b/crates/ide-diagnostics/src/handlers/no_such_field.rs
@@ -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(
@@ -335,6 +347,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(
r#"
diff --git a/crates/ide-diagnostics/src/handlers/non_exhaustive_let.rs b/crates/ide-diagnostics/src/handlers/non_exhaustive_let.rs
index c86ecd2f03..bc10e82854 100644
--- a/crates/ide-diagnostics/src/handlers/non_exhaustive_let.rs
+++ b/crates/ide-diagnostics/src/handlers/non_exhaustive_let.rs
@@ -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 @@ mod tests {
//- 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 main() {
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
}
"#,
);
@@ -133,6 +203,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(
r#"
diff --git a/crates/ide-diagnostics/src/handlers/remove_unnecessary_else.rs b/crates/ide-diagnostics/src/handlers/remove_unnecessary_else.rs
index 7dc5b5b45e..04f48ae3db 100644
--- a/crates/ide-diagnostics/src/handlers/remove_unnecessary_else.rs
+++ b/crates/ide-diagnostics/src/handlers/remove_unnecessary_else.rs
@@ -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
diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs
index 95fcfce291..8753eab43a 100644
--- a/crates/load-cargo/src/lib.rs
+++ b/crates/load-cargo/src/lib.rs
@@ -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
diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs
index 9b9111012b..4ea136afbb 100644
--- a/crates/project-model/src/project_json.rs
+++ b/crates/project-model/src/project_json.rs
@@ -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 @@ impl From<RunnableKindData> for RunnableKind {
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,
}
}
}
diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs
index a03ed562e1..395cea6f76 100644
--- a/crates/project-model/src/tests.rs
+++ b/crates/project-model/src/tests.rs
@@ -193,6 +193,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");
check_crate_graph(crate_graph, expect_file!["../test_data/output/rust_project_cfg_groups.txt"]);
diff --git a/crates/project-model/test_data/labeled-project.json b/crates/project-model/test_data/labeled-project.json
new file mode 100644
index 0000000000..5c0e1f3397
--- /dev/null
+++ b/crates/project-model/test_data/labeled-project.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$"
+ }
+ ]
+}
diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs
index 82f04aa78e..ad1cca08cb 100644
--- a/crates/rust-analyzer/src/cli/analysis_stats.rs
+++ b/crates/rust-analyzer/src/cli/analysis_stats.rs
@@ -91,6 +91,7 @@ impl flags::AnalysisStats {
}
},
prefill_caches: false,
+ num_worker_threads: 1,
proc_macro_processes: 1,
};
diff --git a/crates/rust-analyzer/src/cli/diagnostics.rs b/crates/rust-analyzer/src/cli/diagnostics.rs
index 575c77f842..efbaad3c49 100644
--- a/crates/rust-analyzer/src/cli/diagnostics.rs
+++ b/crates/rust-analyzer/src/cli/diagnostics.rs
@@ -41,6 +41,7 @@ impl flags::Diagnostics {
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) =
diff --git a/crates/rust-analyzer/src/cli/flags.rs b/crates/rust-analyzer/src/cli/flags.rs
index d68f7ab5b7..03849938f5 100644
--- a/crates/rust-analyzer/src/cli/flags.rs
+++ b/crates/rust-analyzer/src/cli/flags.rs
@@ -191,6 +191,9 @@ xflags::xflags! {
/// 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 {
diff --git a/crates/rust-analyzer/src/cli/lsif.rs b/crates/rust-analyzer/src/cli/lsif.rs
index e5e238db63..3950a581fd 100644
--- a/crates/rust-analyzer/src/cli/lsif.rs
+++ b/crates/rust-analyzer/src/cli/lsif.rs
@@ -293,6 +293,7 @@ impl flags::Lsif {
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));
diff --git a/crates/rust-analyzer/src/cli/prime_caches.rs b/crates/rust-analyzer/src/cli/prime_caches.rs
index d5da679179..beedcfae4e 100644
--- a/crates/rust-analyzer/src/cli/prime_caches.rs
+++ b/crates/rust-analyzer/src/cli/prime_caches.rs
@@ -38,6 +38,7 @@ impl flags::PrimeCaches {
// 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(),
};
diff --git a/crates/rust-analyzer/src/cli/run_tests.rs b/crates/rust-analyzer/src/cli/run_tests.rs
index d4a56d773e..e8c88cadf6 100644
--- a/crates/rust-analyzer/src/cli/run_tests.rs
+++ b/crates/rust-analyzer/src/cli/run_tests.rs
@@ -23,6 +23,7 @@ impl flags::RunTests {
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) =
diff --git a/crates/rust-analyzer/src/cli/rustc_tests.rs b/crates/rust-analyzer/src/cli/rustc_tests.rs
index e8c6c5f4d4..49f28352b6 100644
--- a/crates/rust-analyzer/src/cli/rustc_tests.rs
+++ b/crates/rust-analyzer/src/cli/rustc_tests.rs
@@ -103,6 +103,7 @@ impl Tester {
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) =
diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs
index ed0476697c..ef6d4399e6 100644
--- a/crates/rust-analyzer/src/cli/scip.rs
+++ b/crates/rust-analyzer/src/cli/scip.rs
@@ -52,6 +52,7 @@ impl flags::Scip {
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);
diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs
index 6bc0792daa..7b00aebbfc 100644
--- a/crates/rust-analyzer/src/cli/ssr.rs
+++ b/crates/rust-analyzer/src/cli/ssr.rs
@@ -20,6 +20,7 @@ impl flags::Ssr {
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 @@ impl flags::Search {
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(
diff --git a/crates/rust-analyzer/src/cli/unresolved_references.rs b/crates/rust-analyzer/src/cli/unresolved_references.rs
index 49c6fcb91e..2d9b870f4d 100644
--- a/crates/rust-analyzer/src/cli/unresolved_references.rs
+++ b/crates/rust-analyzer/src/cli/unresolved_references.rs
@@ -44,6 +44,7 @@ impl flags::UnresolvedReferences {
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) =
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 0dda7f3cc2..2ccd85f0e3 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -948,18 +948,18 @@ config_data! {
/// 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 @@ config_data! {
/// 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
diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs
index d16ca2fb48..6a74b8a54d 100644
--- a/crates/rust-analyzer/src/integrated_benchmarks.rs
+++ b/crates/rust-analyzer/src/integrated_benchmarks.rs
@@ -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,
};
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 64decc9e0d..83edbc722b 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -303,6 +303,15 @@ impl GlobalState {
.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 @@ impl GlobalState {
));
}
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 @@ impl GlobalState {
&& 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 @@ impl GlobalState {
} => 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 {
diff --git a/crates/rust-analyzer/src/target_spec.rs b/crates/rust-analyzer/src/target_spec.rs
index b8d9acc02a..8be061cacf 100644
--- a/crates/rust-analyzer/src/target_spec.rs
+++ b/crates/rust-analyzer/src/target_spec.rs
@@ -6,7 +6,7 @@ use cargo_metadata::PackageId;
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 @@ impl CargoTargetSpec {
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 @@ impl CargoTargetSpec {
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) {
diff --git a/crates/stdx/src/lib.rs b/crates/stdx/src/lib.rs
index 7ab26b1890..a1af4cc6be 100644
--- a/crates/stdx/src/lib.rs
+++ b/crates/stdx/src/lib.rs
@@ -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,
@@ -367,6 +381,37 @@ mod tests {
}
#[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]
fn test_replace(src: &str, from: char, to: &str, expected: &str) {
diff --git a/crates/syntax/src/algo.rs b/crates/syntax/src/algo.rs
index 3ab9c90262..c679921b3f 100644
--- a/crates/syntax/src/algo.rs
+++ b/crates/syntax/src/algo.rs
@@ -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
+}
diff --git a/crates/syntax/src/ast/edit.rs b/crates/syntax/src/ast/edit.rs
index 9b30642fe4..194d06900a 100644
--- a/crates/syntax/src/ast/edit.rs
+++ b/crates/syntax/src/ast/edit.rs
@@ -43,6 +43,12 @@ impl ops::Add<u8> for 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)
diff --git a/crates/syntax/src/ast/syntax_factory/constructors.rs b/crates/syntax/src/ast/syntax_factory/constructors.rs
index 50fe565380..55c80ed167 100644
--- a/crates/syntax/src/ast/syntax_factory/constructors.rs
+++ b/crates/syntax/src/ast/syntax_factory/constructors.rs
@@ -5,7 +5,7 @@ use crate::{
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
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 @@ impl SyntaxFactory {
unreachable!()
};
- if let Some(mut mapping) = self.mappings() {
- SyntaxMappingBuilder::new(ast.syntax().clone()).finish(&mut mapping);
- }
-
ast
}
@@ -1765,6 +1927,65 @@ impl SyntaxFactory {
}
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
diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md
index 8460c2c7d0..35fba5accd 100644
--- a/docs/book/src/configuration_generated.md
+++ b/docs/book/src/configuration_generated.md
@@ -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}
diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json
index be0bdc8d55..047dbba11f 100644
--- a/editors/code/package-lock.json
+++ b/editors/code/package-lock.json
@@ -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": {
diff --git a/editors/code/package.json b/editors/code/package.json
index fc20597e88..1dd513c9de 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -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",