diff options
| -rw-r--r-- | crates/ide-assists/src/handlers/add_turbo_fish.rs | 140 | ||||
| -rw-r--r-- | crates/ide-assists/src/handlers/destructure_tuple_binding.rs | 241 | ||||
| -rw-r--r-- | crates/ide-assists/src/tests.rs | 5 | ||||
| -rw-r--r-- | crates/syntax/src/ast/edit_in_place.rs | 177 | ||||
| -rw-r--r-- | crates/syntax/src/ast/make.rs | 9 |
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", ) }); |
