diff options
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide/src/move_item.rs')
-rw-r--r-- | src/tools/rust-analyzer/crates/ide/src/move_item.rs | 890 |
1 files changed, 890 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide/src/move_item.rs b/src/tools/rust-analyzer/crates/ide/src/move_item.rs new file mode 100644 index 000000000..02e9fb8b5 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/move_item.rs @@ -0,0 +1,890 @@ +use std::{iter::once, mem}; + +use hir::Semantics; +use ide_db::{base_db::FileRange, helpers::pick_best_token, RootDatabase}; +use itertools::Itertools; +use syntax::{algo, ast, match_ast, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, TextRange}; +use text_edit::{TextEdit, TextEditBuilder}; + +#[derive(Copy, Clone, Debug)] +pub enum Direction { + Up, + Down, +} + +// Feature: Move Item +// +// Move item under cursor or selection up and down. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Move item up** +// | VS Code | **Rust Analyzer: Move item down** +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065576-04298180-91b1-11eb-91ce-4505e99ed598.gif[] +pub(crate) fn move_item( + db: &RootDatabase, + range: FileRange, + direction: Direction, +) -> Option<TextEdit> { + let sema = Semantics::new(db); + let file = sema.parse(range.file_id); + + let item = if range.range.is_empty() { + SyntaxElement::Token(pick_best_token( + file.syntax().token_at_offset(range.range.start()), + |kind| match kind { + SyntaxKind::IDENT | SyntaxKind::LIFETIME_IDENT => 2, + kind if kind.is_trivia() => 0, + _ => 1, + }, + )?) + } else { + file.syntax().covering_element(range.range) + }; + + find_ancestors(item, direction, range.range) +} + +fn find_ancestors(item: SyntaxElement, direction: Direction, range: TextRange) -> Option<TextEdit> { + let root = match item { + SyntaxElement::Node(node) => node, + SyntaxElement::Token(token) => token.parent()?, + }; + + let movable = [ + SyntaxKind::ARG_LIST, + SyntaxKind::GENERIC_PARAM_LIST, + SyntaxKind::GENERIC_ARG_LIST, + SyntaxKind::VARIANT_LIST, + SyntaxKind::TYPE_BOUND_LIST, + SyntaxKind::MATCH_ARM, + SyntaxKind::PARAM, + SyntaxKind::LET_STMT, + SyntaxKind::EXPR_STMT, + SyntaxKind::IF_EXPR, + SyntaxKind::FOR_EXPR, + SyntaxKind::LOOP_EXPR, + SyntaxKind::WHILE_EXPR, + SyntaxKind::RETURN_EXPR, + SyntaxKind::MATCH_EXPR, + SyntaxKind::MACRO_CALL, + SyntaxKind::TYPE_ALIAS, + SyntaxKind::TRAIT, + SyntaxKind::IMPL, + SyntaxKind::MACRO_DEF, + SyntaxKind::STRUCT, + SyntaxKind::UNION, + SyntaxKind::ENUM, + SyntaxKind::FN, + SyntaxKind::MODULE, + SyntaxKind::USE, + SyntaxKind::STATIC, + SyntaxKind::CONST, + SyntaxKind::MACRO_RULES, + SyntaxKind::MACRO_DEF, + ]; + + let ancestor = once(root.clone()) + .chain(root.ancestors()) + .find(|ancestor| movable.contains(&ancestor.kind()))?; + + move_in_direction(&ancestor, direction, range) +} + +fn move_in_direction( + node: &SyntaxNode, + direction: Direction, + range: TextRange, +) -> Option<TextEdit> { + match_ast! { + match node { + ast::ArgList(it) => swap_sibling_in_list(node, it.args(), range, direction), + ast::GenericParamList(it) => swap_sibling_in_list(node, it.generic_params(), range, direction), + ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction), + ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction), + ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction), + _ => Some(replace_nodes(range, node, &match direction { + Direction::Up => node.prev_sibling(), + Direction::Down => node.next_sibling(), + }?)) + } + } +} + +fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>( + node: &SyntaxNode, + list: I, + range: TextRange, + direction: Direction, +) -> Option<TextEdit> { + let list_lookup = list.tuple_windows().find(|(l, r)| match direction { + Direction::Up => r.syntax().text_range().contains_range(range), + Direction::Down => l.syntax().text_range().contains_range(range), + }); + + if let Some((l, r)) = list_lookup { + Some(replace_nodes(range, l.syntax(), r.syntax())) + } else { + // Cursor is beyond any movable list item (for example, on curly brace in enum). + // It's not necessary, that parent of list is movable (arg list's parent is not, for example), + // and we have to continue tree traversal to find suitable node. + find_ancestors(SyntaxElement::Node(node.parent()?), direction, range) + } +} + +fn replace_nodes<'a>( + range: TextRange, + mut first: &'a SyntaxNode, + mut second: &'a SyntaxNode, +) -> TextEdit { + let cursor_offset = if range.is_empty() { + // FIXME: `applySnippetTextEdits` does not support non-empty selection ranges + if first.text_range().contains_range(range) { + Some(range.start() - first.text_range().start()) + } else if second.text_range().contains_range(range) { + mem::swap(&mut first, &mut second); + Some(range.start() - first.text_range().start()) + } else { + None + } + } else { + None + }; + + let first_with_cursor = match cursor_offset { + Some(offset) => { + let mut item_text = first.text().to_string(); + item_text.insert_str(offset.into(), "$0"); + item_text + } + None => first.text().to_string(), + }; + + let mut edit = TextEditBuilder::default(); + + algo::diff(first, second).into_text_edit(&mut edit); + edit.replace(second.text_range(), first_with_cursor); + + edit.finish() +} + +#[cfg(test)] +mod tests { + use crate::fixture; + use expect_test::{expect, Expect}; + + use crate::Direction; + + fn check(ra_fixture: &str, expect: Expect, direction: Direction) { + let (analysis, range) = fixture::range(ra_fixture); + let edit = analysis.move_item(range, direction).unwrap().unwrap_or_default(); + let mut file = analysis.file_text(range.file_id).unwrap().to_string(); + edit.apply(&mut file); + expect.assert_eq(&file); + } + + #[test] + fn test_moves_match_arm_up() { + check( + r#" +fn main() { + match true { + true => { + println!("Hello, world"); + }, + false =>$0$0 { + println!("Test"); + } + }; +} +"#, + expect![[r#" + fn main() { + match true { + false =>$0 { + println!("Test"); + } + true => { + println!("Hello, world"); + }, + }; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_match_arm_down() { + check( + r#" +fn main() { + match true { + true =>$0$0 { + println!("Hello, world"); + }, + false => { + println!("Test"); + } + }; +} +"#, + expect![[r#" + fn main() { + match true { + false => { + println!("Test"); + } + true =>$0 { + println!("Hello, world"); + }, + }; + } + "#]], + Direction::Down, + ); + } + + #[test] + fn test_nowhere_to_move() { + check( + r#" +fn main() { + match true { + true =>$0$0 { + println!("Hello, world"); + }, + false => { + println!("Test"); + } + }; +} +"#, + expect![[r#" + fn main() { + match true { + true => { + println!("Hello, world"); + }, + false => { + println!("Test"); + } + }; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_let_stmt_up() { + check( + r#" +fn main() { + let test = 123; + let test2$0$0 = 456; +} +"#, + expect![[r#" + fn main() { + let test2$0 = 456; + let test = 123; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_expr_up() { + check( + r#" +fn main() { + println!("Hello, world"); + println!("All I want to say is...");$0$0 +} +"#, + expect![[r#" + fn main() { + println!("All I want to say is...");$0 + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + if true { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + if true { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + for i in 0..10 { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + for i in 0..10 { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + loop { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + loop { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + while true { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + while true { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + return 123;$0$0 +} +"#, + expect![[r#" + fn main() { + return 123;$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_nowhere_to_move_stmt() { + check( + r#" +fn main() { + println!("All I want to say is...");$0$0 + println!("Hello, world"); +} +"#, + expect![[r#" + fn main() { + println!("All I want to say is..."); + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_move_item() { + check( + r#" +fn main() {} + +fn foo() {}$0$0 +"#, + expect![[r#" + fn foo() {}$0 + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_move_impl_up() { + check( + r#" +struct Yay; + +trait Wow {} + +impl Wow for Yay $0$0{} +"#, + expect![[r#" + struct Yay; + + impl Wow for Yay $0{} + + trait Wow {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_move_use_up() { + check( + r#" +use std::vec::Vec; +use std::collections::HashMap$0$0; +"#, + expect![[r#" + use std::collections::HashMap$0; + use std::vec::Vec; + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_match_expr_up() { + check( + r#" +fn main() { + let test = 123; + + $0match test { + 456 => {}, + _ => {} + };$0 +} +"#, + expect![[r#" + fn main() { + match test { + 456 => {}, + _ => {} + }; + + let test = 123; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_param() { + check( + r#" +fn test(one: i32, two$0$0: u32) {} + +fn main() { + test(123, 456); +} +"#, + expect![[r#" + fn test(two$0: u32, one: i32) {} + + fn main() { + test(123, 456); + } + "#]], + Direction::Up, + ); + check( + r#" +fn f($0$0arg: u8, arg2: u16) {} +"#, + expect![[r#" + fn f(arg2: u16, $0arg: u8) {} + "#]], + Direction::Down, + ); + } + + #[test] + fn test_moves_arg_up() { + check( + r#" +fn test(one: i32, two: u32) {} + +fn main() { + test(123, 456$0$0); +} +"#, + expect![[r#" + fn test(one: i32, two: u32) {} + + fn main() { + test(456$0, 123); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_arg_down() { + check( + r#" +fn test(one: i32, two: u32) {} + +fn main() { + test(123$0$0, 456); +} +"#, + expect![[r#" + fn test(one: i32, two: u32) {} + + fn main() { + test(456, 123$0); + } + "#]], + Direction::Down, + ); + } + + #[test] + fn test_nowhere_to_move_arg() { + check( + r#" +fn test(one: i32, two: u32) {} + +fn main() { + test(123$0$0, 456); +} +"#, + expect![[r#" + fn test(one: i32, two: u32) {} + + fn main() { + test(123, 456); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_generic_param_up() { + check( + r#" +struct Test<A, B$0$0>(A, B); + +fn main() {} +"#, + expect![[r#" + struct Test<B$0, A>(A, B); + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_generic_arg_up() { + check( + r#" +struct Test<A, B>(A, B); + +fn main() { + let t = Test::<i32, &str$0$0>(123, "yay"); +} +"#, + expect![[r#" + struct Test<A, B>(A, B); + + fn main() { + let t = Test::<&str$0, i32>(123, "yay"); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_variant_up() { + check( + r#" +enum Hello { + One, + Two$0$0 +} + +fn main() {} +"#, + expect![[r#" + enum Hello { + Two$0, + One + } + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_type_bound_up() { + check( + r#" +trait One {} + +trait Two {} + +fn test<T: One + Two$0$0>(t: T) {} + +fn main() {} +"#, + expect![[r#" + trait One {} + + trait Two {} + + fn test<T: Two$0 + One>(t: T) {} + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_prioritizes_trait_items() { + check( + r#" +struct Test; + +trait Yay { + type One; + + type Two; + + fn inner(); +} + +impl Yay for Test { + type One = i32; + + type Two = u32; + + fn inner() {$0$0 + println!("Mmmm"); + } +} +"#, + expect![[r#" + struct Test; + + trait Yay { + type One; + + type Two; + + fn inner(); + } + + impl Yay for Test { + type One = i32; + + fn inner() {$0 + println!("Mmmm"); + } + + type Two = u32; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_weird_nesting() { + check( + r#" +fn test() { + mod hello { + fn inner() {} + } + + mod hi {$0$0 + fn inner() {} + } +} +"#, + expect![[r#" + fn test() { + mod hi {$0 + fn inner() {} + } + + mod hello { + fn inner() {} + } + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_cursor_at_item_start() { + check( + r#" +$0$0#[derive(Debug)] +enum FooBar { + Foo, + Bar, +} + +fn main() {} +"#, + expect![[r##" + fn main() {} + + $0#[derive(Debug)] + enum FooBar { + Foo, + Bar, + } + "##]], + Direction::Down, + ); + check( + r#" +$0$0enum FooBar { + Foo, + Bar, +} + +fn main() {} +"#, + expect![[r#" + fn main() {} + + $0enum FooBar { + Foo, + Bar, + } + "#]], + Direction::Down, + ); + check( + r#" +struct Test; + +trait SomeTrait {} + +$0$0impl SomeTrait for Test {} + +fn main() {} +"#, + expect![[r#" + struct Test; + + $0impl SomeTrait for Test {} + + trait SomeTrait {} + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_cursor_at_item_end() { + check( + r#" +enum FooBar { + Foo, + Bar, +}$0$0 + +fn main() {} +"#, + expect![[r#" + fn main() {} + + enum FooBar { + Foo, + Bar, + }$0 + "#]], + Direction::Down, + ); + check( + r#" +struct Test; + +trait SomeTrait {} + +impl SomeTrait for Test {}$0$0 + +fn main() {} +"#, + expect![[r#" + struct Test; + + impl SomeTrait for Test {}$0 + + trait SomeTrait {} + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn handles_empty_file() { + check(r#"$0$0"#, expect![[r#""#]], Direction::Up); + } +} |