about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/ide/src/lib.rs10
-rw-r--r--crates/ide/src/move_item.rs620
-rw-r--r--crates/rust-analyzer/src/handlers.rs19
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs22
-rw-r--r--crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--crates/rust-analyzer/src/to_proto.rs12
-rw-r--r--docs/dev/lsp-extensions.md28
-rw-r--r--editors/code/package.json10
-rw-r--r--editors/code/src/commands.ts45
-rw-r--r--editors/code/src/lsp_ext.ts13
-rw-r--r--editors/code/src/main.ts2
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, &params.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);