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 @@ macro_rules! eprintln {
mod inlay_hints;
mod join_lines;
mod matching_brace;
mod move_item;
mod parent_module;
mod references;
mod fn_references;
@ -76,6 +77,7 @@ macro_rules! eprintln {
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 @@ pub fn resolve_annotation(&self, annotation: Annotation) -> Cancelable<Annotatio
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

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

View file

@ -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,
}

View file

@ -507,6 +507,7 @@ fn on_request(&mut self, request_received: Instant, req: Request) -> Result<()>
.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)

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(
snap: &GlobalStateSnapshot,
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
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"
}
```

View file

@ -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": [

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 {
async function handleKeypress() {
const editor = ctx.activeRustEditor;

View file

@ -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>("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('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);