From 7d604584954660d255ad0929d3be8ce03f879d0c Mon Sep 17 00:00:00 2001 From: ivan770 Date: Tue, 16 Mar 2021 14:37:00 +0200 Subject: [PATCH] Item up and down movers --- crates/ide/src/lib.rs | 10 + crates/ide/src/move_item.rs | 392 ++++++++++++++++++++++++++ crates/rust-analyzer/src/handlers.rs | 19 ++ crates/rust-analyzer/src/lsp_ext.rs | 22 ++ crates/rust-analyzer/src/main_loop.rs | 1 + crates/rust-analyzer/src/to_proto.rs | 12 + docs/dev/lsp-extensions.md | 28 +- editors/code/package.json | 10 + editors/code/src/commands.ts | 28 ++ editors/code/src/lsp_ext.ts | 13 + editors/code/src/main.ts | 2 + 11 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 crates/ide/src/move_item.rs diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 662da5a966..3f73c06322 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 0000000000..be62d008dd --- /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); + } +} diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index b6f484e515..8daf278672 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -1424,6 +1424,25 @@ pub(crate) fn handle_open_cargo_toml( Ok(Some(res)) } +pub(crate) fn handle_move_item( + snap: GlobalStateSnapshot, + params: lsp_ext::MoveItemParams, +) -> Result> { + let _p = profile::span("handle_move_item"); + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let range = from_proto::file_range(&snap, params.text_document, params.range)?; + + let direction = match params.direction { + lsp_ext::MoveItemDirection::Up => ide::Direction::Up, + lsp_ext::MoveItemDirection::Down => ide::Direction::Down, + }; + + match snap.analysis.move_item(range, direction)? { + Some(text_edit) => Ok(Some(to_proto::text_document_edit(&snap, file_id, text_edit)?)), + None => Ok(None), + } +} + fn to_command_link(command: lsp_types::Command, tooltip: String) -> lsp_ext::CommandLink { lsp_ext::CommandLink { tooltip: Some(tooltip), command } } diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index efcdcd1d97..0e1fec2095 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -402,3 +402,25 @@ pub(crate) enum CodeLensResolveData { pub fn supports_utf8(caps: &lsp_types::ClientCapabilities) -> bool { caps.offset_encoding.as_deref().unwrap_or_default().iter().any(|it| it == "utf-8") } + +pub enum MoveItem {} + +impl Request for MoveItem { + type Params = MoveItemParams; + type Result = Option; + const METHOD: &'static str = "experimental/moveItem"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MoveItemParams { + pub direction: MoveItemDirection, + pub text_document: TextDocumentIdentifier, + pub range: Range, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum MoveItemDirection { + Up, + Down, +} diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 984790d354..022a208518 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -507,6 +507,7 @@ impl GlobalState { .on::(handlers::handle_hover) .on::(handlers::handle_open_docs) .on::(handlers::handle_open_cargo_toml) + .on::(handlers::handle_move_item) .on::(handlers::handle_on_type_formatting) .on::(handlers::handle_document_symbol) .on::(handlers::handle_workspace_symbol) diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index 70501618e8..3171708d5a 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs @@ -658,6 +658,18 @@ pub(crate) fn goto_definition_response( } } +pub(crate) fn text_document_edit( + snap: &GlobalStateSnapshot, + file_id: FileId, + edit: TextEdit, +) -> Result { + let text_document = optional_versioned_text_document_identifier(snap, file_id); + let line_index = snap.file_line_index(file_id)?; + let edits = + edit.into_iter().map(|it| lsp_types::OneOf::Left(text_edit(&line_index, it))).collect(); + Ok(lsp_types::TextDocumentEdit { text_document, edits }) +} + pub(crate) fn snippet_text_document_edit( snap: &GlobalStateSnapshot, is_snippet: bool, diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 694fafcd5c..8a6f9f06e7 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@