about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/ide-assists/src/handlers/add_turbo_fish.rs140
-rw-r--r--crates/ide-assists/src/handlers/destructure_tuple_binding.rs241
-rw-r--r--crates/ide-assists/src/tests.rs5
-rw-r--r--crates/syntax/src/ast/edit_in_place.rs177
-rw-r--r--crates/syntax/src/ast/make.rs9
5 files changed, 432 insertions, 140 deletions
diff --git a/crates/ide-assists/src/handlers/add_turbo_fish.rs b/crates/ide-assists/src/handlers/add_turbo_fish.rs
index 36f68d17677..88fd0b1b733 100644
--- a/crates/ide-assists/src/handlers/add_turbo_fish.rs
+++ b/crates/ide-assists/src/handlers/add_turbo_fish.rs
@@ -1,6 +1,9 @@
+use either::Either;
 use ide_db::defs::{Definition, NameRefClass};
-use itertools::Itertools;
-use syntax::{ast, AstNode, SyntaxKind, T};
+use syntax::{
+    ast::{self, make, HasArgList},
+    ted, AstNode,
+};
 
 use crate::{
     assist_context::{AssistContext, Assists},
@@ -25,21 +28,45 @@ use crate::{
 // }
 // ```
 pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
-    let ident = ctx.find_token_syntax_at_offset(SyntaxKind::IDENT).or_else(|| {
-        let arg_list = ctx.find_node_at_offset::<ast::ArgList>()?;
-        if arg_list.args().next().is_some() {
-            return None;
-        }
-        cov_mark::hit!(add_turbo_fish_after_call);
-        cov_mark::hit!(add_type_ascription_after_call);
-        arg_list.l_paren_token()?.prev_token().filter(|it| it.kind() == SyntaxKind::IDENT)
-    })?;
-    let next_token = ident.next_token()?;
-    if next_token.kind() == T![::] {
+    let turbofish_target =
+        ctx.find_node_at_offset::<ast::PathSegment>().map(Either::Left).or_else(|| {
+            let callable_expr = ctx.find_node_at_offset::<ast::CallableExpr>()?;
+
+            if callable_expr.arg_list()?.args().next().is_some() {
+                return None;
+            }
+
+            cov_mark::hit!(add_turbo_fish_after_call);
+            cov_mark::hit!(add_type_ascription_after_call);
+
+            match callable_expr {
+                ast::CallableExpr::Call(it) => {
+                    let ast::Expr::PathExpr(path) = it.expr()? else {
+                        return None;
+                    };
+
+                    Some(Either::Left(path.path()?.segment()?))
+                }
+                ast::CallableExpr::MethodCall(it) => Some(Either::Right(it)),
+            }
+        })?;
+
+    let already_has_turbofish = match &turbofish_target {
+        Either::Left(path_segment) => path_segment.generic_arg_list().is_some(),
+        Either::Right(method_call) => method_call.generic_arg_list().is_some(),
+    };
+
+    if already_has_turbofish {
         cov_mark::hit!(add_turbo_fish_one_fish_is_enough);
         return None;
     }
-    let name_ref = ast::NameRef::cast(ident.parent()?)?;
+
+    let name_ref = match &turbofish_target {
+        Either::Left(path_segment) => path_segment.name_ref()?,
+        Either::Right(method_call) => method_call.name_ref()?,
+    };
+    let ident = name_ref.ident_token()?;
+
     let def = match NameRefClass::classify(&ctx.sema, &name_ref)? {
         NameRefClass::Definition(def) => def,
         NameRefClass::FieldShorthand { .. } | NameRefClass::ExternCrateShorthand { .. } => {
@@ -58,20 +85,27 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opti
 
     if let Some(let_stmt) = ctx.find_node_at_offset::<ast::LetStmt>() {
         if let_stmt.colon_token().is_none() {
-            let type_pos = let_stmt.pat()?.syntax().last_token()?.text_range().end();
-            let semi_pos = let_stmt.syntax().last_token()?.text_range().end();
+            if let_stmt.pat().is_none() {
+                return None;
+            }
 
             acc.add(
                 AssistId("add_type_ascription", AssistKind::RefactorRewrite),
                 "Add `: _` before assignment operator",
                 ident.text_range(),
-                |builder| {
+                |edit| {
+                    let let_stmt = edit.make_mut(let_stmt);
+
                     if let_stmt.semicolon_token().is_none() {
-                        builder.insert(semi_pos, ";");
+                        ted::append_child(let_stmt.syntax(), make::tokens::semicolon());
                     }
-                    match ctx.config.snippet_cap {
-                        Some(cap) => builder.insert_snippet(cap, type_pos, ": ${0:_}"),
-                        None => builder.insert(type_pos, ": _"),
+
+                    let placeholder_ty = make::ty_placeholder().clone_for_update();
+
+                    let_stmt.set_ty(Some(placeholder_ty.clone()));
+
+                    if let Some(cap) = ctx.config.snippet_cap {
+                        edit.add_placeholder_snippet(cap, placeholder_ty);
                     }
                 },
             )?
@@ -91,38 +125,46 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opti
         AssistId("add_turbo_fish", AssistKind::RefactorRewrite),
         "Add `::<>`",
         ident.text_range(),
-        |builder| {
-            builder.trigger_signature_help();
-            match ctx.config.snippet_cap {
-                Some(cap) => {
-                    let fish_head = get_snippet_fish_head(number_of_arguments);
-                    let snip = format!("::<{fish_head}>");
-                    builder.insert_snippet(cap, ident.text_range().end(), snip)
+        |edit| {
+            edit.trigger_signature_help();
+
+            let new_arg_list = match turbofish_target {
+                Either::Left(path_segment) => {
+                    edit.make_mut(path_segment).get_or_create_generic_arg_list()
+                }
+                Either::Right(method_call) => {
+                    edit.make_mut(method_call).get_or_create_generic_arg_list()
                 }
-                None => {
-                    let fish_head = std::iter::repeat("_").take(number_of_arguments).format(", ");
-                    let snip = format!("::<{fish_head}>");
-                    builder.insert(ident.text_range().end(), snip);
+            };
+
+            let fish_head = get_fish_head(number_of_arguments).clone_for_update();
+
+            // Note: we need to replace the `new_arg_list` instead of being able to use something like
+            // `GenericArgList::add_generic_arg` as `PathSegment::get_or_create_generic_arg_list`
+            // always creates a non-turbofish form generic arg list.
+            ted::replace(new_arg_list.syntax(), fish_head.syntax());
+
+            if let Some(cap) = ctx.config.snippet_cap {
+                for arg in fish_head.generic_args() {
+                    edit.add_placeholder_snippet(cap, arg)
                 }
             }
         },
     )
 }
 
-/// This will create a snippet string with tabstops marked
-fn get_snippet_fish_head(number_of_arguments: usize) -> String {
-    let mut fish_head = (1..number_of_arguments)
-        .format_with("", |i, f| f(&format_args!("${{{i}:_}}, ")))
-        .to_string();
-
-    // tabstop 0 is a special case and always the last one
-    fish_head.push_str("${0:_}");
-    fish_head
+/// This will create a turbofish generic arg list corresponding to the number of arguments
+fn get_fish_head(number_of_arguments: usize) -> ast::GenericArgList {
+    let args = (0..number_of_arguments).map(|_| make::type_arg(make::ty_placeholder()).into());
+    make::turbofish_generic_arg_list(args)
 }
 
 #[cfg(test)]
 mod tests {
-    use crate::tests::{check_assist, check_assist_by_label, check_assist_not_applicable};
+    use crate::tests::{
+        check_assist, check_assist_by_label, check_assist_not_applicable,
+        check_assist_not_applicable_by_label,
+    };
 
     use super::*;
 
@@ -364,6 +406,20 @@ fn main() {
     }
 
     #[test]
+    fn add_type_ascription_missing_pattern() {
+        check_assist_not_applicable_by_label(
+            add_turbo_fish,
+            r#"
+fn make<T>() -> T {}
+fn main() {
+    let = make$0()
+}
+"#,
+            "Add `: _` before assignment operator",
+        );
+    }
+
+    #[test]
     fn add_turbo_fish_function_lifetime_parameter() {
         check_assist(
             add_turbo_fish,
diff --git a/crates/ide-assists/src/handlers/destructure_tuple_binding.rs b/crates/ide-assists/src/handlers/destructure_tuple_binding.rs
index f30ca2552d3..65b497e83aa 100644
--- a/crates/ide-assists/src/handlers/destructure_tuple_binding.rs
+++ b/crates/ide-assists/src/handlers/destructure_tuple_binding.rs
@@ -3,10 +3,12 @@ use ide_db::{
     defs::Definition,
     search::{FileReference, SearchScope, UsageSearchResult},
 };
+use itertools::Itertools;
 use syntax::{
-    ast::{self, AstNode, FieldExpr, HasName, IdentPat, MethodCallExpr},
-    TextRange,
+    ast::{self, make, AstNode, FieldExpr, HasName, IdentPat, MethodCallExpr},
+    ted, T,
 };
+use text_edit::TextRange;
 
 use crate::assist_context::{AssistContext, Assists, SourceChangeBuilder};
 
@@ -61,27 +63,36 @@ pub(crate) fn destructure_tuple_binding_impl(
         acc.add(
             AssistId("destructure_tuple_binding_in_sub_pattern", AssistKind::RefactorRewrite),
             "Destructure tuple in sub-pattern",
-            data.range,
-            |builder| {
-                edit_tuple_assignment(ctx, builder, &data, true);
-                edit_tuple_usages(&data, builder, ctx, true);
-            },
+            data.ident_pat.syntax().text_range(),
+            |edit| destructure_tuple_edit_impl(ctx, edit, &data, true),
         );
     }
 
     acc.add(
         AssistId("destructure_tuple_binding", AssistKind::RefactorRewrite),
         if with_sub_pattern { "Destructure tuple in place" } else { "Destructure tuple" },
-        data.range,
-        |builder| {
-            edit_tuple_assignment(ctx, builder, &data, false);
-            edit_tuple_usages(&data, builder, ctx, false);
-        },
+        data.ident_pat.syntax().text_range(),
+        |edit| destructure_tuple_edit_impl(ctx, edit, &data, false),
     );
 
     Some(())
 }
 
+fn destructure_tuple_edit_impl(
+    ctx: &AssistContext<'_>,
+    edit: &mut SourceChangeBuilder,
+    data: &TupleData,
+    in_sub_pattern: bool,
+) {
+    let assignment_edit = edit_tuple_assignment(ctx, edit, &data, in_sub_pattern);
+    let current_file_usages_edit = edit_tuple_usages(&data, edit, ctx, in_sub_pattern);
+
+    assignment_edit.apply();
+    if let Some(usages_edit) = current_file_usages_edit {
+        usages_edit.into_iter().for_each(|usage_edit| usage_edit.apply(edit))
+    }
+}
+
 fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleData> {
     if ident_pat.at_token().is_some() {
         // Cannot destructure pattern with sub-pattern:
@@ -109,7 +120,6 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
     }
 
     let name = ident_pat.name()?.to_string();
-    let range = ident_pat.syntax().text_range();
 
     let usages = ctx.sema.to_def(&ident_pat).map(|def| {
         Definition::Local(def)
@@ -122,7 +132,7 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
         .map(|i| generate_name(ctx, i, &name, &ident_pat, &usages))
         .collect::<Vec<_>>();
 
-    Some(TupleData { ident_pat, range, ref_type, field_names, usages })
+    Some(TupleData { ident_pat, ref_type, field_names, usages })
 }
 
 fn generate_name(
@@ -142,72 +152,100 @@ enum RefType {
 }
 struct TupleData {
     ident_pat: IdentPat,
-    // name: String,
-    range: TextRange,
     ref_type: Option<RefType>,
     field_names: Vec<String>,
-    // field_types: Vec<Type>,
     usages: Option<UsageSearchResult>,
 }
 fn edit_tuple_assignment(
     ctx: &AssistContext<'_>,
-    builder: &mut SourceChangeBuilder,
+    edit: &mut SourceChangeBuilder,
     data: &TupleData,
     in_sub_pattern: bool,
-) {
+) -> AssignmentEdit {
+    let ident_pat = edit.make_mut(data.ident_pat.clone());
+
     let tuple_pat = {
         let original = &data.ident_pat;
         let is_ref = original.ref_token().is_some();
         let is_mut = original.mut_token().is_some();
-        let fields = data.field_names.iter().map(|name| {
-            ast::Pat::from(ast::make::ident_pat(is_ref, is_mut, ast::make::name(name)))
-        });
-        ast::make::tuple_pat(fields)
+        let fields = data
+            .field_names
+            .iter()
+            .map(|name| ast::Pat::from(make::ident_pat(is_ref, is_mut, make::name(name))));
+        make::tuple_pat(fields).clone_for_update()
     };
 
-    let add_cursor = |text: &str| {
-        // place cursor on first tuple item
-        let first_tuple = &data.field_names[0];
-        text.replacen(first_tuple, &format!("$0{first_tuple}"), 1)
-    };
+    if let Some(cap) = ctx.config.snippet_cap {
+        // place cursor on first tuple name
+        if let Some(ast::Pat::IdentPat(first_pat)) = tuple_pat.fields().next() {
+            edit.add_tabstop_before(
+                cap,
+                first_pat.name().expect("first ident pattern should have a name"),
+            )
+        }
+    }
 
-    // with sub_pattern: keep original tuple and add subpattern: `tup @ (_0, _1)`
-    if in_sub_pattern {
-        let text = format!(" @ {tuple_pat}");
-        match ctx.config.snippet_cap {
-            Some(cap) => {
-                let snip = add_cursor(&text);
-                builder.insert_snippet(cap, data.range.end(), snip);
-            }
-            None => builder.insert(data.range.end(), text),
-        };
-    } else {
-        let text = tuple_pat.to_string();
-        match ctx.config.snippet_cap {
-            Some(cap) => {
-                let snip = add_cursor(&text);
-                builder.replace_snippet(cap, data.range, snip);
-            }
-            None => builder.replace(data.range, text),
-        };
+    AssignmentEdit { ident_pat, tuple_pat, in_sub_pattern }
+}
+struct AssignmentEdit {
+    ident_pat: ast::IdentPat,
+    tuple_pat: ast::TuplePat,
+    in_sub_pattern: bool,
+}
+
+impl AssignmentEdit {
+    fn apply(self) {
+        // with sub_pattern: keep original tuple and add subpattern: `tup @ (_0, _1)`
+        if self.in_sub_pattern {
+            self.ident_pat.set_pat(Some(self.tuple_pat.into()))
+        } else {
+            ted::replace(self.ident_pat.syntax(), self.tuple_pat.syntax())
+        }
     }
 }
 
 fn edit_tuple_usages(
     data: &TupleData,
-    builder: &mut SourceChangeBuilder,
+    edit: &mut SourceChangeBuilder,
     ctx: &AssistContext<'_>,
     in_sub_pattern: bool,
-) {
+) -> Option<Vec<EditTupleUsage>> {
+    let mut current_file_usages = None;
+
     if let Some(usages) = data.usages.as_ref() {
-        for (file_id, refs) in usages.iter() {
-            builder.edit_file(*file_id);
+        // We need to collect edits first before actually applying them
+        // as mapping nodes to their mutable node versions requires an
+        // unmodified syntax tree.
+        //
+        // We also defer editing usages in the current file first since
+        // tree mutation in the same file breaks when `builder.edit_file`
+        // is called
+
+        if let Some((_, refs)) = usages.iter().find(|(file_id, _)| **file_id == ctx.file_id()) {
+            current_file_usages = Some(
+                refs.iter()
+                    .filter_map(|r| edit_tuple_usage(ctx, edit, r, data, in_sub_pattern))
+                    .collect_vec(),
+            );
+        }
 
-            for r in refs {
-                edit_tuple_usage(ctx, builder, r, data, in_sub_pattern);
+        for (file_id, refs) in usages.iter() {
+            if *file_id == ctx.file_id() {
+                continue;
             }
+
+            edit.edit_file(*file_id);
+
+            let tuple_edits = refs
+                .iter()
+                .filter_map(|r| edit_tuple_usage(ctx, edit, r, data, in_sub_pattern))
+                .collect_vec();
+
+            tuple_edits.into_iter().for_each(|tuple_edit| tuple_edit.apply(edit))
         }
     }
+
+    current_file_usages
 }
 fn edit_tuple_usage(
     ctx: &AssistContext<'_>,
@@ -215,25 +253,14 @@ fn edit_tuple_usage(
     usage: &FileReference,
     data: &TupleData,
     in_sub_pattern: bool,
-) {
+) -> Option<EditTupleUsage> {
     match detect_tuple_index(usage, data) {
-        Some(index) => edit_tuple_field_usage(ctx, builder, data, index),
-        None => {
-            if in_sub_pattern {
-                cov_mark::hit!(destructure_tuple_call_with_subpattern);
-                return;
-            }
-
-            // no index access -> make invalid -> requires handling by user
-            // -> put usage in block comment
-            //
-            // Note: For macro invocations this might result in still valid code:
-            //   When a macro accepts the tuple as argument, as well as no arguments at all,
-            //   uncommenting the tuple still leaves the macro call working (see `tests::in_macro_call::empty_macro`).
-            //   But this is an unlikely case. Usually the resulting macro call will become erroneous.
-            builder.insert(usage.range.start(), "/*");
-            builder.insert(usage.range.end(), "*/");
+        Some(index) => Some(edit_tuple_field_usage(ctx, builder, data, index)),
+        None if in_sub_pattern => {
+            cov_mark::hit!(destructure_tuple_call_with_subpattern);
+            return None;
         }
+        None => Some(EditTupleUsage::NoIndex(usage.range)),
     }
 }
 
@@ -242,19 +269,47 @@ fn edit_tuple_field_usage(
     builder: &mut SourceChangeBuilder,
     data: &TupleData,
     index: TupleIndex,
-) {
+) -> EditTupleUsage {
     let field_name = &data.field_names[index.index];
+    let field_name = make::expr_path(make::ext::ident_path(field_name));
 
     if data.ref_type.is_some() {
-        let ref_data = handle_ref_field_usage(ctx, &index.field_expr);
-        builder.replace(ref_data.range, ref_data.format(field_name));
+        let (replace_expr, ref_data) = handle_ref_field_usage(ctx, &index.field_expr);
+        let replace_expr = builder.make_mut(replace_expr);
+        EditTupleUsage::ReplaceExpr(replace_expr, ref_data.wrap_expr(field_name))
     } else {
-        builder.replace(index.range, field_name);
+        let field_expr = builder.make_mut(index.field_expr);
+        EditTupleUsage::ReplaceExpr(field_expr.into(), field_name)
+    }
+}
+enum EditTupleUsage {
+    /// no index access -> make invalid -> requires handling by user
+    /// -> put usage in block comment
+    ///
+    /// Note: For macro invocations this might result in still valid code:
+    ///   When a macro accepts the tuple as argument, as well as no arguments at all,
+    ///   uncommenting the tuple still leaves the macro call working (see `tests::in_macro_call::empty_macro`).
+    ///   But this is an unlikely case. Usually the resulting macro call will become erroneous.
+    NoIndex(TextRange),
+    ReplaceExpr(ast::Expr, ast::Expr),
+}
+
+impl EditTupleUsage {
+    fn apply(self, edit: &mut SourceChangeBuilder) {
+        match self {
+            EditTupleUsage::NoIndex(range) => {
+                edit.insert(range.start(), "/*");
+                edit.insert(range.end(), "*/");
+            }
+            EditTupleUsage::ReplaceExpr(target_expr, replace_with) => {
+                ted::replace(target_expr.syntax(), replace_with.clone_for_update().syntax())
+            }
+        }
     }
 }
+
 struct TupleIndex {
     index: usize,
-    range: TextRange,
     field_expr: FieldExpr,
 }
 fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIndex> {
@@ -296,7 +351,7 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
                 return None;
             }
 
-            Some(TupleIndex { index: idx, range: field_expr.syntax().text_range(), field_expr })
+            Some(TupleIndex { index: idx, field_expr })
         } else {
             // tuple index out of range
             None
@@ -307,32 +362,34 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
 }
 
 struct RefData {
-    range: TextRange,
     needs_deref: bool,
     needs_parentheses: bool,
 }
 impl RefData {
-    fn format(&self, field_name: &str) -> String {
-        match (self.needs_deref, self.needs_parentheses) {
-            (true, true) => format!("(*{field_name})"),
-            (true, false) => format!("*{field_name}"),
-            (false, true) => format!("({field_name})"),
-            (false, false) => field_name.to_string(),
+    fn wrap_expr(&self, mut expr: ast::Expr) -> ast::Expr {
+        if self.needs_deref {
+            expr = make::expr_prefix(T![*], expr);
         }
+
+        if self.needs_parentheses {
+            expr = make::expr_paren(expr);
+        }
+
+        return expr;
     }
 }
-fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> RefData {
+fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> (ast::Expr, RefData) {
     let s = field_expr.syntax();
-    let mut ref_data =
-        RefData { range: s.text_range(), needs_deref: true, needs_parentheses: true };
+    let mut ref_data = RefData { needs_deref: true, needs_parentheses: true };
+    let mut target_node = field_expr.clone().into();
 
     let parent = match s.parent().map(ast::Expr::cast) {
         Some(Some(parent)) => parent,
         Some(None) => {
             ref_data.needs_parentheses = false;
-            return ref_data;
+            return (target_node, ref_data);
         }
-        None => return ref_data,
+        None => return (target_node, ref_data),
     };
 
     match parent {
@@ -342,7 +399,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
             // there might be a ref outside: `&(t.0)` -> can be removed
             if let Some(it) = it.syntax().parent().and_then(ast::RefExpr::cast) {
                 ref_data.needs_deref = false;
-                ref_data.range = it.syntax().text_range();
+                target_node = it.into();
             }
         }
         ast::Expr::RefExpr(it) => {
@@ -351,8 +408,8 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
             ref_data.needs_parentheses = false;
             // might be surrounded by parens -> can be removed too
             match it.syntax().parent().and_then(ast::ParenExpr::cast) {
-                Some(parent) => ref_data.range = parent.syntax().text_range(),
-                None => ref_data.range = it.syntax().text_range(),
+                Some(parent) => target_node = parent.into(),
+                None => target_node = it.into(),
             };
         }
         // higher precedence than deref `*`
@@ -414,7 +471,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
         }
     };
 
-    ref_data
+    (target_node, ref_data)
 }
 
 #[cfg(test)]
diff --git a/crates/ide-assists/src/tests.rs b/crates/ide-assists/src/tests.rs
index 566384615bb..25b3d6d9da9 100644
--- a/crates/ide-assists/src/tests.rs
+++ b/crates/ide-assists/src/tests.rs
@@ -100,6 +100,11 @@ pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) {
     check(assist, ra_fixture, ExpectedResult::NotApplicable, None);
 }
 
+#[track_caller]
+pub(crate) fn check_assist_not_applicable_by_label(assist: Handler, ra_fixture: &str, label: &str) {
+    check(assist, ra_fixture, ExpectedResult::NotApplicable, Some(label));
+}
+
 /// Check assist in unresolved state. Useful to check assists for lazy computation.
 #[track_caller]
 pub(crate) fn check_assist_unresolved(assist: Handler, ra_fixture: &str) {
diff --git a/crates/syntax/src/ast/edit_in_place.rs b/crates/syntax/src/ast/edit_in_place.rs
index a85e1d1d9d0..37d8212042d 100644
--- a/crates/syntax/src/ast/edit_in_place.rs
+++ b/crates/syntax/src/ast/edit_in_place.rs
@@ -3,18 +3,17 @@
 use std::iter::{empty, successors};
 
 use parser::{SyntaxKind, T};
-use rowan::SyntaxElement;
 
 use crate::{
     algo::{self, neighbor},
     ast::{self, edit::IndentLevel, make, HasGenericParams},
     ted::{self, Position},
-    AstNode, AstToken, Direction,
+    AstNode, AstToken, Direction, SyntaxElement,
     SyntaxKind::{ATTR, COMMENT, WHITESPACE},
     SyntaxNode, SyntaxToken,
 };
 
-use super::HasName;
+use super::{HasArgList, HasName};
 
 pub trait GenericParamsOwnerEdit: ast::HasGenericParams {
     fn get_or_create_generic_param_list(&self) -> ast::GenericParamList;
@@ -362,6 +361,24 @@ impl ast::PathSegment {
     }
 }
 
+impl ast::MethodCallExpr {
+    pub fn get_or_create_generic_arg_list(&self) -> ast::GenericArgList {
+        if self.generic_arg_list().is_none() {
+            let generic_arg_list = make::turbofish_generic_arg_list(empty()).clone_for_update();
+
+            if let Some(arg_list) = self.arg_list() {
+                ted::insert_raw(
+                    ted::Position::before(arg_list.syntax()),
+                    generic_arg_list.syntax(),
+                );
+            } else {
+                ted::append_child(self.syntax(), generic_arg_list.syntax());
+            }
+        }
+        self.generic_arg_list().unwrap()
+    }
+}
+
 impl Removable for ast::UseTree {
     fn remove(&self) {
         for dir in [Direction::Next, Direction::Prev] {
@@ -559,7 +576,7 @@ impl ast::AssocItemList {
                 None => (IndentLevel::single(), Position::last_child_of(self.syntax()), "\n"),
             },
         };
-        let elements: Vec<SyntaxElement<_>> = vec![
+        let elements: Vec<SyntaxElement> = vec![
             make::tokens::whitespace(&format!("{whitespace}{indent}")).into(),
             item.syntax().clone().into(),
         ];
@@ -629,6 +646,50 @@ impl ast::MatchArmList {
     }
 }
 
+impl ast::LetStmt {
+    pub fn set_ty(&self, ty: Option<ast::Type>) {
+        match ty {
+            None => {
+                if let Some(colon_token) = self.colon_token() {
+                    ted::remove(colon_token);
+                }
+
+                if let Some(existing_ty) = self.ty() {
+                    if let Some(sibling) = existing_ty.syntax().prev_sibling_or_token() {
+                        if sibling.kind() == SyntaxKind::WHITESPACE {
+                            ted::remove(sibling);
+                        }
+                    }
+
+                    ted::remove(existing_ty.syntax());
+                }
+
+                // Remove any trailing ws
+                if let Some(last) = self.syntax().last_token().filter(|it| it.kind() == WHITESPACE)
+                {
+                    last.detach();
+                }
+            }
+            Some(new_ty) => {
+                if self.colon_token().is_none() {
+                    ted::insert_raw(
+                        Position::after(
+                            self.pat().expect("let stmt should have a pattern").syntax(),
+                        ),
+                        make::token(T![:]),
+                    );
+                }
+
+                if let Some(old_ty) = self.ty() {
+                    ted::replace(old_ty.syntax(), new_ty.syntax());
+                } else {
+                    ted::insert(Position::after(self.colon_token().unwrap()), new_ty.syntax());
+                }
+            }
+        }
+    }
+}
+
 impl ast::RecordExprFieldList {
     pub fn add_field(&self, field: ast::RecordExprField) {
         let is_multiline = self.syntax().text().contains_char('\n');
@@ -753,7 +814,7 @@ impl ast::VariantList {
                 None => (IndentLevel::single(), Position::last_child_of(self.syntax())),
             },
         };
-        let elements: Vec<SyntaxElement<_>> = vec![
+        let elements: Vec<SyntaxElement> = vec![
             make::tokens::whitespace(&format!("{}{indent}", "\n")).into(),
             variant.syntax().clone().into(),
             ast::make::token(T![,]).into(),
@@ -788,6 +849,53 @@ fn normalize_ws_between_braces(node: &SyntaxNode) -> Option<()> {
     Some(())
 }
 
+impl ast::IdentPat {
+    pub fn set_pat(&self, pat: Option<ast::Pat>) {
+        match pat {
+            None => {
+                if let Some(at_token) = self.at_token() {
+                    // Remove `@ Pat`
+                    let start = at_token.clone().into();
+                    let end = self
+                        .pat()
+                        .map(|it| it.syntax().clone().into())
+                        .unwrap_or_else(|| at_token.into());
+
+                    ted::remove_all(start..=end);
+
+                    // Remove any trailing ws
+                    if let Some(last) =
+                        self.syntax().last_token().filter(|it| it.kind() == WHITESPACE)
+                    {
+                        last.detach();
+                    }
+                }
+            }
+            Some(pat) => {
+                if let Some(old_pat) = self.pat() {
+                    // Replace existing pattern
+                    ted::replace(old_pat.syntax(), pat.syntax())
+                } else if let Some(at_token) = self.at_token() {
+                    // Have an `@` token but not a pattern yet
+                    ted::insert(ted::Position::after(at_token), pat.syntax());
+                } else {
+                    // Don't have an `@`, should have a name
+                    let name = self.name().unwrap();
+
+                    ted::insert_all(
+                        ted::Position::after(name.syntax()),
+                        vec![
+                            make::token(T![@]).into(),
+                            make::tokens::single_space().into(),
+                            pat.syntax().clone().into(),
+                        ],
+                    )
+                }
+            }
+        }
+    }
+}
+
 pub trait HasVisibilityEdit: ast::HasVisibility {
     fn set_visibility(&self, visbility: ast::Visibility) {
         match self.visibility() {
@@ -890,6 +998,65 @@ mod tests {
     }
 
     #[test]
+    fn test_ident_pat_set_pat() {
+        #[track_caller]
+        fn check(before: &str, expected: &str, pat: Option<ast::Pat>) {
+            let pat = pat.map(|it| it.clone_for_update());
+
+            let ident_pat = ast_mut_from_text::<ast::IdentPat>(&format!("fn f() {{ {before} }}"));
+            ident_pat.set_pat(pat);
+
+            let after = ast_mut_from_text::<ast::IdentPat>(&format!("fn f() {{ {expected} }}"));
+            assert_eq!(ident_pat.to_string(), after.to_string());
+        }
+
+        // replacing
+        check("let a @ _;", "let a @ ();", Some(make::tuple_pat([]).into()));
+
+        // note: no trailing semicolon is added for the below tests since it
+        // seems to be picked up by the ident pat during error recovery?
+
+        // adding
+        check("let a ", "let a @ ()", Some(make::tuple_pat([]).into()));
+        check("let a @ ", "let a @ ()", Some(make::tuple_pat([]).into()));
+
+        // removing
+        check("let a @ ()", "let a", None);
+        check("let a @ ", "let a", None);
+    }
+
+    #[test]
+    fn test_let_stmt_set_ty() {
+        #[track_caller]
+        fn check(before: &str, expected: &str, ty: Option<ast::Type>) {
+            let ty = ty.map(|it| it.clone_for_update());
+
+            let let_stmt = ast_mut_from_text::<ast::LetStmt>(&format!("fn f() {{ {before} }}"));
+            let_stmt.set_ty(ty);
+
+            let after = ast_mut_from_text::<ast::LetStmt>(&format!("fn f() {{ {expected} }}"));
+            assert_eq!(let_stmt.to_string(), after.to_string(), "{let_stmt:#?}\n!=\n{after:#?}");
+        }
+
+        // adding
+        check("let a;", "let a: ();", Some(make::ty_tuple([])));
+        // no semicolon due to it being eaten during error recovery
+        check("let a:", "let a: ()", Some(make::ty_tuple([])));
+
+        // replacing
+        check("let a: u8;", "let a: ();", Some(make::ty_tuple([])));
+        check("let a: u8 = 3;", "let a: () = 3;", Some(make::ty_tuple([])));
+        check("let a: = 3;", "let a: () = 3;", Some(make::ty_tuple([])));
+
+        // removing
+        check("let a: u8;", "let a;", None);
+        check("let a:;", "let a;", None);
+
+        check("let a: u8 = 3;", "let a = 3;", None);
+        check("let a: = 3;", "let a = 3;", None);
+    }
+
+    #[test]
     fn add_variant_to_empty_enum() {
         let variant = make::variant(make::name("Bar"), None).clone_for_update();
 
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs
index 31a858b91a7..ad63cc55862 100644
--- a/crates/syntax/src/ast/make.rs
+++ b/crates/syntax/src/ast/make.rs
@@ -941,6 +941,13 @@ pub fn lifetime_arg(lifetime: ast::Lifetime) -> ast::LifetimeArg {
     ast_from_text(&format!("const S: T<{lifetime}> = ();"))
 }
 
+pub fn turbofish_generic_arg_list(
+    args: impl IntoIterator<Item = ast::GenericArg>,
+) -> ast::GenericArgList {
+    let args = args.into_iter().join(", ");
+    ast_from_text(&format!("const S: T::<{args}> = ();"))
+}
+
 pub(crate) fn generic_arg_list(
     args: impl IntoIterator<Item = ast::GenericArg>,
 ) -> ast::GenericArgList {
@@ -1126,7 +1133,7 @@ pub mod tokens {
 
     pub(super) static SOURCE_FILE: Lazy<Parse<SourceFile>> = Lazy::new(|| {
         SourceFile::parse(
-            "const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p)\n;\n\n",
+            "const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p, { let a @ [] })\n;\n\n",
         )
     });