Auto merge of #16813 - wyatt-herkamp:to-from-cfg_attr-assist, r=Veykril

Wrap/Unwrap cfg_attr

https://github.com/rust-lang/rust-analyzer/assets/11785959/f5f1bb71-22e7-438b-9a22-65ebab1b362d

https://github.com/rust-lang/rust-analyzer/assets/11785959/36933a4e-0000-455a-abe3-af774cd854d0

## TODO
- [x] Add Tests
- [x] Wrap derive elements

Closes #13965
This commit is contained in:
bors 2024-04-15 12:56:55 +00:00
commit 40bb8f3272
3 changed files with 603 additions and 0 deletions

View File

@ -0,0 +1,581 @@
use ide_db::source_change::SourceChangeBuilder;
use itertools::Itertools;
use syntax::{
algo,
ast::{self, make, AstNode},
ted::{self, Position},
NodeOrToken, SyntaxToken, TextRange, T,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: wrap_unwrap_cfg_attr
//
// Wraps an attribute to a cfg_attr attribute or unwraps a cfg_attr attribute to the inner attributes.
//
// ```
// #[derive$0(Debug)]
// struct S {
// field: i32
// }
// ```
// ->
// ```
// #[cfg_attr($0, derive(Debug))]
// struct S {
// field: i32
// }
enum WrapUnwrapOption {
WrapDerive { derive: TextRange, attr: ast::Attr },
WrapAttr(ast::Attr),
}
/// Attempts to get the derive attribute from a derive attribute list
///
/// This will collect all the tokens in the "path" within the derive attribute list
/// But a derive attribute list doesn't have paths. So we need to collect all the tokens before and after the ident
///
/// If this functions return None just map to WrapAttr
fn attempt_get_derive(attr: ast::Attr, ident: SyntaxToken) -> WrapUnwrapOption {
let attempt_attr = || {
{
let mut derive = ident.text_range();
// TokenTree is all the tokens between the `(` and `)`. They do not have paths. So a path `serde::Serialize` would be [Ident Colon Colon Ident]
// So lets say we have derive(Debug, serde::Serialize, Copy) ident would be on Serialize
// We need to grab all previous tokens until we find a `,` or `(` and all following tokens until we find a `,` or `)`
// We also want to consume the following comma if it exists
let mut prev = algo::skip_trivia_token(
ident.prev_sibling_or_token()?.into_token()?,
syntax::Direction::Prev,
)?;
let mut following = algo::skip_trivia_token(
ident.next_sibling_or_token()?.into_token()?,
syntax::Direction::Next,
)?;
if (prev.kind() == T![,] || prev.kind() == T!['('])
&& (following.kind() == T![,] || following.kind() == T![')'])
{
// This would be a single ident such as Debug. As no path is present
if following.kind() == T![,] {
derive = derive.cover(following.text_range());
} else if following.kind() == T![')'] && prev.kind() == T![,] {
derive = derive.cover(prev.text_range());
}
Some(WrapUnwrapOption::WrapDerive { derive, attr: attr.clone() })
} else {
let mut consumed_comma = false;
// Collect the path
while let Some(prev_token) = algo::skip_trivia_token(prev, syntax::Direction::Prev)
{
let kind = prev_token.kind();
if kind == T![,] {
consumed_comma = true;
derive = derive.cover(prev_token.text_range());
break;
} else if kind == T!['('] {
break;
} else {
derive = derive.cover(prev_token.text_range());
}
prev = prev_token.prev_sibling_or_token()?.into_token()?;
}
while let Some(next_token) =
algo::skip_trivia_token(following.clone(), syntax::Direction::Next)
{
let kind = next_token.kind();
match kind {
T![,] if !consumed_comma => {
derive = derive.cover(next_token.text_range());
break;
}
T![')'] | T![,] => break,
_ => derive = derive.cover(next_token.text_range()),
}
following = next_token.next_sibling_or_token()?.into_token()?;
}
Some(WrapUnwrapOption::WrapDerive { derive, attr: attr.clone() })
}
}
};
if ident.parent().and_then(ast::TokenTree::cast).is_none()
|| !attr.simple_name().map(|v| v.eq("derive")).unwrap_or_default()
{
WrapUnwrapOption::WrapAttr(attr)
} else {
attempt_attr().unwrap_or(WrapUnwrapOption::WrapAttr(attr))
}
}
pub(crate) fn wrap_unwrap_cfg_attr(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let option = if ctx.has_empty_selection() {
let ident = ctx.find_token_syntax_at_offset(T![ident]);
let attr = ctx.find_node_at_offset::<ast::Attr>();
match (attr, ident) {
(Some(attr), Some(ident))
if attr.simple_name().map(|v| v.eq("derive")).unwrap_or_default() =>
{
Some(attempt_get_derive(attr.clone(), ident))
}
(Some(attr), _) => Some(WrapUnwrapOption::WrapAttr(attr)),
_ => None,
}
} else {
let covering_element = ctx.covering_element();
match covering_element {
NodeOrToken::Node(node) => ast::Attr::cast(node).map(WrapUnwrapOption::WrapAttr),
NodeOrToken::Token(ident) if ident.kind() == syntax::T![ident] => {
let attr = ident.parent_ancestors().find_map(ast::Attr::cast)?;
Some(attempt_get_derive(attr.clone(), ident))
}
_ => None,
}
}?;
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::WrapDerive { derive, attr } => wrap_derive(acc, ctx, attr, derive),
}
}
fn wrap_derive(
acc: &mut Assists,
ctx: &AssistContext<'_>,
attr: ast::Attr,
derive_element: TextRange,
) -> Option<()> {
let range = attr.syntax().text_range();
let token_tree = attr.token_tree()?;
let mut path_text = String::new();
let mut cfg_derive_tokens = Vec::new();
let mut new_derive = Vec::new();
for tt in token_tree.token_trees_and_tokens() {
let NodeOrToken::Token(token) = tt else {
continue;
};
if token.kind() == T!['('] || token.kind() == T![')'] {
continue;
}
if derive_element.contains_range(token.text_range()) {
if token.kind() != T![,] && token.kind() != syntax::SyntaxKind::WHITESPACE {
path_text.push_str(token.text());
cfg_derive_tokens.push(NodeOrToken::Token(token));
}
} else {
new_derive.push(NodeOrToken::Token(token));
}
}
let handle_source_change = |edit: &mut SourceChangeBuilder| {
let new_derive = make::attr_outer(make::meta_token_tree(
make::ext::ident_path("derive"),
make::token_tree(T!['('], new_derive),
))
.clone_for_update();
let meta = make::meta_token_tree(
make::ext::ident_path("cfg_attr"),
make::token_tree(
T!['('],
vec![
NodeOrToken::Token(make::token(T![,])),
NodeOrToken::Token(make::tokens::whitespace(" ")),
NodeOrToken::Token(make::tokens::ident("derive")),
NodeOrToken::Node(make::token_tree(T!['('], cfg_derive_tokens)),
],
),
);
// Remove the derive attribute
let edit_attr = edit.make_syntax_mut(attr.syntax().clone());
ted::replace(edit_attr, new_derive.syntax().clone());
let cfg_attr = make::attr_outer(meta).clone_for_update();
ted::insert_all_raw(
Position::after(new_derive.syntax().clone()),
vec![make::tokens::whitespace("\n").into(), cfg_attr.syntax().clone().into()],
);
if let Some(snippet_cap) = ctx.config.snippet_cap {
if let Some(first_meta) =
cfg_attr.meta().and_then(|meta| meta.token_tree()).and_then(|tt| tt.l_paren_token())
{
edit.add_tabstop_after_token(snippet_cap, first_meta)
}
}
};
acc.add(
AssistId("wrap_unwrap_cfg_attr", AssistKind::Refactor),
format!("Wrap #[derive({path_text})] in `cfg_attr`",),
range,
handle_source_change,
);
Some(())
}
fn wrap_cfg_attr(acc: &mut Assists, ctx: &AssistContext<'_>, attr: ast::Attr) -> Option<()> {
let range = attr.syntax().text_range();
let path = attr.path()?;
let handle_source_change = |edit: &mut SourceChangeBuilder| {
let mut raw_tokens = vec![
NodeOrToken::Token(make::token(T![,])),
NodeOrToken::Token(make::tokens::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::tokens::whitespace(" ")));
raw_tokens.push(NodeOrToken::Token(eq.clone()));
raw_tokens.push(NodeOrToken::Token(make::tokens::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());
}
}
let meta = make::meta_token_tree(
make::ext::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)
}
.clone_for_update();
let attr_syntax = edit.make_syntax_mut(attr.syntax().clone());
ted::replace(attr_syntax, cfg_attr.syntax());
if let Some(snippet_cap) = ctx.config.snippet_cap {
if let Some(first_meta) =
cfg_attr.meta().and_then(|meta| meta.token_tree()).and_then(|tt| tt.l_paren_token())
{
edit.add_tabstop_after_token(snippet_cap, first_meta)
}
}
};
acc.add(
AssistId("wrap_unwrap_cfg_attr", AssistKind::Refactor),
"Convert to `cfg_attr`",
range,
handle_source_change,
);
Some(())
}
fn unwrap_cfg_attr(acc: &mut Assists, attr: ast::Attr) -> Option<()> {
let range = attr.syntax().text_range();
let meta = attr.meta()?;
let meta_tt = meta.token_tree()?;
let mut inner_attrs = Vec::with_capacity(1);
let mut found_comma = false;
let mut iter = meta_tt.token_trees_and_tokens().skip(1).peekable();
while let Some(tt) = iter.next() {
if let NodeOrToken::Token(token) = &tt {
if token.kind() == T![')'] {
break;
}
if token.kind() == T![,] {
found_comma = true;
continue;
}
}
if !found_comma {
continue;
}
let Some(attr_name) = tt.into_token().and_then(|token| {
if token.kind() == T![ident] {
Some(make::ext::ident_path(token.text()))
} else {
None
}
}) else {
continue;
};
let next_tt = iter.next()?;
let meta = match next_tt {
NodeOrToken::Node(tt) => make::meta_token_tree(attr_name, tt),
NodeOrToken::Token(token) if token.kind() == T![,] || token.kind() == T![')'] => {
make::meta_path(attr_name)
}
NodeOrToken::Token(token) => {
let equals = algo::skip_trivia_token(token, syntax::Direction::Next)?;
if equals.kind() != T![=] {
return None;
}
let expr_token =
algo::skip_trivia_token(equals.next_token()?, syntax::Direction::Next)
.and_then(|it| {
if it.kind().is_literal() {
Some(make::expr_literal(it.text()))
} else {
None
}
})?;
make::meta_expr(attr_name, ast::Expr::Literal(expr_token))
}
};
if attr.excl_token().is_some() {
inner_attrs.push(make::attr_inner(meta));
} else {
inner_attrs.push(make::attr_outer(meta));
}
}
if inner_attrs.is_empty() {
return None;
}
let handle_source_change = |f: &mut SourceChangeBuilder| {
let inner_attrs = inner_attrs.iter().map(|it| it.to_string()).join("\n");
f.replace(range, inner_attrs);
};
acc.add(
AssistId("wrap_unwrap_cfg_attr", AssistKind::Refactor),
"Extract Inner Attributes from `cfg_attr`",
range,
handle_source_change,
);
Some(())
}
#[cfg(test)]
mod tests {
use crate::tests::check_assist;
use super::*;
#[test]
fn test_basic_to_from_cfg_attr() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive$0(Debug)]
pub struct Test {
test: u32,
}
"#,
r#"
#[cfg_attr($0, derive(Debug))]
pub struct Test {
test: u32,
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[cfg_attr(debug_assertions, $0 derive(Debug))]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive(Debug)]
pub struct Test {
test: u32,
}
"#,
);
}
#[test]
fn to_from_path_attr() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
pub struct Test {
#[foo$0]
test: u32,
}
"#,
r#"
pub struct Test {
#[cfg_attr($0, foo)]
test: u32,
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
pub struct Test {
#[cfg_attr(debug_assertions$0, foo)]
test: u32,
}
"#,
r#"
pub struct Test {
#[foo]
test: u32,
}
"#,
);
}
#[test]
fn to_from_eq_attr() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
pub struct Test {
#[foo = "bar"$0]
test: u32,
}
"#,
r#"
pub struct Test {
#[cfg_attr($0, foo = "bar")]
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 inner_attrs() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
#![no_std$0]
"#,
r#"
#![cfg_attr($0, no_std)]
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
#![cfg_attr(not(feature = "std")$0, no_std)]
"#,
r#"
#![no_std]
"#,
);
}
#[test]
fn test_derive_wrap() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive(Debug$0, Clone, Copy)]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive( Clone, Copy)]
#[cfg_attr($0, derive(Debug))]
pub struct Test {
test: u32,
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive(Clone, Debug$0, Copy)]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive(Clone, Copy)]
#[cfg_attr($0, derive(Debug))]
pub struct Test {
test: u32,
}
"#,
);
}
#[test]
fn test_derive_wrap_with_path() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive(std::fmt::Debug$0, Clone, Copy)]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive( Clone, Copy)]
#[cfg_attr($0, derive(std::fmt::Debug))]
pub struct Test {
test: u32,
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive(Clone, std::fmt::Debug$0, Copy)]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive(Clone, Copy)]
#[cfg_attr($0, derive(std::fmt::Debug))]
pub struct Test {
test: u32,
}
"#,
);
}
#[test]
fn test_derive_wrap_at_end() {
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive(std::fmt::Debug, Clone, Cop$0y)]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive(std::fmt::Debug, Clone)]
#[cfg_attr($0, derive(Copy))]
pub struct Test {
test: u32,
}
"#,
);
check_assist(
wrap_unwrap_cfg_attr,
r#"
#[derive(Clone, Copy, std::fmt::D$0ebug)]
pub struct Test {
test: u32,
}
"#,
r#"
#[derive(Clone, Copy)]
#[cfg_attr($0, derive(std::fmt::Debug))]
pub struct Test {
test: u32,
}
"#,
);
}
}

View File

@ -217,6 +217,7 @@ mod handlers {
mod unwrap_result_return_type;
mod unwrap_tuple;
mod wrap_return_type_in_result;
mod wrap_unwrap_cfg_attr;
pub(crate) fn all() -> &'static [Handler] {
&[
@ -342,6 +343,8 @@ pub(crate) fn all() -> &'static [Handler] {
unwrap_tuple::unwrap_tuple,
unqualify_method_call::unqualify_method_call,
wrap_return_type_in_result::wrap_return_type_in_result,
wrap_unwrap_cfg_attr::wrap_unwrap_cfg_attr,
// These are manually sorted for better priorities. By default,
// priority is determined by the size of the target range (smaller
// target wins). If the ranges are equal, position in this list is

View File

@ -3151,3 +3151,22 @@ fn foo() -> Result<i32, ${0:_}> { Ok(42i32) }
"#####,
)
}
#[test]
fn doctest_wrap_unwrap_cfg_attr() {
check_doc_test(
"wrap_unwrap_cfg_attr",
r#####"
#[derive$0(Debug)]
struct S {
field: i32
}
"#####,
r#####"
#[cfg_attr($0, derive(Debug))]
struct S {
field: i32
}
"#####,
)
}