diff options
| -rw-r--r-- | crates/ide/src/lib.rs | 10 | ||||
| -rw-r--r-- | crates/ide/src/move_item.rs | 620 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/handlers.rs | 19 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lsp_ext.rs | 22 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/main_loop.rs | 1 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/to_proto.rs | 12 | ||||
| -rw-r--r-- | docs/dev/lsp-extensions.md | 28 | ||||
| -rw-r--r-- | editors/code/package.json | 10 | ||||
| -rw-r--r-- | editors/code/src/commands.ts | 45 | ||||
| -rw-r--r-- | editors/code/src/lsp_ext.ts | 13 | ||||
| -rw-r--r-- | editors/code/src/main.ts | 2 |
11 files changed, 781 insertions, 1 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 662da5a9660..3f73c063222 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<Option<TextEdit>> { + self.with_db(|db| move_item::move_item(db, range, direction)) + } + /// Performs an operation on that may be Canceled. fn with_db<F, T>(&self, f: F) -> Cancelable<T> where diff --git a/crates/ide/src/move_item.rs b/crates/ide/src/move_item.rs new file mode 100644 index 00000000000..48690b0731b --- /dev/null +++ b/crates/ide/src/move_item.rs @@ -0,0 +1,620 @@ +use std::iter::once; + +use hir::Semantics; +use ide_db::{base_db::FileRange, RootDatabase}; +use itertools::Itertools; +use syntax::{ + algo, ast, match_ast, AstNode, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, TextRange, +}; +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<TextEdit> { + let sema = Semantics::new(db); + let file = sema.parse(range.file_id); + + let item = 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 { + NodeOrToken::Node(node) => node, + NodeOrToken::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::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, + ]; + + 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(it.args(), range, direction), + ast::GenericParamList(it) => swap_sibling_in_list(it.generic_params(), range, direction), + ast::GenericArgList(it) => swap_sibling_in_list(it.generic_args(), range, direction), + ast::VariantList(it) => swap_sibling_in_list(it.variants(), range, direction), + ast::TypeBoundList(it) => swap_sibling_in_list(it.bounds(), range, direction), + _ => Some(replace_nodes(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>>( + list: I, + range: TextRange, + direction: Direction, +) -> Option<TextEdit> { + let (l, r) = list + .tuple_windows() + .filter(|(l, r)| match direction { + Direction::Up => r.syntax().text_range().contains_range(range), + Direction::Down => l.syntax().text_range().contains_range(range), + }) + .next()?; + + Some(replace_nodes(l.syntax(), r.syntax())) +} + +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_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 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_up() { + check( + r#" +fn test(one: i32, two$0$0: u32) {} + +fn main() { + test(123, 456); +} + "#, + expect![[r#" +fn test(two: u32, one: i32) {} + +fn main() { + test(123, 456); +} + "#]], + Direction::Up, + ); + } + + #[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, 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); +} + "#]], + 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, 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, 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, + 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 + 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() { + 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 { + fn inner() {} + } + + mod hello { + fn inner() {} + } +} + "#]], + 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 880fea62209..85e67554c21 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -1427,6 +1427,25 @@ pub(crate) fn handle_open_cargo_toml( Ok(Some(res)) } +pub(crate) fn handle_move_item( + snap: GlobalStateSnapshot, + params: lsp_ext::MoveItemParams, +) -> Result<Option<lsp_types::TextDocumentEdit>> { + 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 efcdcd1d975..0e1fec2095c 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<lsp_types::TextDocumentEdit>; + 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 c63a0eaeaf2..e88f16cc11d 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -504,6 +504,7 @@ impl GlobalState { .on::<lsp_ext::HoverRequest>(handlers::handle_hover) .on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs) .on::<lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml) + .on::<lsp_ext::MoveItem>(handlers::handle_move_item) .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting) .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol) .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol) diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index c1ca7ff9b64..25169005fac 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<lsp_types::TextDocumentEdit> { + 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 694fafcd5c6..8a6f9f06e7d 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@ <!--- -lsp_ext.rs hash: 4dfa8d7035f4aee7 +lsp_ext.rs hash: e8a7502bd2b2c2f5 If you need to change the above hash to make the test pass, please check if you need to adjust this doc as well and ping this issue: @@ -595,3 +595,29 @@ interface TestInfo { runnable: Runnable; } ``` + +## Hover Actions + +**Issue:** https://github.com/rust-analyzer/rust-analyzer/issues/6823 + +This request is sent from client to server to move item under cursor or selection in some direction. + +**Method:** `experimental/moveItemUp` +**Method:** `experimental/moveItemDown` + +**Request:** `MoveItemParams` + +**Response:** `TextDocumentEdit | null` + +```typescript +export interface MoveItemParams { + textDocument: lc.TextDocumentIdentifier, + range: lc.Range, + direction: Direction +} + +export const enum Direction { + Up = "Up", + Down = "Down" +} +``` diff --git a/editors/code/package.json b/editors/code/package.json index a2ed9b2d58d..faec45276ca 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -208,6 +208,16 @@ "command": "rust-analyzer.peekTests", "title": "Peek related tests", "category": "Rust Analyzer" + }, + { + "command": "rust-analyzer.moveItemUp", + "title": "Move item up", + "category": "Rust Analyzer" + }, + { + "command": "rust-analyzer.moveItemDown", + "title": "Move item down", + "category": "Rust Analyzer" } ], "keybindings": [ diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index bed1f01169c..1a0805bd378 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -134,6 +134,51 @@ export function joinLines(ctx: Ctx): Cmd { }; } +export function moveItemUp(ctx: Ctx): Cmd { + return moveItem(ctx, ra.Direction.Up); +} + +export function moveItemDown(ctx: Ctx): Cmd { + return moveItem(ctx, ra.Direction.Down); +} + +export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd { + return async () => { + const editor = ctx.activeRustEditor; + const client = ctx.client; + if (!editor || !client) return; + + const edit = await client.sendRequest(ra.moveItem, { + range: client.code2ProtocolConverter.asRange(editor.selection), + textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document), + direction + }); + + if (!edit) return; + + let cursor: vscode.Position | null = null; + + await editor.edit((builder) => { + client.protocol2CodeConverter.asTextEdits(edit.edits).forEach((edit: any) => { + builder.replace(edit.range, edit.newText); + + if (direction === ra.Direction.Up) { + if (!cursor || edit.range.end.isBeforeOrEqual(cursor)) { + cursor = edit.range.end; + } + } else { + if (!cursor || edit.range.end.isAfterOrEqual(cursor)) { + cursor = edit.range.end; + } + } + }); + }).then(() => { + const newPosition = cursor ?? editor.selection.start; + editor.selection = new vscode.Selection(newPosition, newPosition); + }); + }; +} + export function onEnter(ctx: Ctx): Cmd { async function handleKeypress() { const editor = ctx.activeRustEditor; diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index 52de29e04fd..00e128b8c3d 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts @@ -127,3 +127,16 @@ export const openCargoToml = new lc.RequestType<OpenCargoTomlParams, lc.Location export interface OpenCargoTomlParams { textDocument: lc.TextDocumentIdentifier; } + +export const moveItem = new lc.RequestType<MoveItemParams, lc.TextDocumentEdit | void, void>("experimental/moveItem"); + +export interface MoveItemParams { + textDocument: lc.TextDocumentIdentifier; + range: lc.Range; + direction: Direction; +} + +export const enum Direction { + Up = "Up", + Down = "Down" +} diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 925103f5625..643fb643f3a 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -114,6 +114,8 @@ async function tryActivate(context: vscode.ExtensionContext) { ctx.registerCommand('openDocs', commands.openDocs); ctx.registerCommand('openCargoToml', commands.openCargoToml); ctx.registerCommand('peekTests', commands.peekTests); + ctx.registerCommand('moveItemUp', commands.moveItemUp); + ctx.registerCommand('moveItemDown', commands.moveItemDown); defaultOnEnter.dispose(); ctx.registerCommand('onEnter', commands.onEnter); |
