8054: Item movers r=matklad a=ivan770

Closes #6823

https://user-images.githubusercontent.com/14003886/111331579-b4f43480-8679-11eb-9af0-e4dabacc4923.mp4

Implementation issues:
- [ ] Most of items are non-movable, since _movability_ of any item has to be determined manually. Common ones are movable though
- [x] Cursor should move with the item

Co-authored-by: ivan770 <leshenko.ivan770@gmail.com>
This commit is contained in:
bors[bot] 2021-03-22 13:08:45 +00:00 committed by GitHub
commit d4fa6721af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 781 additions and 1 deletions

View file

@ -37,6 +37,7 @@ mod hover;
mod inlay_hints; mod inlay_hints;
mod join_lines; mod join_lines;
mod matching_brace; mod matching_brace;
mod move_item;
mod parent_module; mod parent_module;
mod references; mod references;
mod fn_references; mod fn_references;
@ -76,6 +77,7 @@ pub use crate::{
hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult}, hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult},
inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
markup::Markup, markup::Markup,
move_item::Direction,
prime_caches::PrimeCachesProgress, prime_caches::PrimeCachesProgress,
references::{rename::RenameError, ReferenceSearchResult}, references::{rename::RenameError, ReferenceSearchResult},
runnables::{Runnable, RunnableKind, TestId}, runnables::{Runnable, RunnableKind, TestId},
@ -583,6 +585,14 @@ impl Analysis {
self.with_db(|db| annotations::resolve_annotation(db, annotation)) 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. /// Performs an operation on that may be Canceled.
fn with_db<F, T>(&self, f: F) -> Cancelable<T> fn with_db<F, T>(&self, f: F) -> Cancelable<T>
where where

620
crates/ide/src/move_item.rs Normal file
View file

@ -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);
}
}

View file

@ -1427,6 +1427,25 @@ pub(crate) fn handle_open_cargo_toml(
Ok(Some(res)) 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 { fn to_command_link(command: lsp_types::Command, tooltip: String) -> lsp_ext::CommandLink {
lsp_ext::CommandLink { tooltip: Some(tooltip), command } lsp_ext::CommandLink { tooltip: Some(tooltip), command }
} }

View file

@ -402,3 +402,25 @@ pub(crate) enum CodeLensResolveData {
pub fn supports_utf8(caps: &lsp_types::ClientCapabilities) -> bool { pub fn supports_utf8(caps: &lsp_types::ClientCapabilities) -> bool {
caps.offset_encoding.as_deref().unwrap_or_default().iter().any(|it| it == "utf-8") 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,
}

View file

@ -504,6 +504,7 @@ impl GlobalState {
.on::<lsp_ext::HoverRequest>(handlers::handle_hover) .on::<lsp_ext::HoverRequest>(handlers::handle_hover)
.on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs) .on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs)
.on::<lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml) .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::OnTypeFormatting>(handlers::handle_on_type_formatting)
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol) .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)
.on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol) .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)

View file

@ -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( pub(crate) fn snippet_text_document_edit(
snap: &GlobalStateSnapshot, snap: &GlobalStateSnapshot,
is_snippet: bool, is_snippet: bool,

View file

@ -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 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: need to adjust this doc as well and ping this issue:
@ -595,3 +595,29 @@ interface TestInfo {
runnable: Runnable; 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"
}
```

View file

@ -208,6 +208,16 @@
"command": "rust-analyzer.peekTests", "command": "rust-analyzer.peekTests",
"title": "Peek related tests", "title": "Peek related tests",
"category": "Rust Analyzer" "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": [ "keybindings": [

View file

@ -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 { export function onEnter(ctx: Ctx): Cmd {
async function handleKeypress() { async function handleKeypress() {
const editor = ctx.activeRustEditor; const editor = ctx.activeRustEditor;

View file

@ -127,3 +127,16 @@ export const openCargoToml = new lc.RequestType<OpenCargoTomlParams, lc.Location
export interface OpenCargoTomlParams { export interface OpenCargoTomlParams {
textDocument: lc.TextDocumentIdentifier; 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"
}

View file

@ -114,6 +114,8 @@ async function tryActivate(context: vscode.ExtensionContext) {
ctx.registerCommand('openDocs', commands.openDocs); ctx.registerCommand('openDocs', commands.openDocs);
ctx.registerCommand('openCargoToml', commands.openCargoToml); ctx.registerCommand('openCargoToml', commands.openCargoToml);
ctx.registerCommand('peekTests', commands.peekTests); ctx.registerCommand('peekTests', commands.peekTests);
ctx.registerCommand('moveItemUp', commands.moveItemUp);
ctx.registerCommand('moveItemDown', commands.moveItemDown);
defaultOnEnter.dispose(); defaultOnEnter.dispose();
ctx.registerCommand('onEnter', commands.onEnter); ctx.registerCommand('onEnter', commands.onEnter);