From 7d604584954660d255ad0929d3be8ce03f879d0c Mon Sep 17 00:00:00 2001 From: ivan770 Date: Tue, 16 Mar 2021 14:37:00 +0200 Subject: Item up and down movers --- crates/ide/src/lib.rs | 10 ++ crates/ide/src/move_item.rs | 392 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 crates/ide/src/move_item.rs (limited to 'crates/ide') diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 662da5a96..3f73c0632 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -37,6 +37,7 @@ mod hover; mod inlay_hints; mod join_lines; mod matching_brace; +mod move_item; mod parent_module; mod references; mod fn_references; @@ -76,6 +77,7 @@ pub use crate::{ hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult}, inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, markup::Markup, + move_item::Direction, prime_caches::PrimeCachesProgress, references::{rename::RenameError, ReferenceSearchResult}, runnables::{Runnable, RunnableKind, TestId}, @@ -583,6 +585,14 @@ impl Analysis { self.with_db(|db| annotations::resolve_annotation(db, annotation)) } + pub fn move_item( + &self, + range: FileRange, + direction: Direction, + ) -> Cancelable> { + self.with_db(|db| move_item::move_item(db, range, direction)) + } + /// Performs an operation on that may be Canceled. fn with_db(&self, f: F) -> Cancelable where diff --git a/crates/ide/src/move_item.rs b/crates/ide/src/move_item.rs new file mode 100644 index 000000000..be62d008d --- /dev/null +++ b/crates/ide/src/move_item.rs @@ -0,0 +1,392 @@ +use std::iter::once; + +use hir::Semantics; +use ide_db::{base_db::FileRange, RootDatabase}; +use syntax::{algo, AstNode, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode}; +use text_edit::{TextEdit, TextEditBuilder}; + +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** +// |=== +pub(crate) fn move_item( + db: &RootDatabase, + range: FileRange, + direction: Direction, +) -> Option { + let sema = Semantics::new(db); + let file = sema.parse(range.file_id); + + let item = file.syntax().covering_element(range.range); + find_ancestors(item, direction) +} + +fn find_ancestors(item: SyntaxElement, direction: Direction) -> Option { + let movable = [ + SyntaxKind::MATCH_ARM, + // https://github.com/intellij-rust/intellij-rust/blob/master/src/main/kotlin/org/rust/ide/actions/mover/RsStatementUpDownMover.kt + SyntaxKind::LET_STMT, + SyntaxKind::EXPR_STMT, + SyntaxKind::MATCH_EXPR, + // https://github.com/intellij-rust/intellij-rust/blob/master/src/main/kotlin/org/rust/ide/actions/mover/RsItemUpDownMover.kt + SyntaxKind::TRAIT, + SyntaxKind::IMPL, + SyntaxKind::MACRO_CALL, + SyntaxKind::MACRO_DEF, + SyntaxKind::STRUCT, + SyntaxKind::ENUM, + SyntaxKind::MODULE, + SyntaxKind::USE, + SyntaxKind::FN, + SyntaxKind::CONST, + SyntaxKind::TYPE_ALIAS, + ]; + + let root = match item { + NodeOrToken::Node(node) => node, + NodeOrToken::Token(token) => token.parent(), + }; + + let ancestor = once(root.clone()) + .chain(root.ancestors()) + .filter(|ancestor| movable.contains(&ancestor.kind())) + .max_by_key(|ancestor| kind_priority(ancestor.kind()))?; + + move_in_direction(&ancestor, direction) +} + +fn kind_priority(kind: SyntaxKind) -> i32 { + match kind { + SyntaxKind::MATCH_ARM => 4, + + SyntaxKind::LET_STMT | SyntaxKind::EXPR_STMT | SyntaxKind::MATCH_EXPR => 3, + + SyntaxKind::TRAIT + | SyntaxKind::IMPL + | SyntaxKind::MACRO_CALL + | SyntaxKind::MACRO_DEF + | SyntaxKind::STRUCT + | SyntaxKind::ENUM + | SyntaxKind::MODULE + | SyntaxKind::USE + | SyntaxKind::FN + | SyntaxKind::CONST + | SyntaxKind::TYPE_ALIAS => 2, + + // Placeholder for items, that are non-movable, and filtered even before kind_priority call + _ => 1, + } +} + +fn move_in_direction(node: &SyntaxNode, direction: Direction) -> Option { + let sibling = match direction { + Direction::Up => node.prev_sibling(), + Direction::Down => node.next_sibling(), + }?; + + Some(replace_nodes(&sibling, node)) +} + +fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit { + let mut edit = TextEditBuilder::default(); + + algo::diff(first, second).into_text_edit(&mut edit); + algo::diff(second, first).into_text_edit(&mut edit); + + 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 => { + 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 => { + 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 = 456; + let test = 123; +} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_prioritizes_match_arm() { + check( + r#" +fn main() { + match true { + true => { + let test = 123;$0$0 + let test2 = 456; + }, + false => { + println!("Test"); + } + }; +} + "#, + expect![[r#" +fn main() { + match true { + false => { + println!("Test"); + }, + true => { + let test = 123; + let test2 = 456; + } + }; +} + "#]], + Direction::Down, + ); + } + + #[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..."); + 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() {} + +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 {} + +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; +use std::vec::Vec; + "#]], + Direction::Up, + ); + } + + #[test] + fn 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 handles_empty_file() { + check(r#"$0$0"#, expect![[r#""#]], Direction::Up); + } +} -- cgit v1.2.3