Skip to content

Commit 6ef6e4d

Browse files
committed
Improved node lookup algorithm, added more movable nodes
1 parent 904bdff commit 6ef6e4d

File tree

1 file changed

+254
-13
lines changed

1 file changed

+254
-13
lines changed

crates/ide/src/move_item.rs

Lines changed: 254 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ use hir::Semantics;
44
use ide_db::{base_db::FileRange, RootDatabase};
55
use itertools::Itertools;
66
use syntax::{
7-
algo, ast, match_ast, AstNode, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, TextRange,
7+
algo, ast, match_ast, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange,
8+
TokenAtOffset,
89
};
910
use text_edit::{TextEdit, TextEditBuilder};
1011

12+
#[derive(Copy, Clone, Debug)]
1113
pub enum Direction {
1214
Up,
1315
Down,
@@ -31,14 +33,19 @@ pub(crate) fn move_item(
3133
let sema = Semantics::new(db);
3234
let file = sema.parse(range.file_id);
3335

34-
let item = file.syntax().covering_element(range.range);
36+
let item = if range.range.is_empty() {
37+
SyntaxElement::Token(pick_best(file.syntax().token_at_offset(range.range.start()))?)
38+
} else {
39+
file.syntax().covering_element(range.range)
40+
};
41+
3542
find_ancestors(item, direction, range.range)
3643
}
3744

3845
fn find_ancestors(item: SyntaxElement, direction: Direction, range: TextRange) -> Option<TextEdit> {
3946
let root = match item {
40-
NodeOrToken::Node(node) => node,
41-
NodeOrToken::Token(token) => token.parent()?,
47+
SyntaxElement::Node(node) => node,
48+
SyntaxElement::Token(token) => token.parent()?,
4249
};
4350

4451
let movable = [
@@ -51,6 +58,11 @@ fn find_ancestors(item: SyntaxElement, direction: Direction, range: TextRange) -
5158
SyntaxKind::PARAM,
5259
SyntaxKind::LET_STMT,
5360
SyntaxKind::EXPR_STMT,
61+
SyntaxKind::IF_EXPR,
62+
SyntaxKind::FOR_EXPR,
63+
SyntaxKind::LOOP_EXPR,
64+
SyntaxKind::WHILE_EXPR,
65+
SyntaxKind::RETURN_EXPR,
5466
SyntaxKind::MATCH_EXPR,
5567
SyntaxKind::MACRO_CALL,
5668
SyntaxKind::TYPE_ALIAS,
@@ -83,11 +95,11 @@ fn move_in_direction(
8395
) -> Option<TextEdit> {
8496
match_ast! {
8597
match node {
86-
ast::ArgList(it) => swap_sibling_in_list(it.args(), range, direction),
87-
ast::GenericParamList(it) => swap_sibling_in_list(it.generic_params(), range, direction),
88-
ast::GenericArgList(it) => swap_sibling_in_list(it.generic_args(), range, direction),
89-
ast::VariantList(it) => swap_sibling_in_list(it.variants(), range, direction),
90-
ast::TypeBoundList(it) => swap_sibling_in_list(it.bounds(), range, direction),
98+
ast::ArgList(it) => swap_sibling_in_list(node, it.args(), range, direction),
99+
ast::GenericParamList(it) => swap_sibling_in_list(node, it.generic_params(), range, direction),
100+
ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction),
101+
ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction),
102+
ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction),
91103
_ => Some(replace_nodes(node, &match direction {
92104
Direction::Up => node.prev_sibling(),
93105
Direction::Down => node.next_sibling(),
@@ -97,19 +109,27 @@ fn move_in_direction(
97109
}
98110

99111
fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
112+
node: &SyntaxNode,
100113
list: I,
101114
range: TextRange,
102115
direction: Direction,
103116
) -> Option<TextEdit> {
104-
let (l, r) = list
117+
let list_lookup = list
105118
.tuple_windows()
106119
.filter(|(l, r)| match direction {
107120
Direction::Up => r.syntax().text_range().contains_range(range),
108121
Direction::Down => l.syntax().text_range().contains_range(range),
109122
})
110-
.next()?;
111-
112-
Some(replace_nodes(l.syntax(), r.syntax()))
123+
.next();
124+
125+
if let Some((l, r)) = list_lookup {
126+
Some(replace_nodes(l.syntax(), r.syntax()))
127+
} else {
128+
// Cursor is beyond any movable list item (for example, on curly brace in enum).
129+
// It's not necessary, that parent of list is movable (arg list's parent is not, for example),
130+
// and we have to continue tree traversal to find suitable node.
131+
find_ancestors(SyntaxElement::Node(node.parent()?), direction, range)
132+
}
113133
}
114134

115135
fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit {
@@ -121,6 +141,17 @@ fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit {
121141
edit.finish()
122142
}
123143

144+
fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
145+
return tokens.max_by_key(priority);
146+
fn priority(n: &SyntaxToken) -> usize {
147+
match n.kind() {
148+
SyntaxKind::IDENT | SyntaxKind::LIFETIME_IDENT => 2,
149+
kind if kind.is_trivia() => 0,
150+
_ => 1,
151+
}
152+
}
153+
}
154+
124155
#[cfg(test)]
125156
mod tests {
126157
use crate::fixture;
@@ -260,6 +291,107 @@ fn main() {
260291
expect![[r#"
261292
fn main() {
262293
println!("All I want to say is...");
294+
println!("Hello, world");
295+
}
296+
"#]],
297+
Direction::Up,
298+
);
299+
check(
300+
r#"
301+
fn main() {
302+
println!("Hello, world");
303+
304+
if true {
305+
println!("Test");
306+
}$0$0
307+
}
308+
"#,
309+
expect![[r#"
310+
fn main() {
311+
if true {
312+
println!("Test");
313+
}
314+
315+
println!("Hello, world");
316+
}
317+
"#]],
318+
Direction::Up,
319+
);
320+
check(
321+
r#"
322+
fn main() {
323+
println!("Hello, world");
324+
325+
for i in 0..10 {
326+
println!("Test");
327+
}$0$0
328+
}
329+
"#,
330+
expect![[r#"
331+
fn main() {
332+
for i in 0..10 {
333+
println!("Test");
334+
}
335+
336+
println!("Hello, world");
337+
}
338+
"#]],
339+
Direction::Up,
340+
);
341+
check(
342+
r#"
343+
fn main() {
344+
println!("Hello, world");
345+
346+
loop {
347+
println!("Test");
348+
}$0$0
349+
}
350+
"#,
351+
expect![[r#"
352+
fn main() {
353+
loop {
354+
println!("Test");
355+
}
356+
357+
println!("Hello, world");
358+
}
359+
"#]],
360+
Direction::Up,
361+
);
362+
check(
363+
r#"
364+
fn main() {
365+
println!("Hello, world");
366+
367+
while true {
368+
println!("Test");
369+
}$0$0
370+
}
371+
"#,
372+
expect![[r#"
373+
fn main() {
374+
while true {
375+
println!("Test");
376+
}
377+
378+
println!("Hello, world");
379+
}
380+
"#]],
381+
Direction::Up,
382+
);
383+
check(
384+
r#"
385+
fn main() {
386+
println!("Hello, world");
387+
388+
return 123;$0$0
389+
}
390+
"#,
391+
expect![[r#"
392+
fn main() {
393+
return 123;
394+
263395
println!("Hello, world");
264396
}
265397
"#]],
@@ -614,6 +746,115 @@ fn test() {
614746
);
615747
}
616748

749+
#[test]
750+
fn test_cursor_at_item_start() {
751+
check(
752+
r#"
753+
$0$0#[derive(Debug)]
754+
enum FooBar {
755+
Foo,
756+
Bar,
757+
}
758+
759+
fn main() {}
760+
"#,
761+
expect![[r#"
762+
fn main() {}
763+
764+
#[derive(Debug)]
765+
enum FooBar {
766+
Foo,
767+
Bar,
768+
}
769+
"#]],
770+
Direction::Down,
771+
);
772+
check(
773+
r#"
774+
$0$0enum FooBar {
775+
Foo,
776+
Bar,
777+
}
778+
779+
fn main() {}
780+
"#,
781+
expect![[r#"
782+
fn main() {}
783+
784+
enum FooBar {
785+
Foo,
786+
Bar,
787+
}
788+
"#]],
789+
Direction::Down,
790+
);
791+
check(
792+
r#"
793+
struct Test;
794+
795+
trait SomeTrait {}
796+
797+
$0$0impl SomeTrait for Test {}
798+
799+
fn main() {}
800+
"#,
801+
expect![[r#"
802+
struct Test;
803+
804+
impl SomeTrait for Test {}
805+
806+
trait SomeTrait {}
807+
808+
fn main() {}
809+
"#]],
810+
Direction::Up,
811+
);
812+
}
813+
814+
#[test]
815+
fn test_cursor_at_item_end() {
816+
check(
817+
r#"
818+
enum FooBar {
819+
Foo,
820+
Bar,
821+
}$0$0
822+
823+
fn main() {}
824+
"#,
825+
expect![[r#"
826+
fn main() {}
827+
828+
enum FooBar {
829+
Foo,
830+
Bar,
831+
}
832+
"#]],
833+
Direction::Down,
834+
);
835+
check(
836+
r#"
837+
struct Test;
838+
839+
trait SomeTrait {}
840+
841+
impl SomeTrait for Test {}$0$0
842+
843+
fn main() {}
844+
"#,
845+
expect![[r#"
846+
struct Test;
847+
848+
impl SomeTrait for Test {}
849+
850+
trait SomeTrait {}
851+
852+
fn main() {}
853+
"#]],
854+
Direction::Up,
855+
);
856+
}
857+
617858
#[test]
618859
fn handles_empty_file() {
619860
check(r#"$0$0"#, expect![[r#""#]], Direction::Up);

0 commit comments

Comments
 (0)