Auto merge of #15874 - DropDemBits:structured-snippet-migrate-4, r=Veykril

internal: Migrate assists to the structured snippet API, part 4

Continuing from #15260

Migrates the following assists:
- `add_turbo_fish`
- `add_type_ascription`
- `destructure_tuple_binding`
- `destructure_tuple_binding_in_subpattern`

I did this a while ago, but forgot to make a PR for the changes until now. 😅
This commit is contained in:
bors 2023-11-15 09:54:45 +00:00
commit 535eb0da9d
5 changed files with 433 additions and 141 deletions

View file

@ -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()
}
None => {
let fish_head = std::iter::repeat("_").take(number_of_arguments).format(", ");
let snip = format!("::<{fish_head}>");
builder.insert(ident.text_range().end(), snip);
Either::Right(method_call) => {
edit.make_mut(method_call).get_or_create_generic_arg_list()
}
};
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::*;
@ -363,6 +405,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(

View file

@ -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,
) {
if let Some(usages) = data.usages.as_ref() {
for (file_id, refs) in usages.iter() {
builder.edit_file(*file_id);
) -> Option<Vec<EditTupleUsage>> {
let mut current_file_usages = None;
for r in refs {
edit_tuple_usage(ctx, builder, r, data, in_sub_pattern);
if let Some(usages) = data.usages.as_ref() {
// 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 (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)]

View file

@ -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) {

View file

@ -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() {
@ -889,6 +997,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();

View file

@ -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",
)
});