aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-03-22 13:08:45 +0000
committerGitHub <[email protected]>2021-03-22 13:08:45 +0000
commitd4fa6721afacec78a750df1bb1f0e7e950eaf73c (patch)
treeefabf84f708868484e0dac7893f77ddfba6d9c21
parent3af1885bd2c4d3470d203a216488946ee8572970 (diff)
parentd331155f8db056a0f7a406498c96f759f620d2c7 (diff)
Merge #8054
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 <[email protected]>
-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 662da5a96..3f73c0632 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -37,6 +37,7 @@ mod hover;
37mod inlay_hints; 37mod inlay_hints;
38mod join_lines; 38mod join_lines;
39mod matching_brace; 39mod matching_brace;
40mod move_item;
40mod parent_module; 41mod parent_module;
41mod references; 42mod references;
42mod fn_references; 43mod fn_references;
@@ -76,6 +77,7 @@ pub use crate::{
76 hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult}, 77 hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult},
77 inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, 78 inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
78 markup::Markup, 79 markup::Markup,
80 move_item::Direction,
79 prime_caches::PrimeCachesProgress, 81 prime_caches::PrimeCachesProgress,
80 references::{rename::RenameError, ReferenceSearchResult}, 82 references::{rename::RenameError, ReferenceSearchResult},
81 runnables::{Runnable, RunnableKind, TestId}, 83 runnables::{Runnable, RunnableKind, TestId},
@@ -583,6 +585,14 @@ impl Analysis {
583 self.with_db(|db| annotations::resolve_annotation(db, annotation)) 585 self.with_db(|db| annotations::resolve_annotation(db, annotation))
584 } 586 }
585 587
588 pub fn move_item(
589 &self,
590 range: FileRange,
591 direction: Direction,
592 ) -> Cancelable<Option<TextEdit>> {
593 self.with_db(|db| move_item::move_item(db, range, direction))
594 }
595
586 /// Performs an operation on that may be Canceled. 596 /// Performs an operation on that may be Canceled.
587 fn with_db<F, T>(&self, f: F) -> Cancelable<T> 597 fn with_db<F, T>(&self, f: F) -> Cancelable<T>
588 where 598 where
diff --git a/crates/ide/src/move_item.rs b/crates/ide/src/move_item.rs
new file mode 100644
index 000000000..48690b073
--- /dev/null
+++ b/crates/ide/src/move_item.rs
@@ -0,0 +1,620 @@
1use std::iter::once;
2
3use hir::Semantics;
4use ide_db::{base_db::FileRange, RootDatabase};
5use itertools::Itertools;
6use syntax::{
7 algo, ast, match_ast, AstNode, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, TextRange,
8};
9use text_edit::{TextEdit, TextEditBuilder};
10
11pub enum Direction {
12 Up,
13 Down,
14}
15
16// Feature: Move Item
17//
18// Move item under cursor or selection up and down.
19//
20// |===
21// | Editor | Action Name
22//
23// | VS Code | **Rust Analyzer: Move item up**
24// | VS Code | **Rust Analyzer: Move item down**
25// |===
26pub(crate) fn move_item(
27 db: &RootDatabase,
28 range: FileRange,
29 direction: Direction,
30) -> Option<TextEdit> {
31 let sema = Semantics::new(db);
32 let file = sema.parse(range.file_id);
33
34 let item = file.syntax().covering_element(range.range);
35 find_ancestors(item, direction, range.range)
36}
37
38fn find_ancestors(item: SyntaxElement, direction: Direction, range: TextRange) -> Option<TextEdit> {
39 let root = match item {
40 NodeOrToken::Node(node) => node,
41 NodeOrToken::Token(token) => token.parent()?,
42 };
43
44 let movable = [
45 SyntaxKind::ARG_LIST,
46 SyntaxKind::GENERIC_PARAM_LIST,
47 SyntaxKind::GENERIC_ARG_LIST,
48 SyntaxKind::VARIANT_LIST,
49 SyntaxKind::TYPE_BOUND_LIST,
50 SyntaxKind::MATCH_ARM,
51 SyntaxKind::PARAM,
52 SyntaxKind::LET_STMT,
53 SyntaxKind::EXPR_STMT,
54 SyntaxKind::MATCH_EXPR,
55 SyntaxKind::MACRO_CALL,
56 SyntaxKind::TYPE_ALIAS,
57 SyntaxKind::TRAIT,
58 SyntaxKind::IMPL,
59 SyntaxKind::MACRO_DEF,
60 SyntaxKind::STRUCT,
61 SyntaxKind::UNION,
62 SyntaxKind::ENUM,
63 SyntaxKind::FN,
64 SyntaxKind::MODULE,
65 SyntaxKind::USE,
66 SyntaxKind::STATIC,
67 SyntaxKind::CONST,
68 SyntaxKind::MACRO_RULES,
69 ];
70
71 let ancestor = once(root.clone())
72 .chain(root.ancestors())
73 .find(|ancestor| movable.contains(&ancestor.kind()))?;
74
75 move_in_direction(&ancestor, direction, range)
76}
77
78fn move_in_direction(
79 node: &SyntaxNode,
80 direction: Direction,
81 range: TextRange,
82) -> Option<TextEdit> {
83 match_ast! {
84 match node {
85 ast::ArgList(it) => swap_sibling_in_list(it.args(), range, direction),
86 ast::GenericParamList(it) => swap_sibling_in_list(it.generic_params(), range, direction),
87 ast::GenericArgList(it) => swap_sibling_in_list(it.generic_args(), range, direction),
88 ast::VariantList(it) => swap_sibling_in_list(it.variants(), range, direction),
89 ast::TypeBoundList(it) => swap_sibling_in_list(it.bounds(), range, direction),
90 _ => Some(replace_nodes(node, &match direction {
91 Direction::Up => node.prev_sibling(),
92 Direction::Down => node.next_sibling(),
93 }?))
94 }
95 }
96}
97
98fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
99 list: I,
100 range: TextRange,
101 direction: Direction,
102) -> Option<TextEdit> {
103 let (l, r) = list
104 .tuple_windows()
105 .filter(|(l, r)| match direction {
106 Direction::Up => r.syntax().text_range().contains_range(range),
107 Direction::Down => l.syntax().text_range().contains_range(range),
108 })
109 .next()?;
110
111 Some(replace_nodes(l.syntax(), r.syntax()))
112}
113
114fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit {
115 let mut edit = TextEditBuilder::default();
116
117 algo::diff(first, second).into_text_edit(&mut edit);
118 algo::diff(second, first).into_text_edit(&mut edit);
119
120 edit.finish()
121}
122
123#[cfg(test)]
124mod tests {
125 use crate::fixture;
126 use expect_test::{expect, Expect};
127
128 use crate::Direction;
129
130 fn check(ra_fixture: &str, expect: Expect, direction: Direction) {
131 let (analysis, range) = fixture::range(ra_fixture);
132 let edit = analysis.move_item(range, direction).unwrap().unwrap_or_default();
133 let mut file = analysis.file_text(range.file_id).unwrap().to_string();
134 edit.apply(&mut file);
135 expect.assert_eq(&file);
136 }
137
138 #[test]
139 fn test_moves_match_arm_up() {
140 check(
141 r#"
142fn main() {
143 match true {
144 true => {
145 println!("Hello, world");
146 },
147 false =>$0$0 {
148 println!("Test");
149 }
150 };
151}
152 "#,
153 expect![[r#"
154fn main() {
155 match true {
156 false => {
157 println!("Test");
158 },
159 true => {
160 println!("Hello, world");
161 }
162 };
163}
164 "#]],
165 Direction::Up,
166 );
167 }
168
169 #[test]
170 fn test_moves_match_arm_down() {
171 check(
172 r#"
173fn main() {
174 match true {
175 true =>$0$0 {
176 println!("Hello, world");
177 },
178 false => {
179 println!("Test");
180 }
181 };
182}
183 "#,
184 expect![[r#"
185fn main() {
186 match true {
187 false => {
188 println!("Test");
189 },
190 true => {
191 println!("Hello, world");
192 }
193 };
194}
195 "#]],
196 Direction::Down,
197 );
198 }
199
200 #[test]
201 fn test_nowhere_to_move() {
202 check(
203 r#"
204fn main() {
205 match true {
206 true =>$0$0 {
207 println!("Hello, world");
208 },
209 false => {
210 println!("Test");
211 }
212 };
213}
214 "#,
215 expect![[r#"
216fn main() {
217 match true {
218 true => {
219 println!("Hello, world");
220 },
221 false => {
222 println!("Test");
223 }
224 };
225}
226 "#]],
227 Direction::Up,
228 );
229 }
230
231 #[test]
232 fn test_moves_let_stmt_up() {
233 check(
234 r#"
235fn main() {
236 let test = 123;
237 let test2$0$0 = 456;
238}
239 "#,
240 expect![[r#"
241fn main() {
242 let test2 = 456;
243 let test = 123;
244}
245 "#]],
246 Direction::Up,
247 );
248 }
249
250 #[test]
251 fn test_moves_expr_up() {
252 check(
253 r#"
254fn main() {
255 println!("Hello, world");
256 println!("All I want to say is...");$0$0
257}
258 "#,
259 expect![[r#"
260fn main() {
261 println!("All I want to say is...");
262 println!("Hello, world");
263}
264 "#]],
265 Direction::Up,
266 );
267 }
268
269 #[test]
270 fn test_nowhere_to_move_stmt() {
271 check(
272 r#"
273fn main() {
274 println!("All I want to say is...");$0$0
275 println!("Hello, world");
276}
277 "#,
278 expect![[r#"
279fn main() {
280 println!("All I want to say is...");
281 println!("Hello, world");
282}
283 "#]],
284 Direction::Up,
285 );
286 }
287
288 #[test]
289 fn test_move_item() {
290 check(
291 r#"
292fn main() {}
293
294fn foo() {}$0$0
295 "#,
296 expect![[r#"
297fn foo() {}
298
299fn main() {}
300 "#]],
301 Direction::Up,
302 );
303 }
304
305 #[test]
306 fn test_move_impl_up() {
307 check(
308 r#"
309struct Yay;
310
311trait Wow {}
312
313impl Wow for Yay $0$0{}
314 "#,
315 expect![[r#"
316struct Yay;
317
318impl Wow for Yay {}
319
320trait Wow {}
321 "#]],
322 Direction::Up,
323 );
324 }
325
326 #[test]
327 fn test_move_use_up() {
328 check(
329 r#"
330use std::vec::Vec;
331use std::collections::HashMap$0$0;
332 "#,
333 expect![[r#"
334use std::collections::HashMap;
335use std::vec::Vec;
336 "#]],
337 Direction::Up,
338 );
339 }
340
341 #[test]
342 fn test_moves_match_expr_up() {
343 check(
344 r#"
345fn main() {
346 let test = 123;
347
348 $0match test {
349 456 => {},
350 _ => {}
351 };$0
352}
353 "#,
354 expect![[r#"
355fn main() {
356 match test {
357 456 => {},
358 _ => {}
359 };
360
361 let test = 123;
362}
363 "#]],
364 Direction::Up,
365 );
366 }
367
368 #[test]
369 fn test_moves_param_up() {
370 check(
371 r#"
372fn test(one: i32, two$0$0: u32) {}
373
374fn main() {
375 test(123, 456);
376}
377 "#,
378 expect![[r#"
379fn test(two: u32, one: i32) {}
380
381fn main() {
382 test(123, 456);
383}
384 "#]],
385 Direction::Up,
386 );
387 }
388
389 #[test]
390 fn test_moves_arg_up() {
391 check(
392 r#"
393fn test(one: i32, two: u32) {}
394
395fn main() {
396 test(123, 456$0$0);
397}
398 "#,
399 expect![[r#"
400fn test(one: i32, two: u32) {}
401
402fn main() {
403 test(456, 123);
404}
405 "#]],
406 Direction::Up,
407 );
408 }
409
410 #[test]
411 fn test_moves_arg_down() {
412 check(
413 r#"
414fn test(one: i32, two: u32) {}
415
416fn main() {
417 test(123$0$0, 456);
418}
419 "#,
420 expect![[r#"
421fn test(one: i32, two: u32) {}
422
423fn main() {
424 test(456, 123);
425}
426 "#]],
427 Direction::Down,
428 );
429 }
430
431 #[test]
432 fn test_nowhere_to_move_arg() {
433 check(
434 r#"
435fn test(one: i32, two: u32) {}
436
437fn main() {
438 test(123$0$0, 456);
439}
440 "#,
441 expect![[r#"
442fn test(one: i32, two: u32) {}
443
444fn main() {
445 test(123, 456);
446}
447 "#]],
448 Direction::Up,
449 );
450 }
451
452 #[test]
453 fn test_moves_generic_param_up() {
454 check(
455 r#"
456struct Test<A, B$0$0>(A, B);
457
458fn main() {}
459 "#,
460 expect![[r#"
461struct Test<B, A>(A, B);
462
463fn main() {}
464 "#]],
465 Direction::Up,
466 );
467 }
468
469 #[test]
470 fn test_moves_generic_arg_up() {
471 check(
472 r#"
473struct Test<A, B>(A, B);
474
475fn main() {
476 let t = Test::<i32, &str$0$0>(123, "yay");
477}
478 "#,
479 expect![[r#"
480struct Test<A, B>(A, B);
481
482fn main() {
483 let t = Test::<&str, i32>(123, "yay");
484}
485 "#]],
486 Direction::Up,
487 );
488 }
489
490 #[test]
491 fn test_moves_variant_up() {
492 check(
493 r#"
494enum Hello {
495 One,
496 Two$0$0
497}
498
499fn main() {}
500 "#,
501 expect![[r#"
502enum Hello {
503 Two,
504 One
505}
506
507fn main() {}
508 "#]],
509 Direction::Up,
510 );
511 }
512
513 #[test]
514 fn test_moves_type_bound_up() {
515 check(
516 r#"
517trait One {}
518
519trait Two {}
520
521fn test<T: One + Two$0$0>(t: T) {}
522
523fn main() {}
524 "#,
525 expect![[r#"
526trait One {}
527
528trait Two {}
529
530fn test<T: Two + One>(t: T) {}
531
532fn main() {}
533 "#]],
534 Direction::Up,
535 );
536 }
537
538 #[test]
539 fn test_prioritizes_trait_items() {
540 check(
541 r#"
542struct Test;
543
544trait Yay {
545 type One;
546
547 type Two;
548
549 fn inner();
550}
551
552impl Yay for Test {
553 type One = i32;
554
555 type Two = u32;
556
557 fn inner() {$0$0
558 println!("Mmmm");
559 }
560}
561 "#,
562 expect![[r#"
563struct Test;
564
565trait Yay {
566 type One;
567
568 type Two;
569
570 fn inner();
571}
572
573impl Yay for Test {
574 type One = i32;
575
576 fn inner() {
577 println!("Mmmm");
578 }
579
580 type Two = u32;
581}
582 "#]],
583 Direction::Up,
584 );
585 }
586
587 #[test]
588 fn test_weird_nesting() {
589 check(
590 r#"
591fn test() {
592 mod hello {
593 fn inner() {}
594 }
595
596 mod hi {$0$0
597 fn inner() {}
598 }
599}
600 "#,
601 expect![[r#"
602fn test() {
603 mod hi {
604 fn inner() {}
605 }
606
607 mod hello {
608 fn inner() {}
609 }
610}
611 "#]],
612 Direction::Up,
613 );
614 }
615
616 #[test]
617 fn handles_empty_file() {
618 check(r#"$0$0"#, expect![[r#""#]], Direction::Up);
619 }
620}
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 880fea622..85e67554c 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(
1427 Ok(Some(res)) 1427 Ok(Some(res))
1428} 1428}
1429 1429
1430pub(crate) fn handle_move_item(
1431 snap: GlobalStateSnapshot,
1432 params: lsp_ext::MoveItemParams,
1433) -> Result<Option<lsp_types::TextDocumentEdit>> {
1434 let _p = profile::span("handle_move_item");
1435 let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
1436 let range = from_proto::file_range(&snap, params.text_document, params.range)?;
1437
1438 let direction = match params.direction {
1439 lsp_ext::MoveItemDirection::Up => ide::Direction::Up,
1440 lsp_ext::MoveItemDirection::Down => ide::Direction::Down,
1441 };
1442
1443 match snap.analysis.move_item(range, direction)? {
1444 Some(text_edit) => Ok(Some(to_proto::text_document_edit(&snap, file_id, text_edit)?)),
1445 None => Ok(None),
1446 }
1447}
1448
1430fn to_command_link(command: lsp_types::Command, tooltip: String) -> lsp_ext::CommandLink { 1449fn to_command_link(command: lsp_types::Command, tooltip: String) -> lsp_ext::CommandLink {
1431 lsp_ext::CommandLink { tooltip: Some(tooltip), command } 1450 lsp_ext::CommandLink { tooltip: Some(tooltip), command }
1432} 1451}
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index efcdcd1d9..0e1fec209 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 {
402pub fn supports_utf8(caps: &lsp_types::ClientCapabilities) -> bool { 402pub fn supports_utf8(caps: &lsp_types::ClientCapabilities) -> bool {
403 caps.offset_encoding.as_deref().unwrap_or_default().iter().any(|it| it == "utf-8") 403 caps.offset_encoding.as_deref().unwrap_or_default().iter().any(|it| it == "utf-8")
404} 404}
405
406pub enum MoveItem {}
407
408impl Request for MoveItem {
409 type Params = MoveItemParams;
410 type Result = Option<lsp_types::TextDocumentEdit>;
411 const METHOD: &'static str = "experimental/moveItem";
412}
413
414#[derive(Serialize, Deserialize, Debug)]
415#[serde(rename_all = "camelCase")]
416pub struct MoveItemParams {
417 pub direction: MoveItemDirection,
418 pub text_document: TextDocumentIdentifier,
419 pub range: Range,
420}
421
422#[derive(Serialize, Deserialize, Debug)]
423pub enum MoveItemDirection {
424 Up,
425 Down,
426}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index c63a0eaea..e88f16cc1 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -504,6 +504,7 @@ impl GlobalState {
504 .on::<lsp_ext::HoverRequest>(handlers::handle_hover) 504 .on::<lsp_ext::HoverRequest>(handlers::handle_hover)
505 .on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs) 505 .on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs)
506 .on::<lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml) 506 .on::<lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml)
507 .on::<lsp_ext::MoveItem>(handlers::handle_move_item)
507 .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting) 508 .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)
508 .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol) 509 .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)
509 .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol) 510 .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 c1ca7ff9b..25169005f 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(
658 } 658 }
659} 659}
660 660
661pub(crate) fn text_document_edit(
662 snap: &GlobalStateSnapshot,
663 file_id: FileId,
664 edit: TextEdit,
665) -> Result<lsp_types::TextDocumentEdit> {
666 let text_document = optional_versioned_text_document_identifier(snap, file_id);
667 let line_index = snap.file_line_index(file_id)?;
668 let edits =
669 edit.into_iter().map(|it| lsp_types::OneOf::Left(text_edit(&line_index, it))).collect();
670 Ok(lsp_types::TextDocumentEdit { text_document, edits })
671}
672
661pub(crate) fn snippet_text_document_edit( 673pub(crate) fn snippet_text_document_edit(
662 snap: &GlobalStateSnapshot, 674 snap: &GlobalStateSnapshot,
663 is_snippet: bool, 675 is_snippet: bool,
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 694fafcd5..8a6f9f06e 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
1<!--- 1<!---
2lsp_ext.rs hash: 4dfa8d7035f4aee7 2lsp_ext.rs hash: e8a7502bd2b2c2f5
3 3
4If you need to change the above hash to make the test pass, please check if you 4If you need to change the above hash to make the test pass, please check if you
5need to adjust this doc as well and ping this issue: 5need to adjust this doc as well and ping this issue:
@@ -595,3 +595,29 @@ interface TestInfo {
595 runnable: Runnable; 595 runnable: Runnable;
596} 596}
597``` 597```
598
599## Hover Actions
600
601**Issue:** https://github.com/rust-analyzer/rust-analyzer/issues/6823
602
603This request is sent from client to server to move item under cursor or selection in some direction.
604
605**Method:** `experimental/moveItemUp`
606**Method:** `experimental/moveItemDown`
607
608**Request:** `MoveItemParams`
609
610**Response:** `TextDocumentEdit | null`
611
612```typescript
613export interface MoveItemParams {
614 textDocument: lc.TextDocumentIdentifier,
615 range: lc.Range,
616 direction: Direction
617}
618
619export const enum Direction {
620 Up = "Up",
621 Down = "Down"
622}
623```
diff --git a/editors/code/package.json b/editors/code/package.json
index a2ed9b2d5..faec45276 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -208,6 +208,16 @@
208 "command": "rust-analyzer.peekTests", 208 "command": "rust-analyzer.peekTests",
209 "title": "Peek related tests", 209 "title": "Peek related tests",
210 "category": "Rust Analyzer" 210 "category": "Rust Analyzer"
211 },
212 {
213 "command": "rust-analyzer.moveItemUp",
214 "title": "Move item up",
215 "category": "Rust Analyzer"
216 },
217 {
218 "command": "rust-analyzer.moveItemDown",
219 "title": "Move item down",
220 "category": "Rust Analyzer"
211 } 221 }
212 ], 222 ],
213 "keybindings": [ 223 "keybindings": [
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index bed1f0116..1a0805bd3 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -134,6 +134,51 @@ export function joinLines(ctx: Ctx): Cmd {
134 }; 134 };
135} 135}
136 136
137export function moveItemUp(ctx: Ctx): Cmd {
138 return moveItem(ctx, ra.Direction.Up);
139}
140
141export function moveItemDown(ctx: Ctx): Cmd {
142 return moveItem(ctx, ra.Direction.Down);
143}
144
145export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
146 return async () => {
147 const editor = ctx.activeRustEditor;
148 const client = ctx.client;
149 if (!editor || !client) return;
150
151 const edit = await client.sendRequest(ra.moveItem, {
152 range: client.code2ProtocolConverter.asRange(editor.selection),
153 textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
154 direction
155 });
156
157 if (!edit) return;
158
159 let cursor: vscode.Position | null = null;
160
161 await editor.edit((builder) => {
162 client.protocol2CodeConverter.asTextEdits(edit.edits).forEach((edit: any) => {
163 builder.replace(edit.range, edit.newText);
164
165 if (direction === ra.Direction.Up) {
166 if (!cursor || edit.range.end.isBeforeOrEqual(cursor)) {
167 cursor = edit.range.end;
168 }
169 } else {
170 if (!cursor || edit.range.end.isAfterOrEqual(cursor)) {
171 cursor = edit.range.end;
172 }
173 }
174 });
175 }).then(() => {
176 const newPosition = cursor ?? editor.selection.start;
177 editor.selection = new vscode.Selection(newPosition, newPosition);
178 });
179 };
180}
181
137export function onEnter(ctx: Ctx): Cmd { 182export function onEnter(ctx: Ctx): Cmd {
138 async function handleKeypress() { 183 async function handleKeypress() {
139 const editor = ctx.activeRustEditor; 184 const editor = ctx.activeRustEditor;
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 52de29e04..00e128b8c 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
127export interface OpenCargoTomlParams { 127export interface OpenCargoTomlParams {
128 textDocument: lc.TextDocumentIdentifier; 128 textDocument: lc.TextDocumentIdentifier;
129} 129}
130
131export const moveItem = new lc.RequestType<MoveItemParams, lc.TextDocumentEdit | void, void>("experimental/moveItem");
132
133export interface MoveItemParams {
134 textDocument: lc.TextDocumentIdentifier;
135 range: lc.Range;
136 direction: Direction;
137}
138
139export const enum Direction {
140 Up = "Up",
141 Down = "Down"
142}
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 925103f56..643fb643f 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -114,6 +114,8 @@ async function tryActivate(context: vscode.ExtensionContext) {
114 ctx.registerCommand('openDocs', commands.openDocs); 114 ctx.registerCommand('openDocs', commands.openDocs);
115 ctx.registerCommand('openCargoToml', commands.openCargoToml); 115 ctx.registerCommand('openCargoToml', commands.openCargoToml);
116 ctx.registerCommand('peekTests', commands.peekTests); 116 ctx.registerCommand('peekTests', commands.peekTests);
117 ctx.registerCommand('moveItemUp', commands.moveItemUp);
118 ctx.registerCommand('moveItemDown', commands.moveItemDown);
117 119
118 defaultOnEnter.dispose(); 120 defaultOnEnter.dispose();
119 ctx.registerCommand('onEnter', commands.onEnter); 121 ctx.registerCommand('onEnter', commands.onEnter);