Item up and down movers

This commit is contained in:
ivan770 2021-03-16 14:37:00 +02:00
parent d704750ba9
commit 7d60458495
No known key found for this signature in database
GPG key ID: D8C4BD5AE4D9CC4D
11 changed files with 536 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

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

@ -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<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)
}
fn find_ancestors(item: SyntaxElement, direction: Direction) -> Option<TextEdit> {
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<TextEdit> {
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);
}
}

View file

@ -1424,6 +1424,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

@ -507,6 +507,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,34 @@ 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: lc.TextDocumentEdit = await client.sendRequest(ra.moveItem, {
range: client.code2ProtocolConverter.asRange(editor.selection),
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
direction
});
await editor.edit((builder) => {
client.protocol2CodeConverter.asTextEdits(edit.edits).forEach((edit: any) => {
builder.replace(edit.range, edit.newText);
});
});
};
}
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>("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);