diff options
Diffstat (limited to 'crates/ra_ide_api_light')
18 files changed, 3587 insertions, 0 deletions
diff --git a/crates/ra_ide_api_light/Cargo.toml b/crates/ra_ide_api_light/Cargo.toml new file mode 100644 index 000000000..a97d2308f --- /dev/null +++ b/crates/ra_ide_api_light/Cargo.toml | |||
@@ -0,0 +1,19 @@ | |||
1 | [package] | ||
2 | edition = "2018" | ||
3 | name = "ra_editor" | ||
4 | version = "0.1.0" | ||
5 | authors = ["Aleksey Kladov <[email protected]>"] | ||
6 | publish = false | ||
7 | |||
8 | [dependencies] | ||
9 | itertools = "0.8.0" | ||
10 | superslice = "0.1.0" | ||
11 | join_to_string = "0.1.1" | ||
12 | rustc-hash = "1.0" | ||
13 | |||
14 | ra_syntax = { path = "../ra_syntax" } | ||
15 | ra_text_edit = { path = "../ra_text_edit" } | ||
16 | |||
17 | [dev-dependencies] | ||
18 | test_utils = { path = "../test_utils" } | ||
19 | proptest = "0.8.7" | ||
diff --git a/crates/ra_ide_api_light/src/assists.rs b/crates/ra_ide_api_light/src/assists.rs new file mode 100644 index 000000000..83eabfc85 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists.rs | |||
@@ -0,0 +1,209 @@ | |||
1 | //! This modules contains various "assits": suggestions for source code edits | ||
2 | //! which are likely to occur at a given cursor positon. For example, if the | ||
3 | //! cursor is on the `,`, a possible assist is swapping the elments around the | ||
4 | //! comma. | ||
5 | |||
6 | mod flip_comma; | ||
7 | mod add_derive; | ||
8 | mod add_impl; | ||
9 | mod introduce_variable; | ||
10 | mod change_visibility; | ||
11 | mod split_import; | ||
12 | mod replace_if_let_with_match; | ||
13 | |||
14 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
15 | use ra_syntax::{ | ||
16 | Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode, | ||
17 | algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, | ||
18 | ast::{self, AstToken}, | ||
19 | }; | ||
20 | use itertools::Itertools; | ||
21 | |||
22 | pub use self::{ | ||
23 | flip_comma::flip_comma, | ||
24 | add_derive::add_derive, | ||
25 | add_impl::add_impl, | ||
26 | introduce_variable::introduce_variable, | ||
27 | change_visibility::change_visibility, | ||
28 | split_import::split_import, | ||
29 | replace_if_let_with_match::replace_if_let_with_match, | ||
30 | }; | ||
31 | |||
32 | /// Return all the assists applicable at the given position. | ||
33 | pub fn assists(file: &SourceFile, range: TextRange) -> Vec<LocalEdit> { | ||
34 | let ctx = AssistCtx::new(file, range); | ||
35 | [ | ||
36 | flip_comma, | ||
37 | add_derive, | ||
38 | add_impl, | ||
39 | introduce_variable, | ||
40 | change_visibility, | ||
41 | split_import, | ||
42 | replace_if_let_with_match, | ||
43 | ] | ||
44 | .iter() | ||
45 | .filter_map(|&assist| ctx.clone().apply(assist)) | ||
46 | .collect() | ||
47 | } | ||
48 | |||
49 | #[derive(Debug)] | ||
50 | pub struct LocalEdit { | ||
51 | pub label: String, | ||
52 | pub edit: TextEdit, | ||
53 | pub cursor_position: Option<TextUnit>, | ||
54 | } | ||
55 | |||
56 | fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { | ||
57 | node.siblings(direction) | ||
58 | .skip(1) | ||
59 | .find(|node| !node.kind().is_trivia()) | ||
60 | } | ||
61 | |||
62 | /// `AssistCtx` allows to apply an assist or check if it could be applied. | ||
63 | /// | ||
64 | /// Assists use a somewhat overengeneered approach, given the current needs. The | ||
65 | /// assists workflow consists of two phases. In the first phase, a user asks for | ||
66 | /// the list of available assists. In the second phase, the user picks a | ||
67 | /// particular assist and it gets applied. | ||
68 | /// | ||
69 | /// There are two peculiarities here: | ||
70 | /// | ||
71 | /// * first, we ideally avoid computing more things then neccessary to answer | ||
72 | /// "is assist applicable" in the first phase. | ||
73 | /// * second, when we are appling assist, we don't have a gurantee that there | ||
74 | /// weren't any changes between the point when user asked for assists and when | ||
75 | /// they applied a particular assist. So, when applying assist, we need to do | ||
76 | /// all the checks from scratch. | ||
77 | /// | ||
78 | /// To avoid repeating the same code twice for both "check" and "apply" | ||
79 | /// functions, we use an approach remeniscent of that of Django's function based | ||
80 | /// views dealing with forms. Each assist receives a runtime parameter, | ||
81 | /// `should_compute_edit`. It first check if an edit is applicable (potentially | ||
82 | /// computing info required to compute the actual edit). If it is applicable, | ||
83 | /// and `should_compute_edit` is `true`, it then computes the actual edit. | ||
84 | /// | ||
85 | /// So, to implement the original assists workflow, we can first apply each edit | ||
86 | /// with `should_compute_edit = false`, and then applying the selected edit | ||
87 | /// again, with `should_compute_edit = true` this time. | ||
88 | /// | ||
89 | /// Note, however, that we don't actually use such two-phase logic at the | ||
90 | /// moment, because the LSP API is pretty awkward in this place, and it's much | ||
91 | /// easier to just compute the edit eagarly :-) | ||
92 | #[derive(Debug, Clone)] | ||
93 | pub struct AssistCtx<'a> { | ||
94 | source_file: &'a SourceFile, | ||
95 | range: TextRange, | ||
96 | should_compute_edit: bool, | ||
97 | } | ||
98 | |||
99 | #[derive(Debug)] | ||
100 | pub enum Assist { | ||
101 | Applicable, | ||
102 | Edit(LocalEdit), | ||
103 | } | ||
104 | |||
105 | #[derive(Default)] | ||
106 | struct AssistBuilder { | ||
107 | edit: TextEditBuilder, | ||
108 | cursor_position: Option<TextUnit>, | ||
109 | } | ||
110 | |||
111 | impl<'a> AssistCtx<'a> { | ||
112 | pub fn new(source_file: &'a SourceFile, range: TextRange) -> AssistCtx { | ||
113 | AssistCtx { | ||
114 | source_file, | ||
115 | range, | ||
116 | should_compute_edit: false, | ||
117 | } | ||
118 | } | ||
119 | |||
120 | pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> { | ||
121 | self.should_compute_edit = true; | ||
122 | match assist(self) { | ||
123 | None => None, | ||
124 | Some(Assist::Edit(e)) => Some(e), | ||
125 | Some(Assist::Applicable) => unreachable!(), | ||
126 | } | ||
127 | } | ||
128 | |||
129 | pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool { | ||
130 | self.should_compute_edit = false; | ||
131 | match assist(self) { | ||
132 | None => false, | ||
133 | Some(Assist::Edit(_)) => unreachable!(), | ||
134 | Some(Assist::Applicable) => true, | ||
135 | } | ||
136 | } | ||
137 | |||
138 | fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> { | ||
139 | if !self.should_compute_edit { | ||
140 | return Some(Assist::Applicable); | ||
141 | } | ||
142 | let mut edit = AssistBuilder::default(); | ||
143 | f(&mut edit); | ||
144 | Some(Assist::Edit(LocalEdit { | ||
145 | label: label.into(), | ||
146 | edit: edit.edit.finish(), | ||
147 | cursor_position: edit.cursor_position, | ||
148 | })) | ||
149 | } | ||
150 | |||
151 | pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { | ||
152 | find_leaf_at_offset(self.source_file.syntax(), self.range.start()) | ||
153 | } | ||
154 | pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> { | ||
155 | find_node_at_offset(self.source_file.syntax(), self.range.start()) | ||
156 | } | ||
157 | pub(crate) fn covering_node(&self) -> &'a SyntaxNode { | ||
158 | find_covering_node(self.source_file.syntax(), self.range) | ||
159 | } | ||
160 | } | ||
161 | |||
162 | impl AssistBuilder { | ||
163 | fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { | ||
164 | self.edit.replace(range, replace_with.into()) | ||
165 | } | ||
166 | fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into<String>) { | ||
167 | let mut replace_with = replace_with.into(); | ||
168 | if let Some(indent) = calc_indent(node) { | ||
169 | replace_with = reindent(&replace_with, indent) | ||
170 | } | ||
171 | self.replace(node.range(), replace_with) | ||
172 | } | ||
173 | #[allow(unused)] | ||
174 | fn delete(&mut self, range: TextRange) { | ||
175 | self.edit.delete(range) | ||
176 | } | ||
177 | fn insert(&mut self, offset: TextUnit, text: impl Into<String>) { | ||
178 | self.edit.insert(offset, text.into()) | ||
179 | } | ||
180 | fn set_cursor(&mut self, offset: TextUnit) { | ||
181 | self.cursor_position = Some(offset) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | fn calc_indent(node: &SyntaxNode) -> Option<&str> { | ||
186 | let prev = node.prev_sibling()?; | ||
187 | let ws_text = ast::Whitespace::cast(prev)?.text(); | ||
188 | ws_text.rfind('\n').map(|pos| &ws_text[pos + 1..]) | ||
189 | } | ||
190 | |||
191 | fn reindent(text: &str, indent: &str) -> String { | ||
192 | let indent = format!("\n{}", indent); | ||
193 | text.lines().intersperse(&indent).collect() | ||
194 | } | ||
195 | |||
196 | #[cfg(test)] | ||
197 | fn check_assist(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) { | ||
198 | crate::test_utils::check_action(before, after, |file, off| { | ||
199 | let range = TextRange::offset_len(off, 0.into()); | ||
200 | AssistCtx::new(file, range).apply(assist) | ||
201 | }) | ||
202 | } | ||
203 | |||
204 | #[cfg(test)] | ||
205 | fn check_assist_range(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) { | ||
206 | crate::test_utils::check_action_range(before, after, |file, range| { | ||
207 | AssistCtx::new(file, range).apply(assist) | ||
208 | }) | ||
209 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/add_derive.rs b/crates/ra_ide_api_light/src/assists/add_derive.rs new file mode 100644 index 000000000..6e964d011 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/add_derive.rs | |||
@@ -0,0 +1,84 @@ | |||
1 | use ra_syntax::{ | ||
2 | ast::{self, AstNode, AttrsOwner}, | ||
3 | SyntaxKind::{WHITESPACE, COMMENT}, | ||
4 | TextUnit, | ||
5 | }; | ||
6 | |||
7 | use crate::assists::{AssistCtx, Assist}; | ||
8 | |||
9 | pub fn add_derive(ctx: AssistCtx) -> Option<Assist> { | ||
10 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; | ||
11 | let node_start = derive_insertion_offset(nominal)?; | ||
12 | ctx.build("add `#[derive]`", |edit| { | ||
13 | let derive_attr = nominal | ||
14 | .attrs() | ||
15 | .filter_map(|x| x.as_call()) | ||
16 | .filter(|(name, _arg)| name == "derive") | ||
17 | .map(|(_name, arg)| arg) | ||
18 | .next(); | ||
19 | let offset = match derive_attr { | ||
20 | None => { | ||
21 | edit.insert(node_start, "#[derive()]\n"); | ||
22 | node_start + TextUnit::of_str("#[derive(") | ||
23 | } | ||
24 | Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), | ||
25 | }; | ||
26 | edit.set_cursor(offset) | ||
27 | }) | ||
28 | } | ||
29 | |||
30 | // Insert `derive` after doc comments. | ||
31 | fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option<TextUnit> { | ||
32 | let non_ws_child = nominal | ||
33 | .syntax() | ||
34 | .children() | ||
35 | .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; | ||
36 | Some(non_ws_child.range().start()) | ||
37 | } | ||
38 | |||
39 | #[cfg(test)] | ||
40 | mod tests { | ||
41 | use super::*; | ||
42 | use crate::assists::check_assist; | ||
43 | |||
44 | #[test] | ||
45 | fn add_derive_new() { | ||
46 | check_assist( | ||
47 | add_derive, | ||
48 | "struct Foo { a: i32, <|>}", | ||
49 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
50 | ); | ||
51 | check_assist( | ||
52 | add_derive, | ||
53 | "struct Foo { <|> a: i32, }", | ||
54 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
55 | ); | ||
56 | } | ||
57 | |||
58 | #[test] | ||
59 | fn add_derive_existing() { | ||
60 | check_assist( | ||
61 | add_derive, | ||
62 | "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", | ||
63 | "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", | ||
64 | ); | ||
65 | } | ||
66 | |||
67 | #[test] | ||
68 | fn add_derive_new_with_doc_comment() { | ||
69 | check_assist( | ||
70 | add_derive, | ||
71 | " | ||
72 | /// `Foo` is a pretty important struct. | ||
73 | /// It does stuff. | ||
74 | struct Foo { a: i32<|>, } | ||
75 | ", | ||
76 | " | ||
77 | /// `Foo` is a pretty important struct. | ||
78 | /// It does stuff. | ||
79 | #[derive(<|>)] | ||
80 | struct Foo { a: i32, } | ||
81 | ", | ||
82 | ); | ||
83 | } | ||
84 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/add_impl.rs b/crates/ra_ide_api_light/src/assists/add_impl.rs new file mode 100644 index 000000000..2eda7cae2 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/add_impl.rs | |||
@@ -0,0 +1,66 @@ | |||
1 | use join_to_string::join; | ||
2 | use ra_syntax::{ | ||
3 | ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, | ||
4 | TextUnit, | ||
5 | }; | ||
6 | |||
7 | use crate::assists::{AssistCtx, Assist}; | ||
8 | |||
9 | pub fn add_impl(ctx: AssistCtx) -> Option<Assist> { | ||
10 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; | ||
11 | let name = nominal.name()?; | ||
12 | ctx.build("add impl", |edit| { | ||
13 | let type_params = nominal.type_param_list(); | ||
14 | let start_offset = nominal.syntax().range().end(); | ||
15 | let mut buf = String::new(); | ||
16 | buf.push_str("\n\nimpl"); | ||
17 | if let Some(type_params) = type_params { | ||
18 | type_params.syntax().text().push_to(&mut buf); | ||
19 | } | ||
20 | buf.push_str(" "); | ||
21 | buf.push_str(name.text().as_str()); | ||
22 | if let Some(type_params) = type_params { | ||
23 | let lifetime_params = type_params | ||
24 | .lifetime_params() | ||
25 | .filter_map(|it| it.lifetime()) | ||
26 | .map(|it| it.text()); | ||
27 | let type_params = type_params | ||
28 | .type_params() | ||
29 | .filter_map(|it| it.name()) | ||
30 | .map(|it| it.text()); | ||
31 | join(lifetime_params.chain(type_params)) | ||
32 | .surround_with("<", ">") | ||
33 | .to_buf(&mut buf); | ||
34 | } | ||
35 | buf.push_str(" {\n"); | ||
36 | edit.set_cursor(start_offset + TextUnit::of_str(&buf)); | ||
37 | buf.push_str("\n}"); | ||
38 | edit.insert(start_offset, buf); | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | #[cfg(test)] | ||
43 | mod tests { | ||
44 | use super::*; | ||
45 | use crate::assists::check_assist; | ||
46 | |||
47 | #[test] | ||
48 | fn test_add_impl() { | ||
49 | check_assist( | ||
50 | add_impl, | ||
51 | "struct Foo {<|>}\n", | ||
52 | "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", | ||
53 | ); | ||
54 | check_assist( | ||
55 | add_impl, | ||
56 | "struct Foo<T: Clone> {<|>}", | ||
57 | "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}", | ||
58 | ); | ||
59 | check_assist( | ||
60 | add_impl, | ||
61 | "struct Foo<'a, T: Foo<'a>> {<|>}", | ||
62 | "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", | ||
63 | ); | ||
64 | } | ||
65 | |||
66 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/change_visibility.rs b/crates/ra_ide_api_light/src/assists/change_visibility.rs new file mode 100644 index 000000000..89729e2c2 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/change_visibility.rs | |||
@@ -0,0 +1,116 @@ | |||
1 | use ra_syntax::{ | ||
2 | AstNode, | ||
3 | ast::{self, VisibilityOwner, NameOwner}, | ||
4 | SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT}, | ||
5 | }; | ||
6 | |||
7 | use crate::assists::{AssistCtx, Assist}; | ||
8 | |||
9 | pub fn change_visibility(ctx: AssistCtx) -> Option<Assist> { | ||
10 | if let Some(vis) = ctx.node_at_offset::<ast::Visibility>() { | ||
11 | return change_vis(ctx, vis); | ||
12 | } | ||
13 | add_vis(ctx) | ||
14 | } | ||
15 | |||
16 | fn add_vis(ctx: AssistCtx) -> Option<Assist> { | ||
17 | let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() { | ||
18 | FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, | ||
19 | _ => false, | ||
20 | }); | ||
21 | |||
22 | let offset = if let Some(keyword) = item_keyword { | ||
23 | let parent = keyword.parent()?; | ||
24 | let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; | ||
25 | // Parent is not a definition, can't add visibility | ||
26 | if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) { | ||
27 | return None; | ||
28 | } | ||
29 | // Already have visibility, do nothing | ||
30 | if parent.children().any(|child| child.kind() == VISIBILITY) { | ||
31 | return None; | ||
32 | } | ||
33 | parent.range().start() | ||
34 | } else { | ||
35 | let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?; | ||
36 | let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?; | ||
37 | if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() { | ||
38 | return None; | ||
39 | } | ||
40 | field.syntax().range().start() | ||
41 | }; | ||
42 | |||
43 | ctx.build("make pub(crate)", |edit| { | ||
44 | edit.insert(offset, "pub(crate) "); | ||
45 | edit.set_cursor(offset); | ||
46 | }) | ||
47 | } | ||
48 | |||
49 | fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> { | ||
50 | if vis.syntax().text() != "pub" { | ||
51 | return None; | ||
52 | } | ||
53 | ctx.build("chage to pub(crate)", |edit| { | ||
54 | edit.replace(vis.syntax().range(), "pub(crate)"); | ||
55 | edit.set_cursor(vis.syntax().range().start()); | ||
56 | }) | ||
57 | } | ||
58 | |||
59 | #[cfg(test)] | ||
60 | mod tests { | ||
61 | use super::*; | ||
62 | use crate::assists::check_assist; | ||
63 | |||
64 | #[test] | ||
65 | fn change_visibility_adds_pub_crate_to_items() { | ||
66 | check_assist( | ||
67 | change_visibility, | ||
68 | "<|>fn foo() {}", | ||
69 | "<|>pub(crate) fn foo() {}", | ||
70 | ); | ||
71 | check_assist( | ||
72 | change_visibility, | ||
73 | "f<|>n foo() {}", | ||
74 | "<|>pub(crate) fn foo() {}", | ||
75 | ); | ||
76 | check_assist( | ||
77 | change_visibility, | ||
78 | "<|>struct Foo {}", | ||
79 | "<|>pub(crate) struct Foo {}", | ||
80 | ); | ||
81 | check_assist( | ||
82 | change_visibility, | ||
83 | "<|>mod foo {}", | ||
84 | "<|>pub(crate) mod foo {}", | ||
85 | ); | ||
86 | check_assist( | ||
87 | change_visibility, | ||
88 | "<|>trait Foo {}", | ||
89 | "<|>pub(crate) trait Foo {}", | ||
90 | ); | ||
91 | check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}"); | ||
92 | check_assist( | ||
93 | change_visibility, | ||
94 | "unsafe f<|>n foo() {}", | ||
95 | "<|>pub(crate) unsafe fn foo() {}", | ||
96 | ); | ||
97 | } | ||
98 | |||
99 | #[test] | ||
100 | fn change_visibility_works_with_struct_fields() { | ||
101 | check_assist( | ||
102 | change_visibility, | ||
103 | "struct S { <|>field: u32 }", | ||
104 | "struct S { <|>pub(crate) field: u32 }", | ||
105 | ) | ||
106 | } | ||
107 | |||
108 | #[test] | ||
109 | fn change_visibility_pub_to_pub_crate() { | ||
110 | check_assist( | ||
111 | change_visibility, | ||
112 | "<|>pub fn foo() {}", | ||
113 | "<|>pub(crate) fn foo() {}", | ||
114 | ) | ||
115 | } | ||
116 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/flip_comma.rs b/crates/ra_ide_api_light/src/assists/flip_comma.rs new file mode 100644 index 000000000..a343413cc --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/flip_comma.rs | |||
@@ -0,0 +1,31 @@ | |||
1 | use ra_syntax::{ | ||
2 | Direction, | ||
3 | SyntaxKind::COMMA, | ||
4 | }; | ||
5 | |||
6 | use crate::assists::{non_trivia_sibling, AssistCtx, Assist}; | ||
7 | |||
8 | pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> { | ||
9 | let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?; | ||
10 | let prev = non_trivia_sibling(comma, Direction::Prev)?; | ||
11 | let next = non_trivia_sibling(comma, Direction::Next)?; | ||
12 | ctx.build("flip comma", |edit| { | ||
13 | edit.replace(prev.range(), next.text()); | ||
14 | edit.replace(next.range(), prev.text()); | ||
15 | }) | ||
16 | } | ||
17 | |||
18 | #[cfg(test)] | ||
19 | mod tests { | ||
20 | use super::*; | ||
21 | use crate::assists::check_assist; | ||
22 | |||
23 | #[test] | ||
24 | fn flip_comma_works_for_function_parameters() { | ||
25 | check_assist( | ||
26 | flip_comma, | ||
27 | "fn foo(x: i32,<|> y: Result<(), ()>) {}", | ||
28 | "fn foo(y: Result<(), ()>,<|> x: i32) {}", | ||
29 | ) | ||
30 | } | ||
31 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/introduce_variable.rs b/crates/ra_ide_api_light/src/assists/introduce_variable.rs new file mode 100644 index 000000000..523ec7034 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/introduce_variable.rs | |||
@@ -0,0 +1,144 @@ | |||
1 | use ra_syntax::{ | ||
2 | ast::{self, AstNode}, | ||
3 | SyntaxKind::WHITESPACE, | ||
4 | SyntaxNode, TextUnit, | ||
5 | }; | ||
6 | |||
7 | use crate::assists::{AssistCtx, Assist}; | ||
8 | |||
9 | pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option<Assist> { | ||
10 | let node = ctx.covering_node(); | ||
11 | let expr = node.ancestors().filter_map(ast::Expr::cast).next()?; | ||
12 | |||
13 | let anchor_stmt = anchor_stmt(expr)?; | ||
14 | let indent = anchor_stmt.prev_sibling()?; | ||
15 | if indent.kind() != WHITESPACE { | ||
16 | return None; | ||
17 | } | ||
18 | ctx.build("introduce variable", move |edit| { | ||
19 | let mut buf = String::new(); | ||
20 | |||
21 | buf.push_str("let var_name = "); | ||
22 | expr.syntax().text().push_to(&mut buf); | ||
23 | let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) { | ||
24 | Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) | ||
25 | } else { | ||
26 | false | ||
27 | }; | ||
28 | if is_full_stmt { | ||
29 | edit.replace(expr.syntax().range(), buf); | ||
30 | } else { | ||
31 | buf.push_str(";"); | ||
32 | indent.text().push_to(&mut buf); | ||
33 | edit.replace(expr.syntax().range(), "var_name".to_string()); | ||
34 | edit.insert(anchor_stmt.range().start(), buf); | ||
35 | } | ||
36 | edit.set_cursor(anchor_stmt.range().start() + TextUnit::of_str("let ")); | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | /// Statement or last in the block expression, which will follow | ||
41 | /// the freshly introduced var. | ||
42 | fn anchor_stmt(expr: &ast::Expr) -> Option<&SyntaxNode> { | ||
43 | expr.syntax().ancestors().find(|&node| { | ||
44 | if ast::Stmt::cast(node).is_some() { | ||
45 | return true; | ||
46 | } | ||
47 | if let Some(expr) = node | ||
48 | .parent() | ||
49 | .and_then(ast::Block::cast) | ||
50 | .and_then(|it| it.expr()) | ||
51 | { | ||
52 | if expr.syntax() == node { | ||
53 | return true; | ||
54 | } | ||
55 | } | ||
56 | false | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | #[cfg(test)] | ||
61 | mod tests { | ||
62 | use super::*; | ||
63 | use crate::assists::check_assist_range; | ||
64 | |||
65 | #[test] | ||
66 | fn test_introduce_var_simple() { | ||
67 | check_assist_range( | ||
68 | introduce_variable, | ||
69 | " | ||
70 | fn foo() { | ||
71 | foo(<|>1 + 1<|>); | ||
72 | }", | ||
73 | " | ||
74 | fn foo() { | ||
75 | let <|>var_name = 1 + 1; | ||
76 | foo(var_name); | ||
77 | }", | ||
78 | ); | ||
79 | } | ||
80 | |||
81 | #[test] | ||
82 | fn test_introduce_var_expr_stmt() { | ||
83 | check_assist_range( | ||
84 | introduce_variable, | ||
85 | " | ||
86 | fn foo() { | ||
87 | <|>1 + 1<|>; | ||
88 | }", | ||
89 | " | ||
90 | fn foo() { | ||
91 | let <|>var_name = 1 + 1; | ||
92 | }", | ||
93 | ); | ||
94 | } | ||
95 | |||
96 | #[test] | ||
97 | fn test_introduce_var_part_of_expr_stmt() { | ||
98 | check_assist_range( | ||
99 | introduce_variable, | ||
100 | " | ||
101 | fn foo() { | ||
102 | <|>1<|> + 1; | ||
103 | }", | ||
104 | " | ||
105 | fn foo() { | ||
106 | let <|>var_name = 1; | ||
107 | var_name + 1; | ||
108 | }", | ||
109 | ); | ||
110 | } | ||
111 | |||
112 | #[test] | ||
113 | fn test_introduce_var_last_expr() { | ||
114 | check_assist_range( | ||
115 | introduce_variable, | ||
116 | " | ||
117 | fn foo() { | ||
118 | bar(<|>1 + 1<|>) | ||
119 | }", | ||
120 | " | ||
121 | fn foo() { | ||
122 | let <|>var_name = 1 + 1; | ||
123 | bar(var_name) | ||
124 | }", | ||
125 | ); | ||
126 | } | ||
127 | |||
128 | #[test] | ||
129 | fn test_introduce_var_last_full_expr() { | ||
130 | check_assist_range( | ||
131 | introduce_variable, | ||
132 | " | ||
133 | fn foo() { | ||
134 | <|>bar(1 + 1)<|> | ||
135 | }", | ||
136 | " | ||
137 | fn foo() { | ||
138 | let <|>var_name = bar(1 + 1); | ||
139 | var_name | ||
140 | }", | ||
141 | ); | ||
142 | } | ||
143 | |||
144 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs new file mode 100644 index 000000000..30c371480 --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs | |||
@@ -0,0 +1,92 @@ | |||
1 | use ra_syntax::{ | ||
2 | AstNode, SyntaxKind::{L_CURLY, R_CURLY, WHITESPACE}, | ||
3 | ast, | ||
4 | }; | ||
5 | |||
6 | use crate::assists::{AssistCtx, Assist}; | ||
7 | |||
8 | pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> { | ||
9 | let if_expr: &ast::IfExpr = ctx.node_at_offset()?; | ||
10 | let cond = if_expr.condition()?; | ||
11 | let pat = cond.pat()?; | ||
12 | let expr = cond.expr()?; | ||
13 | let then_block = if_expr.then_branch()?; | ||
14 | let else_block = if_expr.else_branch()?; | ||
15 | |||
16 | ctx.build("replace with match", |edit| { | ||
17 | let match_expr = build_match_expr(expr, pat, then_block, else_block); | ||
18 | edit.replace_node_and_indent(if_expr.syntax(), match_expr); | ||
19 | edit.set_cursor(if_expr.syntax().range().start()) | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | fn build_match_expr( | ||
24 | expr: &ast::Expr, | ||
25 | pat1: &ast::Pat, | ||
26 | arm1: &ast::Block, | ||
27 | arm2: &ast::Block, | ||
28 | ) -> String { | ||
29 | let mut buf = String::new(); | ||
30 | buf.push_str(&format!("match {} {{\n", expr.syntax().text())); | ||
31 | buf.push_str(&format!( | ||
32 | " {} => {}\n", | ||
33 | pat1.syntax().text(), | ||
34 | format_arm(arm1) | ||
35 | )); | ||
36 | buf.push_str(&format!(" _ => {}\n", format_arm(arm2))); | ||
37 | buf.push_str("}"); | ||
38 | buf | ||
39 | } | ||
40 | |||
41 | fn format_arm(block: &ast::Block) -> String { | ||
42 | match extract_expression(block) { | ||
43 | None => block.syntax().text().to_string(), | ||
44 | Some(e) => format!("{},", e.syntax().text()), | ||
45 | } | ||
46 | } | ||
47 | |||
48 | fn extract_expression(block: &ast::Block) -> Option<&ast::Expr> { | ||
49 | let expr = block.expr()?; | ||
50 | let non_trivial_children = block.syntax().children().filter(|it| { | ||
51 | !(it == &expr.syntax() | ||
52 | || it.kind() == L_CURLY | ||
53 | || it.kind() == R_CURLY | ||
54 | || it.kind() == WHITESPACE) | ||
55 | }); | ||
56 | if non_trivial_children.count() > 0 { | ||
57 | return None; | ||
58 | } | ||
59 | Some(expr) | ||
60 | } | ||
61 | |||
62 | #[cfg(test)] | ||
63 | mod tests { | ||
64 | use super::*; | ||
65 | use crate::assists::check_assist; | ||
66 | |||
67 | #[test] | ||
68 | fn test_replace_if_let_with_match_unwraps_simple_expressions() { | ||
69 | check_assist( | ||
70 | replace_if_let_with_match, | ||
71 | " | ||
72 | impl VariantData { | ||
73 | pub fn is_struct(&self) -> bool { | ||
74 | if <|>let VariantData::Struct(..) = *self { | ||
75 | true | ||
76 | } else { | ||
77 | false | ||
78 | } | ||
79 | } | ||
80 | } ", | ||
81 | " | ||
82 | impl VariantData { | ||
83 | pub fn is_struct(&self) -> bool { | ||
84 | <|>match *self { | ||
85 | VariantData::Struct(..) => true, | ||
86 | _ => false, | ||
87 | } | ||
88 | } | ||
89 | } ", | ||
90 | ) | ||
91 | } | ||
92 | } | ||
diff --git a/crates/ra_ide_api_light/src/assists/split_import.rs b/crates/ra_ide_api_light/src/assists/split_import.rs new file mode 100644 index 000000000..e4015f07d --- /dev/null +++ b/crates/ra_ide_api_light/src/assists/split_import.rs | |||
@@ -0,0 +1,56 @@ | |||
1 | use ra_syntax::{ | ||
2 | TextUnit, AstNode, SyntaxKind::COLONCOLON, | ||
3 | ast, | ||
4 | algo::generate, | ||
5 | }; | ||
6 | |||
7 | use crate::assists::{AssistCtx, Assist}; | ||
8 | |||
9 | pub fn split_import(ctx: AssistCtx) -> Option<Assist> { | ||
10 | let colon_colon = ctx | ||
11 | .leaf_at_offset() | ||
12 | .find(|leaf| leaf.kind() == COLONCOLON)?; | ||
13 | let path = colon_colon.parent().and_then(ast::Path::cast)?; | ||
14 | let top_path = generate(Some(path), |it| it.parent_path()).last()?; | ||
15 | |||
16 | let use_tree = top_path.syntax().ancestors().find_map(ast::UseTree::cast); | ||
17 | if use_tree.is_none() { | ||
18 | return None; | ||
19 | } | ||
20 | |||
21 | let l_curly = colon_colon.range().end(); | ||
22 | let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) { | ||
23 | Some(tree) => tree.syntax().range().end(), | ||
24 | None => top_path.syntax().range().end(), | ||
25 | }; | ||
26 | |||
27 | ctx.build("split import", |edit| { | ||
28 | edit.insert(l_curly, "{"); | ||
29 | edit.insert(r_curly, "}"); | ||
30 | edit.set_cursor(l_curly + TextUnit::of_str("{")); | ||
31 | }) | ||
32 | } | ||
33 | |||
34 | #[cfg(test)] | ||
35 | mod tests { | ||
36 | use super::*; | ||
37 | use crate::assists::check_assist; | ||
38 | |||
39 | #[test] | ||
40 | fn test_split_import() { | ||
41 | check_assist( | ||
42 | split_import, | ||
43 | "use crate::<|>db::RootDatabase;", | ||
44 | "use crate::{<|>db::RootDatabase};", | ||
45 | ) | ||
46 | } | ||
47 | |||
48 | #[test] | ||
49 | fn split_import_works_with_trees() { | ||
50 | check_assist( | ||
51 | split_import, | ||
52 | "use algo:<|>:visitor::{Visitor, visit}", | ||
53 | "use algo::{<|>visitor::{Visitor, visit}}", | ||
54 | ) | ||
55 | } | ||
56 | } | ||
diff --git a/crates/ra_ide_api_light/src/diagnostics.rs b/crates/ra_ide_api_light/src/diagnostics.rs new file mode 100644 index 000000000..2b695dfdf --- /dev/null +++ b/crates/ra_ide_api_light/src/diagnostics.rs | |||
@@ -0,0 +1,266 @@ | |||
1 | use itertools::Itertools; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | Location, SourceFile, SyntaxKind, TextRange, SyntaxNode, | ||
5 | ast::{self, AstNode}, | ||
6 | |||
7 | }; | ||
8 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
9 | |||
10 | use crate::{Diagnostic, LocalEdit, Severity}; | ||
11 | |||
12 | pub fn diagnostics(file: &SourceFile) -> Vec<Diagnostic> { | ||
13 | fn location_to_range(location: Location) -> TextRange { | ||
14 | match location { | ||
15 | Location::Offset(offset) => TextRange::offset_len(offset, 1.into()), | ||
16 | Location::Range(range) => range, | ||
17 | } | ||
18 | } | ||
19 | |||
20 | let mut errors: Vec<Diagnostic> = file | ||
21 | .errors() | ||
22 | .into_iter() | ||
23 | .map(|err| Diagnostic { | ||
24 | range: location_to_range(err.location()), | ||
25 | msg: format!("Syntax Error: {}", err), | ||
26 | severity: Severity::Error, | ||
27 | fix: None, | ||
28 | }) | ||
29 | .collect(); | ||
30 | |||
31 | for node in file.syntax().descendants() { | ||
32 | check_unnecessary_braces_in_use_statement(&mut errors, node); | ||
33 | check_struct_shorthand_initialization(&mut errors, node); | ||
34 | } | ||
35 | |||
36 | errors | ||
37 | } | ||
38 | |||
39 | fn check_unnecessary_braces_in_use_statement( | ||
40 | acc: &mut Vec<Diagnostic>, | ||
41 | node: &SyntaxNode, | ||
42 | ) -> Option<()> { | ||
43 | let use_tree_list = ast::UseTreeList::cast(node)?; | ||
44 | if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { | ||
45 | let range = use_tree_list.syntax().range(); | ||
46 | let edit = | ||
47 | text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(single_use_tree) | ||
48 | .unwrap_or_else(|| { | ||
49 | let to_replace = single_use_tree.syntax().text().to_string(); | ||
50 | let mut edit_builder = TextEditBuilder::default(); | ||
51 | edit_builder.delete(range); | ||
52 | edit_builder.insert(range.start(), to_replace); | ||
53 | edit_builder.finish() | ||
54 | }); | ||
55 | |||
56 | acc.push(Diagnostic { | ||
57 | range, | ||
58 | msg: format!("Unnecessary braces in use statement"), | ||
59 | severity: Severity::WeakWarning, | ||
60 | fix: Some(LocalEdit { | ||
61 | label: "Remove unnecessary braces".to_string(), | ||
62 | edit, | ||
63 | cursor_position: None, | ||
64 | }), | ||
65 | }); | ||
66 | } | ||
67 | |||
68 | Some(()) | ||
69 | } | ||
70 | |||
71 | fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( | ||
72 | single_use_tree: &ast::UseTree, | ||
73 | ) -> Option<TextEdit> { | ||
74 | let use_tree_list_node = single_use_tree.syntax().parent()?; | ||
75 | if single_use_tree | ||
76 | .path()? | ||
77 | .segment()? | ||
78 | .syntax() | ||
79 | .first_child()? | ||
80 | .kind() | ||
81 | == SyntaxKind::SELF_KW | ||
82 | { | ||
83 | let start = use_tree_list_node.prev_sibling()?.range().start(); | ||
84 | let end = use_tree_list_node.range().end(); | ||
85 | let range = TextRange::from_to(start, end); | ||
86 | let mut edit_builder = TextEditBuilder::default(); | ||
87 | edit_builder.delete(range); | ||
88 | return Some(edit_builder.finish()); | ||
89 | } | ||
90 | None | ||
91 | } | ||
92 | |||
93 | fn check_struct_shorthand_initialization( | ||
94 | acc: &mut Vec<Diagnostic>, | ||
95 | node: &SyntaxNode, | ||
96 | ) -> Option<()> { | ||
97 | let struct_lit = ast::StructLit::cast(node)?; | ||
98 | let named_field_list = struct_lit.named_field_list()?; | ||
99 | for named_field in named_field_list.fields() { | ||
100 | if let (Some(name_ref), Some(expr)) = (named_field.name_ref(), named_field.expr()) { | ||
101 | let field_name = name_ref.syntax().text().to_string(); | ||
102 | let field_expr = expr.syntax().text().to_string(); | ||
103 | if field_name == field_expr { | ||
104 | let mut edit_builder = TextEditBuilder::default(); | ||
105 | edit_builder.delete(named_field.syntax().range()); | ||
106 | edit_builder.insert(named_field.syntax().range().start(), field_name); | ||
107 | let edit = edit_builder.finish(); | ||
108 | |||
109 | acc.push(Diagnostic { | ||
110 | range: named_field.syntax().range(), | ||
111 | msg: format!("Shorthand struct initialization"), | ||
112 | severity: Severity::WeakWarning, | ||
113 | fix: Some(LocalEdit { | ||
114 | label: "use struct shorthand initialization".to_string(), | ||
115 | edit, | ||
116 | cursor_position: None, | ||
117 | }), | ||
118 | }); | ||
119 | } | ||
120 | } | ||
121 | } | ||
122 | Some(()) | ||
123 | } | ||
124 | |||
125 | #[cfg(test)] | ||
126 | mod tests { | ||
127 | use crate::test_utils::assert_eq_text; | ||
128 | |||
129 | use super::*; | ||
130 | |||
131 | type DiagnosticChecker = fn(&mut Vec<Diagnostic>, &SyntaxNode) -> Option<()>; | ||
132 | |||
133 | fn check_not_applicable(code: &str, func: DiagnosticChecker) { | ||
134 | let file = SourceFile::parse(code); | ||
135 | let mut diagnostics = Vec::new(); | ||
136 | for node in file.syntax().descendants() { | ||
137 | func(&mut diagnostics, node); | ||
138 | } | ||
139 | assert!(diagnostics.is_empty()); | ||
140 | } | ||
141 | |||
142 | fn check_apply(before: &str, after: &str, func: DiagnosticChecker) { | ||
143 | let file = SourceFile::parse(before); | ||
144 | let mut diagnostics = Vec::new(); | ||
145 | for node in file.syntax().descendants() { | ||
146 | func(&mut diagnostics, node); | ||
147 | } | ||
148 | let diagnostic = diagnostics | ||
149 | .pop() | ||
150 | .unwrap_or_else(|| panic!("no diagnostics for:\n{}\n", before)); | ||
151 | let fix = diagnostic.fix.unwrap(); | ||
152 | let actual = fix.edit.apply(&before); | ||
153 | assert_eq_text!(after, &actual); | ||
154 | } | ||
155 | |||
156 | #[test] | ||
157 | fn test_check_unnecessary_braces_in_use_statement() { | ||
158 | check_not_applicable( | ||
159 | " | ||
160 | use a; | ||
161 | use a::{c, d::e}; | ||
162 | ", | ||
163 | check_unnecessary_braces_in_use_statement, | ||
164 | ); | ||
165 | check_apply( | ||
166 | "use {b};", | ||
167 | "use b;", | ||
168 | check_unnecessary_braces_in_use_statement, | ||
169 | ); | ||
170 | check_apply( | ||
171 | "use a::{c};", | ||
172 | "use a::c;", | ||
173 | check_unnecessary_braces_in_use_statement, | ||
174 | ); | ||
175 | check_apply( | ||
176 | "use a::{self};", | ||
177 | "use a;", | ||
178 | check_unnecessary_braces_in_use_statement, | ||
179 | ); | ||
180 | check_apply( | ||
181 | "use a::{c, d::{e}};", | ||
182 | "use a::{c, d::e};", | ||
183 | check_unnecessary_braces_in_use_statement, | ||
184 | ); | ||
185 | } | ||
186 | |||
187 | #[test] | ||
188 | fn test_check_struct_shorthand_initialization() { | ||
189 | check_not_applicable( | ||
190 | r#" | ||
191 | struct A { | ||
192 | a: &'static str | ||
193 | } | ||
194 | |||
195 | fn main() { | ||
196 | A { | ||
197 | a: "hello" | ||
198 | } | ||
199 | } | ||
200 | "#, | ||
201 | check_struct_shorthand_initialization, | ||
202 | ); | ||
203 | |||
204 | check_apply( | ||
205 | r#" | ||
206 | struct A { | ||
207 | a: &'static str | ||
208 | } | ||
209 | |||
210 | fn main() { | ||
211 | let a = "haha"; | ||
212 | A { | ||
213 | a: a | ||
214 | } | ||
215 | } | ||
216 | "#, | ||
217 | r#" | ||
218 | struct A { | ||
219 | a: &'static str | ||
220 | } | ||
221 | |||
222 | fn main() { | ||
223 | let a = "haha"; | ||
224 | A { | ||
225 | a | ||
226 | } | ||
227 | } | ||
228 | "#, | ||
229 | check_struct_shorthand_initialization, | ||
230 | ); | ||
231 | |||
232 | check_apply( | ||
233 | r#" | ||
234 | struct A { | ||
235 | a: &'static str, | ||
236 | b: &'static str | ||
237 | } | ||
238 | |||
239 | fn main() { | ||
240 | let a = "haha"; | ||
241 | let b = "bb"; | ||
242 | A { | ||
243 | a: a, | ||
244 | b | ||
245 | } | ||
246 | } | ||
247 | "#, | ||
248 | r#" | ||
249 | struct A { | ||
250 | a: &'static str, | ||
251 | b: &'static str | ||
252 | } | ||
253 | |||
254 | fn main() { | ||
255 | let a = "haha"; | ||
256 | let b = "bb"; | ||
257 | A { | ||
258 | a, | ||
259 | b | ||
260 | } | ||
261 | } | ||
262 | "#, | ||
263 | check_struct_shorthand_initialization, | ||
264 | ); | ||
265 | } | ||
266 | } | ||
diff --git a/crates/ra_ide_api_light/src/extend_selection.rs b/crates/ra_ide_api_light/src/extend_selection.rs new file mode 100644 index 000000000..08cae5a51 --- /dev/null +++ b/crates/ra_ide_api_light/src/extend_selection.rs | |||
@@ -0,0 +1,281 @@ | |||
1 | use ra_syntax::{ | ||
2 | Direction, SyntaxNode, TextRange, TextUnit, | ||
3 | algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset}, | ||
4 | SyntaxKind::*, | ||
5 | }; | ||
6 | |||
7 | pub fn extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> { | ||
8 | let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; | ||
9 | if range.is_empty() { | ||
10 | let offset = range.start(); | ||
11 | let mut leaves = find_leaf_at_offset(root, offset); | ||
12 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { | ||
13 | return Some(extend_ws(root, leaves.next()?, offset)); | ||
14 | } | ||
15 | let leaf_range = match leaves { | ||
16 | LeafAtOffset::None => return None, | ||
17 | LeafAtOffset::Single(l) => { | ||
18 | if string_kinds.contains(&l.kind()) { | ||
19 | extend_single_word_in_comment_or_string(l, offset).unwrap_or_else(|| l.range()) | ||
20 | } else { | ||
21 | l.range() | ||
22 | } | ||
23 | } | ||
24 | LeafAtOffset::Between(l, r) => pick_best(l, r).range(), | ||
25 | }; | ||
26 | return Some(leaf_range); | ||
27 | }; | ||
28 | let node = find_covering_node(root, range); | ||
29 | if string_kinds.contains(&node.kind()) && range == node.range() { | ||
30 | if let Some(range) = extend_comments(node) { | ||
31 | return Some(range); | ||
32 | } | ||
33 | } | ||
34 | |||
35 | match node.ancestors().skip_while(|n| n.range() == range).next() { | ||
36 | None => None, | ||
37 | Some(parent) => Some(parent.range()), | ||
38 | } | ||
39 | } | ||
40 | |||
41 | fn extend_single_word_in_comment_or_string( | ||
42 | leaf: &SyntaxNode, | ||
43 | offset: TextUnit, | ||
44 | ) -> Option<TextRange> { | ||
45 | let text: &str = leaf.leaf_text()?; | ||
46 | let cursor_position: u32 = (offset - leaf.range().start()).into(); | ||
47 | |||
48 | let (before, after) = text.split_at(cursor_position as usize); | ||
49 | |||
50 | fn non_word_char(c: char) -> bool { | ||
51 | !(c.is_alphanumeric() || c == '_') | ||
52 | } | ||
53 | |||
54 | let start_idx = before.rfind(non_word_char)? as u32; | ||
55 | let end_idx = after.find(non_word_char).unwrap_or(after.len()) as u32; | ||
56 | |||
57 | let from: TextUnit = (start_idx + 1).into(); | ||
58 | let to: TextUnit = (cursor_position + end_idx).into(); | ||
59 | |||
60 | let range = TextRange::from_to(from, to); | ||
61 | if range.is_empty() { | ||
62 | None | ||
63 | } else { | ||
64 | Some(range + leaf.range().start()) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | fn extend_ws(root: &SyntaxNode, ws: &SyntaxNode, offset: TextUnit) -> TextRange { | ||
69 | let ws_text = ws.leaf_text().unwrap(); | ||
70 | let suffix = TextRange::from_to(offset, ws.range().end()) - ws.range().start(); | ||
71 | let prefix = TextRange::from_to(ws.range().start(), offset) - ws.range().start(); | ||
72 | let ws_suffix = &ws_text.as_str()[suffix]; | ||
73 | let ws_prefix = &ws_text.as_str()[prefix]; | ||
74 | if ws_text.contains('\n') && !ws_suffix.contains('\n') { | ||
75 | if let Some(node) = ws.next_sibling() { | ||
76 | let start = match ws_prefix.rfind('\n') { | ||
77 | Some(idx) => ws.range().start() + TextUnit::from((idx + 1) as u32), | ||
78 | None => node.range().start(), | ||
79 | }; | ||
80 | let end = if root.text().char_at(node.range().end()) == Some('\n') { | ||
81 | node.range().end() + TextUnit::of_char('\n') | ||
82 | } else { | ||
83 | node.range().end() | ||
84 | }; | ||
85 | return TextRange::from_to(start, end); | ||
86 | } | ||
87 | } | ||
88 | ws.range() | ||
89 | } | ||
90 | |||
91 | fn pick_best<'a>(l: &'a SyntaxNode, r: &'a SyntaxNode) -> &'a SyntaxNode { | ||
92 | return if priority(r) > priority(l) { r } else { l }; | ||
93 | fn priority(n: &SyntaxNode) -> usize { | ||
94 | match n.kind() { | ||
95 | WHITESPACE => 0, | ||
96 | IDENT | SELF_KW | SUPER_KW | CRATE_KW | LIFETIME => 2, | ||
97 | _ => 1, | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | |||
102 | fn extend_comments(node: &SyntaxNode) -> Option<TextRange> { | ||
103 | let prev = adj_comments(node, Direction::Prev); | ||
104 | let next = adj_comments(node, Direction::Next); | ||
105 | if prev != next { | ||
106 | Some(TextRange::from_to(prev.range().start(), next.range().end())) | ||
107 | } else { | ||
108 | None | ||
109 | } | ||
110 | } | ||
111 | |||
112 | fn adj_comments(node: &SyntaxNode, dir: Direction) -> &SyntaxNode { | ||
113 | let mut res = node; | ||
114 | for node in node.siblings(dir) { | ||
115 | match node.kind() { | ||
116 | COMMENT => res = node, | ||
117 | WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (), | ||
118 | _ => break, | ||
119 | } | ||
120 | } | ||
121 | res | ||
122 | } | ||
123 | |||
124 | #[cfg(test)] | ||
125 | mod tests { | ||
126 | use ra_syntax::{SourceFile, AstNode}; | ||
127 | use test_utils::extract_offset; | ||
128 | |||
129 | use super::*; | ||
130 | |||
131 | fn do_check(before: &str, afters: &[&str]) { | ||
132 | let (cursor, before) = extract_offset(before); | ||
133 | let file = SourceFile::parse(&before); | ||
134 | let mut range = TextRange::offset_len(cursor, 0.into()); | ||
135 | for &after in afters { | ||
136 | range = extend_selection(file.syntax(), range).unwrap(); | ||
137 | let actual = &before[range]; | ||
138 | assert_eq!(after, actual); | ||
139 | } | ||
140 | } | ||
141 | |||
142 | #[test] | ||
143 | fn test_extend_selection_arith() { | ||
144 | do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]); | ||
145 | } | ||
146 | |||
147 | #[test] | ||
148 | fn test_extend_selection_start_of_the_lind() { | ||
149 | do_check( | ||
150 | r#" | ||
151 | impl S { | ||
152 | <|> fn foo() { | ||
153 | |||
154 | } | ||
155 | }"#, | ||
156 | &[" fn foo() {\n\n }\n"], | ||
157 | ); | ||
158 | } | ||
159 | |||
160 | #[test] | ||
161 | fn test_extend_selection_doc_comments() { | ||
162 | do_check( | ||
163 | r#" | ||
164 | struct A; | ||
165 | |||
166 | /// bla | ||
167 | /// bla | ||
168 | struct B { | ||
169 | <|> | ||
170 | } | ||
171 | "#, | ||
172 | &[ | ||
173 | "\n \n", | ||
174 | "{\n \n}", | ||
175 | "/// bla\n/// bla\nstruct B {\n \n}", | ||
176 | ], | ||
177 | ) | ||
178 | } | ||
179 | |||
180 | #[test] | ||
181 | fn test_extend_selection_comments() { | ||
182 | do_check( | ||
183 | r#" | ||
184 | fn bar(){} | ||
185 | |||
186 | // fn foo() { | ||
187 | // 1 + <|>1 | ||
188 | // } | ||
189 | |||
190 | // fn foo(){} | ||
191 | "#, | ||
192 | &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"], | ||
193 | ); | ||
194 | |||
195 | do_check( | ||
196 | r#" | ||
197 | // #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
198 | // pub enum Direction { | ||
199 | // <|> Next, | ||
200 | // Prev | ||
201 | // } | ||
202 | "#, | ||
203 | &[ | ||
204 | "// Next,", | ||
205 | "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }", | ||
206 | ], | ||
207 | ); | ||
208 | |||
209 | do_check( | ||
210 | r#" | ||
211 | /* | ||
212 | foo | ||
213 | _bar1<|>*/ | ||
214 | "#, | ||
215 | &["_bar1", "/*\nfoo\n_bar1*/"], | ||
216 | ); | ||
217 | |||
218 | do_check( | ||
219 | r#" | ||
220 | //!<|>foo_2 bar | ||
221 | "#, | ||
222 | &["foo_2", "//!foo_2 bar"], | ||
223 | ); | ||
224 | |||
225 | do_check( | ||
226 | r#" | ||
227 | /<|>/foo bar | ||
228 | "#, | ||
229 | &["//foo bar"], | ||
230 | ); | ||
231 | } | ||
232 | |||
233 | #[test] | ||
234 | fn test_extend_selection_prefer_idents() { | ||
235 | do_check( | ||
236 | r#" | ||
237 | fn main() { foo<|>+bar;} | ||
238 | "#, | ||
239 | &["foo", "foo+bar"], | ||
240 | ); | ||
241 | do_check( | ||
242 | r#" | ||
243 | fn main() { foo+<|>bar;} | ||
244 | "#, | ||
245 | &["bar", "foo+bar"], | ||
246 | ); | ||
247 | } | ||
248 | |||
249 | #[test] | ||
250 | fn test_extend_selection_prefer_lifetimes() { | ||
251 | do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]); | ||
252 | do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]); | ||
253 | } | ||
254 | |||
255 | #[test] | ||
256 | fn test_extend_selection_select_first_word() { | ||
257 | do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]); | ||
258 | do_check( | ||
259 | r#" | ||
260 | impl S { | ||
261 | fn foo() { | ||
262 | // hel<|>lo world | ||
263 | } | ||
264 | } | ||
265 | "#, | ||
266 | &["hello", "// hello world"], | ||
267 | ); | ||
268 | } | ||
269 | |||
270 | #[test] | ||
271 | fn test_extend_selection_string() { | ||
272 | do_check( | ||
273 | r#" | ||
274 | fn bar(){} | ||
275 | |||
276 | " fn f<|>oo() {" | ||
277 | "#, | ||
278 | &["foo", "\" fn foo() {\""], | ||
279 | ); | ||
280 | } | ||
281 | } | ||
diff --git a/crates/ra_ide_api_light/src/folding_ranges.rs b/crates/ra_ide_api_light/src/folding_ranges.rs new file mode 100644 index 000000000..6f3106889 --- /dev/null +++ b/crates/ra_ide_api_light/src/folding_ranges.rs | |||
@@ -0,0 +1,297 @@ | |||
1 | use rustc_hash::FxHashSet; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | ast, AstNode, Direction, SourceFile, SyntaxNode, TextRange, | ||
5 | SyntaxKind::{self, *}, | ||
6 | }; | ||
7 | |||
8 | #[derive(Debug, PartialEq, Eq)] | ||
9 | pub enum FoldKind { | ||
10 | Comment, | ||
11 | Imports, | ||
12 | Block, | ||
13 | } | ||
14 | |||
15 | #[derive(Debug)] | ||
16 | pub struct Fold { | ||
17 | pub range: TextRange, | ||
18 | pub kind: FoldKind, | ||
19 | } | ||
20 | |||
21 | pub fn folding_ranges(file: &SourceFile) -> Vec<Fold> { | ||
22 | let mut res = vec![]; | ||
23 | let mut visited_comments = FxHashSet::default(); | ||
24 | let mut visited_imports = FxHashSet::default(); | ||
25 | |||
26 | for node in file.syntax().descendants() { | ||
27 | // Fold items that span multiple lines | ||
28 | if let Some(kind) = fold_kind(node.kind()) { | ||
29 | if has_newline(node) { | ||
30 | res.push(Fold { | ||
31 | range: node.range(), | ||
32 | kind, | ||
33 | }); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | // Fold groups of comments | ||
38 | if node.kind() == COMMENT && !visited_comments.contains(&node) { | ||
39 | if let Some(range) = contiguous_range_for_comment(node, &mut visited_comments) { | ||
40 | res.push(Fold { | ||
41 | range, | ||
42 | kind: FoldKind::Comment, | ||
43 | }) | ||
44 | } | ||
45 | } | ||
46 | |||
47 | // Fold groups of imports | ||
48 | if node.kind() == USE_ITEM && !visited_imports.contains(&node) { | ||
49 | if let Some(range) = contiguous_range_for_group(node, &mut visited_imports) { | ||
50 | res.push(Fold { | ||
51 | range, | ||
52 | kind: FoldKind::Imports, | ||
53 | }) | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | |||
58 | res | ||
59 | } | ||
60 | |||
61 | fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | ||
62 | match kind { | ||
63 | COMMENT => Some(FoldKind::Comment), | ||
64 | USE_ITEM => Some(FoldKind::Imports), | ||
65 | NAMED_FIELD_DEF_LIST | FIELD_PAT_LIST | ITEM_LIST | EXTERN_ITEM_LIST | USE_TREE_LIST | ||
66 | | BLOCK | ENUM_VARIANT_LIST => Some(FoldKind::Block), | ||
67 | _ => None, | ||
68 | } | ||
69 | } | ||
70 | |||
71 | fn has_newline(node: &SyntaxNode) -> bool { | ||
72 | for descendant in node.descendants() { | ||
73 | if let Some(ws) = ast::Whitespace::cast(descendant) { | ||
74 | if ws.has_newlines() { | ||
75 | return true; | ||
76 | } | ||
77 | } else if let Some(comment) = ast::Comment::cast(descendant) { | ||
78 | if comment.has_newlines() { | ||
79 | return true; | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | false | ||
85 | } | ||
86 | |||
87 | fn contiguous_range_for_group<'a>( | ||
88 | first: &'a SyntaxNode, | ||
89 | visited: &mut FxHashSet<&'a SyntaxNode>, | ||
90 | ) -> Option<TextRange> { | ||
91 | visited.insert(first); | ||
92 | |||
93 | let mut last = first; | ||
94 | for node in first.siblings(Direction::Next) { | ||
95 | if let Some(ws) = ast::Whitespace::cast(node) { | ||
96 | // There is a blank line, which means that the group ends here | ||
97 | if ws.count_newlines_lazy().take(2).count() == 2 { | ||
98 | break; | ||
99 | } | ||
100 | |||
101 | // Ignore whitespace without blank lines | ||
102 | continue; | ||
103 | } | ||
104 | |||
105 | // Stop if we find a node that doesn't belong to the group | ||
106 | if node.kind() != first.kind() { | ||
107 | break; | ||
108 | } | ||
109 | |||
110 | visited.insert(node); | ||
111 | last = node; | ||
112 | } | ||
113 | |||
114 | if first != last { | ||
115 | Some(TextRange::from_to( | ||
116 | first.range().start(), | ||
117 | last.range().end(), | ||
118 | )) | ||
119 | } else { | ||
120 | // The group consists of only one element, therefore it cannot be folded | ||
121 | None | ||
122 | } | ||
123 | } | ||
124 | |||
125 | fn contiguous_range_for_comment<'a>( | ||
126 | first: &'a SyntaxNode, | ||
127 | visited: &mut FxHashSet<&'a SyntaxNode>, | ||
128 | ) -> Option<TextRange> { | ||
129 | visited.insert(first); | ||
130 | |||
131 | // Only fold comments of the same flavor | ||
132 | let group_flavor = ast::Comment::cast(first)?.flavor(); | ||
133 | |||
134 | let mut last = first; | ||
135 | for node in first.siblings(Direction::Next) { | ||
136 | if let Some(ws) = ast::Whitespace::cast(node) { | ||
137 | // There is a blank line, which means the group ends here | ||
138 | if ws.count_newlines_lazy().take(2).count() == 2 { | ||
139 | break; | ||
140 | } | ||
141 | |||
142 | // Ignore whitespace without blank lines | ||
143 | continue; | ||
144 | } | ||
145 | |||
146 | match ast::Comment::cast(node) { | ||
147 | Some(next_comment) if next_comment.flavor() == group_flavor => { | ||
148 | visited.insert(node); | ||
149 | last = node; | ||
150 | } | ||
151 | // The comment group ends because either: | ||
152 | // * An element of a different kind was reached | ||
153 | // * A comment of a different flavor was reached | ||
154 | _ => break, | ||
155 | } | ||
156 | } | ||
157 | |||
158 | if first != last { | ||
159 | Some(TextRange::from_to( | ||
160 | first.range().start(), | ||
161 | last.range().end(), | ||
162 | )) | ||
163 | } else { | ||
164 | // The group consists of only one element, therefore it cannot be folded | ||
165 | None | ||
166 | } | ||
167 | } | ||
168 | |||
169 | #[cfg(test)] | ||
170 | mod tests { | ||
171 | use super::*; | ||
172 | use test_utils::extract_ranges; | ||
173 | |||
174 | fn do_check(text: &str, fold_kinds: &[FoldKind]) { | ||
175 | let (ranges, text) = extract_ranges(text, "fold"); | ||
176 | let file = SourceFile::parse(&text); | ||
177 | let folds = folding_ranges(&file); | ||
178 | |||
179 | assert_eq!( | ||
180 | folds.len(), | ||
181 | ranges.len(), | ||
182 | "The amount of folds is different than the expected amount" | ||
183 | ); | ||
184 | assert_eq!( | ||
185 | folds.len(), | ||
186 | fold_kinds.len(), | ||
187 | "The amount of fold kinds is different than the expected amount" | ||
188 | ); | ||
189 | for ((fold, range), fold_kind) in folds | ||
190 | .into_iter() | ||
191 | .zip(ranges.into_iter()) | ||
192 | .zip(fold_kinds.into_iter()) | ||
193 | { | ||
194 | assert_eq!(fold.range.start(), range.start()); | ||
195 | assert_eq!(fold.range.end(), range.end()); | ||
196 | assert_eq!(&fold.kind, fold_kind); | ||
197 | } | ||
198 | } | ||
199 | |||
200 | #[test] | ||
201 | fn test_fold_comments() { | ||
202 | let text = r#" | ||
203 | <fold>// Hello | ||
204 | // this is a multiline | ||
205 | // comment | ||
206 | //</fold> | ||
207 | |||
208 | // But this is not | ||
209 | |||
210 | fn main() <fold>{ | ||
211 | <fold>// We should | ||
212 | // also | ||
213 | // fold | ||
214 | // this one.</fold> | ||
215 | <fold>//! But this one is different | ||
216 | //! because it has another flavor</fold> | ||
217 | <fold>/* As does this | ||
218 | multiline comment */</fold> | ||
219 | }</fold>"#; | ||
220 | |||
221 | let fold_kinds = &[ | ||
222 | FoldKind::Comment, | ||
223 | FoldKind::Block, | ||
224 | FoldKind::Comment, | ||
225 | FoldKind::Comment, | ||
226 | FoldKind::Comment, | ||
227 | ]; | ||
228 | do_check(text, fold_kinds); | ||
229 | } | ||
230 | |||
231 | #[test] | ||
232 | fn test_fold_imports() { | ||
233 | let text = r#" | ||
234 | <fold>use std::<fold>{ | ||
235 | str, | ||
236 | vec, | ||
237 | io as iop | ||
238 | }</fold>;</fold> | ||
239 | |||
240 | fn main() <fold>{ | ||
241 | }</fold>"#; | ||
242 | |||
243 | let folds = &[FoldKind::Imports, FoldKind::Block, FoldKind::Block]; | ||
244 | do_check(text, folds); | ||
245 | } | ||
246 | |||
247 | #[test] | ||
248 | fn test_fold_import_groups() { | ||
249 | let text = r#" | ||
250 | <fold>use std::str; | ||
251 | use std::vec; | ||
252 | use std::io as iop;</fold> | ||
253 | |||
254 | <fold>use std::mem; | ||
255 | use std::f64;</fold> | ||
256 | |||
257 | use std::collections::HashMap; | ||
258 | // Some random comment | ||
259 | use std::collections::VecDeque; | ||
260 | |||
261 | fn main() <fold>{ | ||
262 | }</fold>"#; | ||
263 | |||
264 | let folds = &[FoldKind::Imports, FoldKind::Imports, FoldKind::Block]; | ||
265 | do_check(text, folds); | ||
266 | } | ||
267 | |||
268 | #[test] | ||
269 | fn test_fold_import_and_groups() { | ||
270 | let text = r#" | ||
271 | <fold>use std::str; | ||
272 | use std::vec; | ||
273 | use std::io as iop;</fold> | ||
274 | |||
275 | <fold>use std::mem; | ||
276 | use std::f64;</fold> | ||
277 | |||
278 | <fold>use std::collections::<fold>{ | ||
279 | HashMap, | ||
280 | VecDeque, | ||
281 | }</fold>;</fold> | ||
282 | // Some random comment | ||
283 | |||
284 | fn main() <fold>{ | ||
285 | }</fold>"#; | ||
286 | |||
287 | let folds = &[ | ||
288 | FoldKind::Imports, | ||
289 | FoldKind::Imports, | ||
290 | FoldKind::Imports, | ||
291 | FoldKind::Block, | ||
292 | FoldKind::Block, | ||
293 | ]; | ||
294 | do_check(text, folds); | ||
295 | } | ||
296 | |||
297 | } | ||
diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs new file mode 100644 index 000000000..5a6af19b7 --- /dev/null +++ b/crates/ra_ide_api_light/src/lib.rs | |||
@@ -0,0 +1,168 @@ | |||
1 | pub mod assists; | ||
2 | mod extend_selection; | ||
3 | mod folding_ranges; | ||
4 | mod line_index; | ||
5 | mod line_index_utils; | ||
6 | mod structure; | ||
7 | #[cfg(test)] | ||
8 | mod test_utils; | ||
9 | mod typing; | ||
10 | mod diagnostics; | ||
11 | |||
12 | pub use self::{ | ||
13 | assists::LocalEdit, | ||
14 | extend_selection::extend_selection, | ||
15 | folding_ranges::{folding_ranges, Fold, FoldKind}, | ||
16 | line_index::{LineCol, LineIndex}, | ||
17 | line_index_utils::translate_offset_with_edit, | ||
18 | structure::{file_structure, StructureNode}, | ||
19 | typing::{join_lines, on_enter, on_dot_typed, on_eq_typed}, | ||
20 | diagnostics::diagnostics | ||
21 | }; | ||
22 | use ra_text_edit::TextEditBuilder; | ||
23 | use ra_syntax::{ | ||
24 | SourceFile, SyntaxNode, TextRange, TextUnit, Direction, | ||
25 | SyntaxKind::{self, *}, | ||
26 | ast::{self, AstNode}, | ||
27 | algo::find_leaf_at_offset, | ||
28 | }; | ||
29 | use rustc_hash::FxHashSet; | ||
30 | |||
31 | #[derive(Debug)] | ||
32 | pub struct HighlightedRange { | ||
33 | pub range: TextRange, | ||
34 | pub tag: &'static str, | ||
35 | } | ||
36 | |||
37 | #[derive(Debug, Copy, Clone)] | ||
38 | pub enum Severity { | ||
39 | Error, | ||
40 | WeakWarning, | ||
41 | } | ||
42 | |||
43 | #[derive(Debug)] | ||
44 | pub struct Diagnostic { | ||
45 | pub range: TextRange, | ||
46 | pub msg: String, | ||
47 | pub severity: Severity, | ||
48 | pub fix: Option<LocalEdit>, | ||
49 | } | ||
50 | |||
51 | pub fn matching_brace(file: &SourceFile, offset: TextUnit) -> Option<TextUnit> { | ||
52 | const BRACES: &[SyntaxKind] = &[ | ||
53 | L_CURLY, R_CURLY, L_BRACK, R_BRACK, L_PAREN, R_PAREN, L_ANGLE, R_ANGLE, | ||
54 | ]; | ||
55 | let (brace_node, brace_idx) = find_leaf_at_offset(file.syntax(), offset) | ||
56 | .filter_map(|node| { | ||
57 | let idx = BRACES.iter().position(|&brace| brace == node.kind())?; | ||
58 | Some((node, idx)) | ||
59 | }) | ||
60 | .next()?; | ||
61 | let parent = brace_node.parent()?; | ||
62 | let matching_kind = BRACES[brace_idx ^ 1]; | ||
63 | let matching_node = parent | ||
64 | .children() | ||
65 | .find(|node| node.kind() == matching_kind)?; | ||
66 | Some(matching_node.range().start()) | ||
67 | } | ||
68 | |||
69 | pub fn highlight(root: &SyntaxNode) -> Vec<HighlightedRange> { | ||
70 | // Visited nodes to handle highlighting priorities | ||
71 | let mut highlighted = FxHashSet::default(); | ||
72 | let mut res = Vec::new(); | ||
73 | for node in root.descendants() { | ||
74 | if highlighted.contains(&node) { | ||
75 | continue; | ||
76 | } | ||
77 | let tag = match node.kind() { | ||
78 | COMMENT => "comment", | ||
79 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", | ||
80 | ATTR => "attribute", | ||
81 | NAME_REF => "text", | ||
82 | NAME => "function", | ||
83 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", | ||
84 | LIFETIME => "parameter", | ||
85 | k if k.is_keyword() => "keyword", | ||
86 | _ => { | ||
87 | if let Some(macro_call) = ast::MacroCall::cast(node) { | ||
88 | if let Some(path) = macro_call.path() { | ||
89 | if let Some(segment) = path.segment() { | ||
90 | if let Some(name_ref) = segment.name_ref() { | ||
91 | highlighted.insert(name_ref.syntax()); | ||
92 | let range_start = name_ref.syntax().range().start(); | ||
93 | let mut range_end = name_ref.syntax().range().end(); | ||
94 | for sibling in path.syntax().siblings(Direction::Next) { | ||
95 | match sibling.kind() { | ||
96 | EXCL | IDENT => range_end = sibling.range().end(), | ||
97 | _ => (), | ||
98 | } | ||
99 | } | ||
100 | res.push(HighlightedRange { | ||
101 | range: TextRange::from_to(range_start, range_end), | ||
102 | tag: "macro", | ||
103 | }) | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | } | ||
108 | continue; | ||
109 | } | ||
110 | }; | ||
111 | res.push(HighlightedRange { | ||
112 | range: node.range(), | ||
113 | tag, | ||
114 | }) | ||
115 | } | ||
116 | res | ||
117 | } | ||
118 | |||
119 | pub fn syntax_tree(file: &SourceFile) -> String { | ||
120 | ::ra_syntax::utils::dump_tree(file.syntax()) | ||
121 | } | ||
122 | |||
123 | #[cfg(test)] | ||
124 | mod tests { | ||
125 | use ra_syntax::AstNode; | ||
126 | |||
127 | use crate::test_utils::{add_cursor, assert_eq_dbg, assert_eq_text, extract_offset}; | ||
128 | |||
129 | use super::*; | ||
130 | |||
131 | #[test] | ||
132 | fn test_highlighting() { | ||
133 | let file = SourceFile::parse( | ||
134 | r#" | ||
135 | // comment | ||
136 | fn main() {} | ||
137 | println!("Hello, {}!", 92); | ||
138 | "#, | ||
139 | ); | ||
140 | let hls = highlight(file.syntax()); | ||
141 | assert_eq_dbg( | ||
142 | r#"[HighlightedRange { range: [1; 11), tag: "comment" }, | ||
143 | HighlightedRange { range: [12; 14), tag: "keyword" }, | ||
144 | HighlightedRange { range: [15; 19), tag: "function" }, | ||
145 | HighlightedRange { range: [29; 37), tag: "macro" }, | ||
146 | HighlightedRange { range: [38; 50), tag: "string" }, | ||
147 | HighlightedRange { range: [52; 54), tag: "literal" }]"#, | ||
148 | &hls, | ||
149 | ); | ||
150 | } | ||
151 | |||
152 | #[test] | ||
153 | fn test_matching_brace() { | ||
154 | fn do_check(before: &str, after: &str) { | ||
155 | let (pos, before) = extract_offset(before); | ||
156 | let file = SourceFile::parse(&before); | ||
157 | let new_pos = match matching_brace(&file, pos) { | ||
158 | None => pos, | ||
159 | Some(pos) => pos, | ||
160 | }; | ||
161 | let actual = add_cursor(&before, new_pos); | ||
162 | assert_eq_text!(after, &actual); | ||
163 | } | ||
164 | |||
165 | do_check("struct Foo { a: i32, }<|>", "struct Foo <|>{ a: i32, }"); | ||
166 | } | ||
167 | |||
168 | } | ||
diff --git a/crates/ra_ide_api_light/src/line_index.rs b/crates/ra_ide_api_light/src/line_index.rs new file mode 100644 index 000000000..898fee7e0 --- /dev/null +++ b/crates/ra_ide_api_light/src/line_index.rs | |||
@@ -0,0 +1,399 @@ | |||
1 | use crate::TextUnit; | ||
2 | use rustc_hash::FxHashMap; | ||
3 | use superslice::Ext; | ||
4 | |||
5 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
6 | pub struct LineIndex { | ||
7 | pub(crate) newlines: Vec<TextUnit>, | ||
8 | pub(crate) utf16_lines: FxHashMap<u32, Vec<Utf16Char>>, | ||
9 | } | ||
10 | |||
11 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
12 | pub struct LineCol { | ||
13 | pub line: u32, | ||
14 | pub col_utf16: u32, | ||
15 | } | ||
16 | |||
17 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] | ||
18 | pub(crate) struct Utf16Char { | ||
19 | pub(crate) start: TextUnit, | ||
20 | pub(crate) end: TextUnit, | ||
21 | } | ||
22 | |||
23 | impl Utf16Char { | ||
24 | fn len(&self) -> TextUnit { | ||
25 | self.end - self.start | ||
26 | } | ||
27 | } | ||
28 | |||
29 | impl LineIndex { | ||
30 | pub fn new(text: &str) -> LineIndex { | ||
31 | let mut utf16_lines = FxHashMap::default(); | ||
32 | let mut utf16_chars = Vec::new(); | ||
33 | |||
34 | let mut newlines = vec![0.into()]; | ||
35 | let mut curr_row = 0.into(); | ||
36 | let mut curr_col = 0.into(); | ||
37 | let mut line = 0; | ||
38 | for c in text.chars() { | ||
39 | curr_row += TextUnit::of_char(c); | ||
40 | if c == '\n' { | ||
41 | newlines.push(curr_row); | ||
42 | |||
43 | // Save any utf-16 characters seen in the previous line | ||
44 | if utf16_chars.len() > 0 { | ||
45 | utf16_lines.insert(line, utf16_chars); | ||
46 | utf16_chars = Vec::new(); | ||
47 | } | ||
48 | |||
49 | // Prepare for processing the next line | ||
50 | curr_col = 0.into(); | ||
51 | line += 1; | ||
52 | continue; | ||
53 | } | ||
54 | |||
55 | let char_len = TextUnit::of_char(c); | ||
56 | if char_len.to_usize() > 1 { | ||
57 | utf16_chars.push(Utf16Char { | ||
58 | start: curr_col, | ||
59 | end: curr_col + char_len, | ||
60 | }); | ||
61 | } | ||
62 | |||
63 | curr_col += char_len; | ||
64 | } | ||
65 | |||
66 | // Save any utf-16 characters seen in the last line | ||
67 | if utf16_chars.len() > 0 { | ||
68 | utf16_lines.insert(line, utf16_chars); | ||
69 | } | ||
70 | |||
71 | LineIndex { | ||
72 | newlines, | ||
73 | utf16_lines, | ||
74 | } | ||
75 | } | ||
76 | |||
77 | pub fn line_col(&self, offset: TextUnit) -> LineCol { | ||
78 | let line = self.newlines.upper_bound(&offset) - 1; | ||
79 | let line_start_offset = self.newlines[line]; | ||
80 | let col = offset - line_start_offset; | ||
81 | |||
82 | LineCol { | ||
83 | line: line as u32, | ||
84 | col_utf16: self.utf8_to_utf16_col(line as u32, col) as u32, | ||
85 | } | ||
86 | } | ||
87 | |||
88 | pub fn offset(&self, line_col: LineCol) -> TextUnit { | ||
89 | //TODO: return Result | ||
90 | let col = self.utf16_to_utf8_col(line_col.line, line_col.col_utf16); | ||
91 | self.newlines[line_col.line as usize] + col | ||
92 | } | ||
93 | |||
94 | fn utf8_to_utf16_col(&self, line: u32, mut col: TextUnit) -> usize { | ||
95 | if let Some(utf16_chars) = self.utf16_lines.get(&line) { | ||
96 | let mut correction = TextUnit::from_usize(0); | ||
97 | for c in utf16_chars { | ||
98 | if col >= c.end { | ||
99 | correction += c.len() - TextUnit::from_usize(1); | ||
100 | } else { | ||
101 | // From here on, all utf16 characters come *after* the character we are mapping, | ||
102 | // so we don't need to take them into account | ||
103 | break; | ||
104 | } | ||
105 | } | ||
106 | |||
107 | col -= correction; | ||
108 | } | ||
109 | |||
110 | col.to_usize() | ||
111 | } | ||
112 | |||
113 | fn utf16_to_utf8_col(&self, line: u32, col: u32) -> TextUnit { | ||
114 | let mut col: TextUnit = col.into(); | ||
115 | if let Some(utf16_chars) = self.utf16_lines.get(&line) { | ||
116 | for c in utf16_chars { | ||
117 | if col >= c.start { | ||
118 | col += c.len() - TextUnit::from_usize(1); | ||
119 | } else { | ||
120 | // From here on, all utf16 characters come *after* the character we are mapping, | ||
121 | // so we don't need to take them into account | ||
122 | break; | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | |||
127 | col | ||
128 | } | ||
129 | } | ||
130 | |||
131 | #[cfg(test)] | ||
132 | /// Simple reference implementation to use in proptests | ||
133 | pub fn to_line_col(text: &str, offset: TextUnit) -> LineCol { | ||
134 | let mut res = LineCol { | ||
135 | line: 0, | ||
136 | col_utf16: 0, | ||
137 | }; | ||
138 | for (i, c) in text.char_indices() { | ||
139 | if i + c.len_utf8() > offset.to_usize() { | ||
140 | // if it's an invalid offset, inside a multibyte char | ||
141 | // return as if it was at the start of the char | ||
142 | break; | ||
143 | } | ||
144 | if c == '\n' { | ||
145 | res.line += 1; | ||
146 | res.col_utf16 = 0; | ||
147 | } else { | ||
148 | res.col_utf16 += 1; | ||
149 | } | ||
150 | } | ||
151 | res | ||
152 | } | ||
153 | |||
154 | #[cfg(test)] | ||
155 | mod test_line_index { | ||
156 | use super::*; | ||
157 | use proptest::{prelude::*, proptest, proptest_helper}; | ||
158 | use ra_text_edit::test_utils::{arb_text, arb_offset}; | ||
159 | |||
160 | #[test] | ||
161 | fn test_line_index() { | ||
162 | let text = "hello\nworld"; | ||
163 | let index = LineIndex::new(text); | ||
164 | assert_eq!( | ||
165 | index.line_col(0.into()), | ||
166 | LineCol { | ||
167 | line: 0, | ||
168 | col_utf16: 0 | ||
169 | } | ||
170 | ); | ||
171 | assert_eq!( | ||
172 | index.line_col(1.into()), | ||
173 | LineCol { | ||
174 | line: 0, | ||
175 | col_utf16: 1 | ||
176 | } | ||
177 | ); | ||
178 | assert_eq!( | ||
179 | index.line_col(5.into()), | ||
180 | LineCol { | ||
181 | line: 0, | ||
182 | col_utf16: 5 | ||
183 | } | ||
184 | ); | ||
185 | assert_eq!( | ||
186 | index.line_col(6.into()), | ||
187 | LineCol { | ||
188 | line: 1, | ||
189 | col_utf16: 0 | ||
190 | } | ||
191 | ); | ||
192 | assert_eq!( | ||
193 | index.line_col(7.into()), | ||
194 | LineCol { | ||
195 | line: 1, | ||
196 | col_utf16: 1 | ||
197 | } | ||
198 | ); | ||
199 | assert_eq!( | ||
200 | index.line_col(8.into()), | ||
201 | LineCol { | ||
202 | line: 1, | ||
203 | col_utf16: 2 | ||
204 | } | ||
205 | ); | ||
206 | assert_eq!( | ||
207 | index.line_col(10.into()), | ||
208 | LineCol { | ||
209 | line: 1, | ||
210 | col_utf16: 4 | ||
211 | } | ||
212 | ); | ||
213 | assert_eq!( | ||
214 | index.line_col(11.into()), | ||
215 | LineCol { | ||
216 | line: 1, | ||
217 | col_utf16: 5 | ||
218 | } | ||
219 | ); | ||
220 | assert_eq!( | ||
221 | index.line_col(12.into()), | ||
222 | LineCol { | ||
223 | line: 1, | ||
224 | col_utf16: 6 | ||
225 | } | ||
226 | ); | ||
227 | |||
228 | let text = "\nhello\nworld"; | ||
229 | let index = LineIndex::new(text); | ||
230 | assert_eq!( | ||
231 | index.line_col(0.into()), | ||
232 | LineCol { | ||
233 | line: 0, | ||
234 | col_utf16: 0 | ||
235 | } | ||
236 | ); | ||
237 | assert_eq!( | ||
238 | index.line_col(1.into()), | ||
239 | LineCol { | ||
240 | line: 1, | ||
241 | col_utf16: 0 | ||
242 | } | ||
243 | ); | ||
244 | assert_eq!( | ||
245 | index.line_col(2.into()), | ||
246 | LineCol { | ||
247 | line: 1, | ||
248 | col_utf16: 1 | ||
249 | } | ||
250 | ); | ||
251 | assert_eq!( | ||
252 | index.line_col(6.into()), | ||
253 | LineCol { | ||
254 | line: 1, | ||
255 | col_utf16: 5 | ||
256 | } | ||
257 | ); | ||
258 | assert_eq!( | ||
259 | index.line_col(7.into()), | ||
260 | LineCol { | ||
261 | line: 2, | ||
262 | col_utf16: 0 | ||
263 | } | ||
264 | ); | ||
265 | } | ||
266 | |||
267 | fn arb_text_with_offset() -> BoxedStrategy<(TextUnit, String)> { | ||
268 | arb_text() | ||
269 | .prop_flat_map(|text| (arb_offset(&text), Just(text))) | ||
270 | .boxed() | ||
271 | } | ||
272 | |||
273 | fn to_line_col(text: &str, offset: TextUnit) -> LineCol { | ||
274 | let mut res = LineCol { | ||
275 | line: 0, | ||
276 | col_utf16: 0, | ||
277 | }; | ||
278 | for (i, c) in text.char_indices() { | ||
279 | if i + c.len_utf8() > offset.to_usize() { | ||
280 | // if it's an invalid offset, inside a multibyte char | ||
281 | // return as if it was at the start of the char | ||
282 | break; | ||
283 | } | ||
284 | if c == '\n' { | ||
285 | res.line += 1; | ||
286 | res.col_utf16 = 0; | ||
287 | } else { | ||
288 | res.col_utf16 += 1; | ||
289 | } | ||
290 | } | ||
291 | res | ||
292 | } | ||
293 | |||
294 | proptest! { | ||
295 | #[test] | ||
296 | fn test_line_index_proptest((offset, text) in arb_text_with_offset()) { | ||
297 | let expected = to_line_col(&text, offset); | ||
298 | let line_index = LineIndex::new(&text); | ||
299 | let actual = line_index.line_col(offset); | ||
300 | |||
301 | assert_eq!(actual, expected); | ||
302 | } | ||
303 | } | ||
304 | } | ||
305 | |||
306 | #[cfg(test)] | ||
307 | mod test_utf8_utf16_conv { | ||
308 | use super::*; | ||
309 | |||
310 | #[test] | ||
311 | fn test_char_len() { | ||
312 | assert_eq!('メ'.len_utf8(), 3); | ||
313 | assert_eq!('メ'.len_utf16(), 1); | ||
314 | } | ||
315 | |||
316 | #[test] | ||
317 | fn test_empty_index() { | ||
318 | let col_index = LineIndex::new( | ||
319 | " | ||
320 | const C: char = 'x'; | ||
321 | ", | ||
322 | ); | ||
323 | assert_eq!(col_index.utf16_lines.len(), 0); | ||
324 | } | ||
325 | |||
326 | #[test] | ||
327 | fn test_single_char() { | ||
328 | let col_index = LineIndex::new( | ||
329 | " | ||
330 | const C: char = 'メ'; | ||
331 | ", | ||
332 | ); | ||
333 | |||
334 | assert_eq!(col_index.utf16_lines.len(), 1); | ||
335 | assert_eq!(col_index.utf16_lines[&1].len(), 1); | ||
336 | assert_eq!( | ||
337 | col_index.utf16_lines[&1][0], | ||
338 | Utf16Char { | ||
339 | start: 17.into(), | ||
340 | end: 20.into() | ||
341 | } | ||
342 | ); | ||
343 | |||
344 | // UTF-8 to UTF-16, no changes | ||
345 | assert_eq!(col_index.utf8_to_utf16_col(1, 15.into()), 15); | ||
346 | |||
347 | // UTF-8 to UTF-16 | ||
348 | assert_eq!(col_index.utf8_to_utf16_col(1, 22.into()), 20); | ||
349 | |||
350 | // UTF-16 to UTF-8, no changes | ||
351 | assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextUnit::from(15)); | ||
352 | |||
353 | // UTF-16 to UTF-8 | ||
354 | assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextUnit::from(21)); | ||
355 | } | ||
356 | |||
357 | #[test] | ||
358 | fn test_string() { | ||
359 | let col_index = LineIndex::new( | ||
360 | " | ||
361 | const C: char = \"メ メ\"; | ||
362 | ", | ||
363 | ); | ||
364 | |||
365 | assert_eq!(col_index.utf16_lines.len(), 1); | ||
366 | assert_eq!(col_index.utf16_lines[&1].len(), 2); | ||
367 | assert_eq!( | ||
368 | col_index.utf16_lines[&1][0], | ||
369 | Utf16Char { | ||
370 | start: 17.into(), | ||
371 | end: 20.into() | ||
372 | } | ||
373 | ); | ||
374 | assert_eq!( | ||
375 | col_index.utf16_lines[&1][1], | ||
376 | Utf16Char { | ||
377 | start: 21.into(), | ||
378 | end: 24.into() | ||
379 | } | ||
380 | ); | ||
381 | |||
382 | // UTF-8 to UTF-16 | ||
383 | assert_eq!(col_index.utf8_to_utf16_col(1, 15.into()), 15); | ||
384 | |||
385 | assert_eq!(col_index.utf8_to_utf16_col(1, 21.into()), 19); | ||
386 | assert_eq!(col_index.utf8_to_utf16_col(1, 25.into()), 21); | ||
387 | |||
388 | assert!(col_index.utf8_to_utf16_col(2, 15.into()) == 15); | ||
389 | |||
390 | // UTF-16 to UTF-8 | ||
391 | assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextUnit::from_usize(15)); | ||
392 | |||
393 | assert_eq!(col_index.utf16_to_utf8_col(1, 18), TextUnit::from_usize(20)); | ||
394 | assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextUnit::from_usize(23)); | ||
395 | |||
396 | assert_eq!(col_index.utf16_to_utf8_col(2, 15), TextUnit::from_usize(15)); | ||
397 | } | ||
398 | |||
399 | } | ||
diff --git a/crates/ra_ide_api_light/src/line_index_utils.rs b/crates/ra_ide_api_light/src/line_index_utils.rs new file mode 100644 index 000000000..ec3269bbb --- /dev/null +++ b/crates/ra_ide_api_light/src/line_index_utils.rs | |||
@@ -0,0 +1,363 @@ | |||
1 | use ra_text_edit::{AtomTextEdit, TextEdit}; | ||
2 | use ra_syntax::{TextUnit, TextRange}; | ||
3 | use crate::{LineIndex, LineCol, line_index::Utf16Char}; | ||
4 | |||
5 | #[derive(Debug, Clone)] | ||
6 | enum Step { | ||
7 | Newline(TextUnit), | ||
8 | Utf16Char(TextRange), | ||
9 | } | ||
10 | |||
11 | #[derive(Debug)] | ||
12 | struct LineIndexStepIter<'a> { | ||
13 | line_index: &'a LineIndex, | ||
14 | next_newline_idx: usize, | ||
15 | utf16_chars: Option<(TextUnit, std::slice::Iter<'a, Utf16Char>)>, | ||
16 | } | ||
17 | |||
18 | impl<'a> LineIndexStepIter<'a> { | ||
19 | fn from(line_index: &LineIndex) -> LineIndexStepIter { | ||
20 | let mut x = LineIndexStepIter { | ||
21 | line_index, | ||
22 | next_newline_idx: 0, | ||
23 | utf16_chars: None, | ||
24 | }; | ||
25 | // skip first newline since it's not real | ||
26 | x.next(); | ||
27 | x | ||
28 | } | ||
29 | } | ||
30 | |||
31 | impl<'a> Iterator for LineIndexStepIter<'a> { | ||
32 | type Item = Step; | ||
33 | fn next(&mut self) -> Option<Step> { | ||
34 | self.utf16_chars | ||
35 | .as_mut() | ||
36 | .and_then(|(newline, x)| { | ||
37 | let x = x.next()?; | ||
38 | Some(Step::Utf16Char(TextRange::from_to( | ||
39 | *newline + x.start, | ||
40 | *newline + x.end, | ||
41 | ))) | ||
42 | }) | ||
43 | .or_else(|| { | ||
44 | let next_newline = *self.line_index.newlines.get(self.next_newline_idx)?; | ||
45 | self.utf16_chars = self | ||
46 | .line_index | ||
47 | .utf16_lines | ||
48 | .get(&(self.next_newline_idx as u32)) | ||
49 | .map(|x| (next_newline, x.iter())); | ||
50 | self.next_newline_idx += 1; | ||
51 | Some(Step::Newline(next_newline)) | ||
52 | }) | ||
53 | } | ||
54 | } | ||
55 | |||
56 | #[derive(Debug)] | ||
57 | struct OffsetStepIter<'a> { | ||
58 | text: &'a str, | ||
59 | offset: TextUnit, | ||
60 | } | ||
61 | |||
62 | impl<'a> Iterator for OffsetStepIter<'a> { | ||
63 | type Item = Step; | ||
64 | fn next(&mut self) -> Option<Step> { | ||
65 | let (next, next_offset) = self | ||
66 | .text | ||
67 | .char_indices() | ||
68 | .filter_map(|(i, c)| { | ||
69 | if c == '\n' { | ||
70 | let next_offset = self.offset + TextUnit::from_usize(i + 1); | ||
71 | let next = Step::Newline(next_offset); | ||
72 | Some((next, next_offset)) | ||
73 | } else { | ||
74 | let char_len = TextUnit::of_char(c); | ||
75 | if char_len.to_usize() > 1 { | ||
76 | let start = self.offset + TextUnit::from_usize(i); | ||
77 | let end = start + char_len; | ||
78 | let next = Step::Utf16Char(TextRange::from_to(start, end)); | ||
79 | let next_offset = end; | ||
80 | Some((next, next_offset)) | ||
81 | } else { | ||
82 | None | ||
83 | } | ||
84 | } | ||
85 | }) | ||
86 | .next()?; | ||
87 | let next_idx = (next_offset - self.offset).to_usize(); | ||
88 | self.text = &self.text[next_idx..]; | ||
89 | self.offset = next_offset; | ||
90 | Some(next) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | #[derive(Debug)] | ||
95 | enum NextSteps<'a> { | ||
96 | Use, | ||
97 | ReplaceMany(OffsetStepIter<'a>), | ||
98 | AddMany(OffsetStepIter<'a>), | ||
99 | } | ||
100 | |||
101 | #[derive(Debug)] | ||
102 | struct TranslatedEdit<'a> { | ||
103 | delete: TextRange, | ||
104 | insert: &'a str, | ||
105 | diff: i64, | ||
106 | } | ||
107 | |||
108 | struct Edits<'a> { | ||
109 | edits: &'a [AtomTextEdit], | ||
110 | current: Option<TranslatedEdit<'a>>, | ||
111 | acc_diff: i64, | ||
112 | } | ||
113 | |||
114 | impl<'a> Edits<'a> { | ||
115 | fn from_text_edit(text_edit: &'a TextEdit) -> Edits<'a> { | ||
116 | let mut x = Edits { | ||
117 | edits: text_edit.as_atoms(), | ||
118 | current: None, | ||
119 | acc_diff: 0, | ||
120 | }; | ||
121 | x.advance_edit(); | ||
122 | x | ||
123 | } | ||
124 | fn advance_edit(&mut self) { | ||
125 | self.acc_diff += self.current.as_ref().map_or(0, |x| x.diff); | ||
126 | match self.edits.split_first() { | ||
127 | Some((next, rest)) => { | ||
128 | let delete = self.translate_range(next.delete); | ||
129 | let diff = next.insert.len() as i64 - next.delete.len().to_usize() as i64; | ||
130 | self.current = Some(TranslatedEdit { | ||
131 | delete, | ||
132 | insert: &next.insert, | ||
133 | diff, | ||
134 | }); | ||
135 | self.edits = rest; | ||
136 | } | ||
137 | None => { | ||
138 | self.current = None; | ||
139 | } | ||
140 | } | ||
141 | } | ||
142 | |||
143 | fn next_inserted_steps(&mut self) -> Option<OffsetStepIter<'a>> { | ||
144 | let cur = self.current.as_ref()?; | ||
145 | let res = Some(OffsetStepIter { | ||
146 | offset: cur.delete.start(), | ||
147 | text: &cur.insert, | ||
148 | }); | ||
149 | self.advance_edit(); | ||
150 | res | ||
151 | } | ||
152 | |||
153 | fn next_steps(&mut self, step: &Step) -> NextSteps { | ||
154 | let step_pos = match step { | ||
155 | &Step::Newline(n) => n, | ||
156 | &Step::Utf16Char(r) => r.end(), | ||
157 | }; | ||
158 | let res = match &mut self.current { | ||
159 | Some(edit) => { | ||
160 | if step_pos <= edit.delete.start() { | ||
161 | NextSteps::Use | ||
162 | } else if step_pos <= edit.delete.end() { | ||
163 | let iter = OffsetStepIter { | ||
164 | offset: edit.delete.start(), | ||
165 | text: &edit.insert, | ||
166 | }; | ||
167 | // empty slice to avoid returning steps again | ||
168 | edit.insert = &edit.insert[edit.insert.len()..]; | ||
169 | NextSteps::ReplaceMany(iter) | ||
170 | } else { | ||
171 | let iter = OffsetStepIter { | ||
172 | offset: edit.delete.start(), | ||
173 | text: &edit.insert, | ||
174 | }; | ||
175 | // empty slice to avoid returning steps again | ||
176 | edit.insert = &edit.insert[edit.insert.len()..]; | ||
177 | self.advance_edit(); | ||
178 | NextSteps::AddMany(iter) | ||
179 | } | ||
180 | } | ||
181 | None => NextSteps::Use, | ||
182 | }; | ||
183 | res | ||
184 | } | ||
185 | |||
186 | fn translate_range(&self, range: TextRange) -> TextRange { | ||
187 | if self.acc_diff == 0 { | ||
188 | range | ||
189 | } else { | ||
190 | let start = self.translate(range.start()); | ||
191 | let end = self.translate(range.end()); | ||
192 | TextRange::from_to(start, end) | ||
193 | } | ||
194 | } | ||
195 | |||
196 | fn translate(&self, x: TextUnit) -> TextUnit { | ||
197 | if self.acc_diff == 0 { | ||
198 | x | ||
199 | } else { | ||
200 | TextUnit::from((x.to_usize() as i64 + self.acc_diff) as u32) | ||
201 | } | ||
202 | } | ||
203 | |||
204 | fn translate_step(&self, x: &Step) -> Step { | ||
205 | if self.acc_diff == 0 { | ||
206 | x.clone() | ||
207 | } else { | ||
208 | match x { | ||
209 | &Step::Newline(n) => Step::Newline(self.translate(n)), | ||
210 | &Step::Utf16Char(r) => Step::Utf16Char(self.translate_range(r)), | ||
211 | } | ||
212 | } | ||
213 | } | ||
214 | } | ||
215 | |||
216 | #[derive(Debug)] | ||
217 | struct RunningLineCol { | ||
218 | line: u32, | ||
219 | last_newline: TextUnit, | ||
220 | col_adjust: TextUnit, | ||
221 | } | ||
222 | |||
223 | impl RunningLineCol { | ||
224 | fn new() -> RunningLineCol { | ||
225 | RunningLineCol { | ||
226 | line: 0, | ||
227 | last_newline: TextUnit::from(0), | ||
228 | col_adjust: TextUnit::from(0), | ||
229 | } | ||
230 | } | ||
231 | |||
232 | fn to_line_col(&self, offset: TextUnit) -> LineCol { | ||
233 | LineCol { | ||
234 | line: self.line, | ||
235 | col_utf16: ((offset - self.last_newline) - self.col_adjust).into(), | ||
236 | } | ||
237 | } | ||
238 | |||
239 | fn add_line(&mut self, newline: TextUnit) { | ||
240 | self.line += 1; | ||
241 | self.last_newline = newline; | ||
242 | self.col_adjust = TextUnit::from(0); | ||
243 | } | ||
244 | |||
245 | fn adjust_col(&mut self, range: &TextRange) { | ||
246 | self.col_adjust += range.len() - TextUnit::from(1); | ||
247 | } | ||
248 | } | ||
249 | |||
250 | pub fn translate_offset_with_edit( | ||
251 | line_index: &LineIndex, | ||
252 | offset: TextUnit, | ||
253 | text_edit: &TextEdit, | ||
254 | ) -> LineCol { | ||
255 | let mut state = Edits::from_text_edit(&text_edit); | ||
256 | |||
257 | let mut res = RunningLineCol::new(); | ||
258 | |||
259 | macro_rules! test_step { | ||
260 | ($x:ident) => { | ||
261 | match &$x { | ||
262 | Step::Newline(n) => { | ||
263 | if offset < *n { | ||
264 | return res.to_line_col(offset); | ||
265 | } else { | ||
266 | res.add_line(*n); | ||
267 | } | ||
268 | } | ||
269 | Step::Utf16Char(x) => { | ||
270 | if offset < x.end() { | ||
271 | // if the offset is inside a multibyte char it's invalid | ||
272 | // clamp it to the start of the char | ||
273 | let clamp = offset.min(x.start()); | ||
274 | return res.to_line_col(clamp); | ||
275 | } else { | ||
276 | res.adjust_col(x); | ||
277 | } | ||
278 | } | ||
279 | } | ||
280 | }; | ||
281 | } | ||
282 | |||
283 | for orig_step in LineIndexStepIter::from(line_index) { | ||
284 | loop { | ||
285 | let translated_step = state.translate_step(&orig_step); | ||
286 | match state.next_steps(&translated_step) { | ||
287 | NextSteps::Use => { | ||
288 | test_step!(translated_step); | ||
289 | break; | ||
290 | } | ||
291 | NextSteps::ReplaceMany(ns) => { | ||
292 | for n in ns { | ||
293 | test_step!(n); | ||
294 | } | ||
295 | break; | ||
296 | } | ||
297 | NextSteps::AddMany(ns) => { | ||
298 | for n in ns { | ||
299 | test_step!(n); | ||
300 | } | ||
301 | } | ||
302 | } | ||
303 | } | ||
304 | } | ||
305 | |||
306 | loop { | ||
307 | match state.next_inserted_steps() { | ||
308 | None => break, | ||
309 | Some(ns) => { | ||
310 | for n in ns { | ||
311 | test_step!(n); | ||
312 | } | ||
313 | } | ||
314 | } | ||
315 | } | ||
316 | |||
317 | res.to_line_col(offset) | ||
318 | } | ||
319 | |||
320 | #[cfg(test)] | ||
321 | mod test { | ||
322 | use super::*; | ||
323 | use proptest::{prelude::*, proptest, proptest_helper}; | ||
324 | use crate::line_index; | ||
325 | use ra_text_edit::test_utils::{arb_offset, arb_text_with_edit}; | ||
326 | use ra_text_edit::TextEdit; | ||
327 | |||
328 | #[derive(Debug)] | ||
329 | struct ArbTextWithEditAndOffset { | ||
330 | text: String, | ||
331 | edit: TextEdit, | ||
332 | edited_text: String, | ||
333 | offset: TextUnit, | ||
334 | } | ||
335 | |||
336 | fn arb_text_with_edit_and_offset() -> BoxedStrategy<ArbTextWithEditAndOffset> { | ||
337 | arb_text_with_edit() | ||
338 | .prop_flat_map(|x| { | ||
339 | let edited_text = x.edit.apply(&x.text); | ||
340 | let arb_offset = arb_offset(&edited_text); | ||
341 | (Just(x), Just(edited_text), arb_offset).prop_map(|(x, edited_text, offset)| { | ||
342 | ArbTextWithEditAndOffset { | ||
343 | text: x.text, | ||
344 | edit: x.edit, | ||
345 | edited_text, | ||
346 | offset, | ||
347 | } | ||
348 | }) | ||
349 | }) | ||
350 | .boxed() | ||
351 | } | ||
352 | |||
353 | proptest! { | ||
354 | #[test] | ||
355 | fn test_translate_offset_with_edit(x in arb_text_with_edit_and_offset()) { | ||
356 | let expected = line_index::to_line_col(&x.edited_text, x.offset); | ||
357 | let line_index = LineIndex::new(&x.text); | ||
358 | let actual = translate_offset_with_edit(&line_index, x.offset, &x.edit); | ||
359 | |||
360 | assert_eq!(actual, expected); | ||
361 | } | ||
362 | } | ||
363 | } | ||
diff --git a/crates/ra_ide_api_light/src/structure.rs b/crates/ra_ide_api_light/src/structure.rs new file mode 100644 index 000000000..8bd57555f --- /dev/null +++ b/crates/ra_ide_api_light/src/structure.rs | |||
@@ -0,0 +1,129 @@ | |||
1 | use crate::TextRange; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | algo::visit::{visitor, Visitor}, | ||
5 | ast::{self, NameOwner}, | ||
6 | AstNode, SourceFile, SyntaxKind, SyntaxNode, WalkEvent, | ||
7 | }; | ||
8 | |||
9 | #[derive(Debug, Clone)] | ||
10 | pub struct StructureNode { | ||
11 | pub parent: Option<usize>, | ||
12 | pub label: String, | ||
13 | pub navigation_range: TextRange, | ||
14 | pub node_range: TextRange, | ||
15 | pub kind: SyntaxKind, | ||
16 | } | ||
17 | |||
18 | pub fn file_structure(file: &SourceFile) -> Vec<StructureNode> { | ||
19 | let mut res = Vec::new(); | ||
20 | let mut stack = Vec::new(); | ||
21 | |||
22 | for event in file.syntax().preorder() { | ||
23 | match event { | ||
24 | WalkEvent::Enter(node) => { | ||
25 | if let Some(mut symbol) = structure_node(node) { | ||
26 | symbol.parent = stack.last().map(|&n| n); | ||
27 | stack.push(res.len()); | ||
28 | res.push(symbol); | ||
29 | } | ||
30 | } | ||
31 | WalkEvent::Leave(node) => { | ||
32 | if structure_node(node).is_some() { | ||
33 | stack.pop().unwrap(); | ||
34 | } | ||
35 | } | ||
36 | } | ||
37 | } | ||
38 | res | ||
39 | } | ||
40 | |||
41 | fn structure_node(node: &SyntaxNode) -> Option<StructureNode> { | ||
42 | fn decl<N: NameOwner>(node: &N) -> Option<StructureNode> { | ||
43 | let name = node.name()?; | ||
44 | Some(StructureNode { | ||
45 | parent: None, | ||
46 | label: name.text().to_string(), | ||
47 | navigation_range: name.syntax().range(), | ||
48 | node_range: node.syntax().range(), | ||
49 | kind: node.syntax().kind(), | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | visitor() | ||
54 | .visit(decl::<ast::FnDef>) | ||
55 | .visit(decl::<ast::StructDef>) | ||
56 | .visit(decl::<ast::NamedFieldDef>) | ||
57 | .visit(decl::<ast::EnumDef>) | ||
58 | .visit(decl::<ast::TraitDef>) | ||
59 | .visit(decl::<ast::Module>) | ||
60 | .visit(decl::<ast::TypeDef>) | ||
61 | .visit(decl::<ast::ConstDef>) | ||
62 | .visit(decl::<ast::StaticDef>) | ||
63 | .visit(|im: &ast::ImplBlock| { | ||
64 | let target_type = im.target_type()?; | ||
65 | let target_trait = im.target_trait(); | ||
66 | let label = match target_trait { | ||
67 | None => format!("impl {}", target_type.syntax().text()), | ||
68 | Some(t) => format!( | ||
69 | "impl {} for {}", | ||
70 | t.syntax().text(), | ||
71 | target_type.syntax().text(), | ||
72 | ), | ||
73 | }; | ||
74 | |||
75 | let node = StructureNode { | ||
76 | parent: None, | ||
77 | label, | ||
78 | navigation_range: target_type.syntax().range(), | ||
79 | node_range: im.syntax().range(), | ||
80 | kind: im.syntax().kind(), | ||
81 | }; | ||
82 | Some(node) | ||
83 | }) | ||
84 | .accept(node)? | ||
85 | } | ||
86 | |||
87 | #[cfg(test)] | ||
88 | mod tests { | ||
89 | use super::*; | ||
90 | use test_utils::assert_eq_dbg; | ||
91 | |||
92 | #[test] | ||
93 | fn test_file_structure() { | ||
94 | let file = SourceFile::parse( | ||
95 | r#" | ||
96 | struct Foo { | ||
97 | x: i32 | ||
98 | } | ||
99 | |||
100 | mod m { | ||
101 | fn bar() {} | ||
102 | } | ||
103 | |||
104 | enum E { X, Y(i32) } | ||
105 | type T = (); | ||
106 | static S: i32 = 92; | ||
107 | const C: i32 = 92; | ||
108 | |||
109 | impl E {} | ||
110 | |||
111 | impl fmt::Debug for E {} | ||
112 | "#, | ||
113 | ); | ||
114 | let structure = file_structure(&file); | ||
115 | assert_eq_dbg( | ||
116 | r#"[StructureNode { parent: None, label: "Foo", navigation_range: [8; 11), node_range: [1; 26), kind: STRUCT_DEF }, | ||
117 | StructureNode { parent: Some(0), label: "x", navigation_range: [18; 19), node_range: [18; 24), kind: NAMED_FIELD_DEF }, | ||
118 | StructureNode { parent: None, label: "m", navigation_range: [32; 33), node_range: [28; 53), kind: MODULE }, | ||
119 | StructureNode { parent: Some(2), label: "bar", navigation_range: [43; 46), node_range: [40; 51), kind: FN_DEF }, | ||
120 | StructureNode { parent: None, label: "E", navigation_range: [60; 61), node_range: [55; 75), kind: ENUM_DEF }, | ||
121 | StructureNode { parent: None, label: "T", navigation_range: [81; 82), node_range: [76; 88), kind: TYPE_DEF }, | ||
122 | StructureNode { parent: None, label: "S", navigation_range: [96; 97), node_range: [89; 108), kind: STATIC_DEF }, | ||
123 | StructureNode { parent: None, label: "C", navigation_range: [115; 116), node_range: [109; 127), kind: CONST_DEF }, | ||
124 | StructureNode { parent: None, label: "impl E", navigation_range: [134; 135), node_range: [129; 138), kind: IMPL_BLOCK }, | ||
125 | StructureNode { parent: None, label: "impl fmt::Debug for E", navigation_range: [160; 161), node_range: [140; 164), kind: IMPL_BLOCK }]"#, | ||
126 | &structure, | ||
127 | ) | ||
128 | } | ||
129 | } | ||
diff --git a/crates/ra_ide_api_light/src/test_utils.rs b/crates/ra_ide_api_light/src/test_utils.rs new file mode 100644 index 000000000..dc2470aa3 --- /dev/null +++ b/crates/ra_ide_api_light/src/test_utils.rs | |||
@@ -0,0 +1,41 @@ | |||
1 | use ra_syntax::{SourceFile, TextRange, TextUnit}; | ||
2 | |||
3 | use crate::LocalEdit; | ||
4 | pub use test_utils::*; | ||
5 | |||
6 | pub fn check_action<F: Fn(&SourceFile, TextUnit) -> Option<LocalEdit>>( | ||
7 | before: &str, | ||
8 | after: &str, | ||
9 | f: F, | ||
10 | ) { | ||
11 | let (before_cursor_pos, before) = extract_offset(before); | ||
12 | let file = SourceFile::parse(&before); | ||
13 | let result = f(&file, before_cursor_pos).expect("code action is not applicable"); | ||
14 | let actual = result.edit.apply(&before); | ||
15 | let actual_cursor_pos = match result.cursor_position { | ||
16 | None => result | ||
17 | .edit | ||
18 | .apply_to_offset(before_cursor_pos) | ||
19 | .expect("cursor position is affected by the edit"), | ||
20 | Some(off) => off, | ||
21 | }; | ||
22 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
23 | assert_eq_text!(after, &actual); | ||
24 | } | ||
25 | |||
26 | pub fn check_action_range<F: Fn(&SourceFile, TextRange) -> Option<LocalEdit>>( | ||
27 | before: &str, | ||
28 | after: &str, | ||
29 | f: F, | ||
30 | ) { | ||
31 | let (range, before) = extract_range(before); | ||
32 | let file = SourceFile::parse(&before); | ||
33 | let result = f(&file, range).expect("code action is not applicable"); | ||
34 | let actual = result.edit.apply(&before); | ||
35 | let actual_cursor_pos = match result.cursor_position { | ||
36 | None => result.edit.apply_to_offset(range.start()).unwrap(), | ||
37 | Some(off) => off, | ||
38 | }; | ||
39 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
40 | assert_eq_text!(after, &actual); | ||
41 | } | ||
diff --git a/crates/ra_ide_api_light/src/typing.rs b/crates/ra_ide_api_light/src/typing.rs new file mode 100644 index 000000000..d8177f245 --- /dev/null +++ b/crates/ra_ide_api_light/src/typing.rs | |||
@@ -0,0 +1,826 @@ | |||
1 | use std::mem; | ||
2 | |||
3 | use itertools::Itertools; | ||
4 | use ra_syntax::{ | ||
5 | algo::{find_node_at_offset, find_covering_node, find_leaf_at_offset, LeafAtOffset}, | ||
6 | ast, | ||
7 | AstNode, Direction, SourceFile, SyntaxKind, | ||
8 | SyntaxKind::*, | ||
9 | SyntaxNode, TextRange, TextUnit, | ||
10 | }; | ||
11 | |||
12 | use crate::{LocalEdit, TextEditBuilder}; | ||
13 | |||
14 | pub fn join_lines(file: &SourceFile, range: TextRange) -> LocalEdit { | ||
15 | let range = if range.is_empty() { | ||
16 | let syntax = file.syntax(); | ||
17 | let text = syntax.text().slice(range.start()..); | ||
18 | let pos = match text.find('\n') { | ||
19 | None => { | ||
20 | return LocalEdit { | ||
21 | label: "join lines".to_string(), | ||
22 | edit: TextEditBuilder::default().finish(), | ||
23 | cursor_position: None, | ||
24 | }; | ||
25 | } | ||
26 | Some(pos) => pos, | ||
27 | }; | ||
28 | TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n')) | ||
29 | } else { | ||
30 | range | ||
31 | }; | ||
32 | |||
33 | let node = find_covering_node(file.syntax(), range); | ||
34 | let mut edit = TextEditBuilder::default(); | ||
35 | for node in node.descendants() { | ||
36 | let text = match node.leaf_text() { | ||
37 | Some(text) => text, | ||
38 | None => continue, | ||
39 | }; | ||
40 | let range = match range.intersection(&node.range()) { | ||
41 | Some(range) => range, | ||
42 | None => continue, | ||
43 | } - node.range().start(); | ||
44 | for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { | ||
45 | let pos: TextUnit = (pos as u32).into(); | ||
46 | let off = node.range().start() + range.start() + pos; | ||
47 | if !edit.invalidates_offset(off) { | ||
48 | remove_newline(&mut edit, node, text.as_str(), off); | ||
49 | } | ||
50 | } | ||
51 | } | ||
52 | |||
53 | LocalEdit { | ||
54 | label: "join lines".to_string(), | ||
55 | edit: edit.finish(), | ||
56 | cursor_position: None, | ||
57 | } | ||
58 | } | ||
59 | |||
60 | pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { | ||
61 | let comment = find_leaf_at_offset(file.syntax(), offset) | ||
62 | .left_biased() | ||
63 | .and_then(ast::Comment::cast)?; | ||
64 | |||
65 | if let ast::CommentFlavor::Multiline = comment.flavor() { | ||
66 | return None; | ||
67 | } | ||
68 | |||
69 | let prefix = comment.prefix(); | ||
70 | if offset < comment.syntax().range().start() + TextUnit::of_str(prefix) + TextUnit::from(1) { | ||
71 | return None; | ||
72 | } | ||
73 | |||
74 | let indent = node_indent(file, comment.syntax())?; | ||
75 | let inserted = format!("\n{}{} ", indent, prefix); | ||
76 | let cursor_position = offset + TextUnit::of_str(&inserted); | ||
77 | let mut edit = TextEditBuilder::default(); | ||
78 | edit.insert(offset, inserted); | ||
79 | Some(LocalEdit { | ||
80 | label: "on enter".to_string(), | ||
81 | edit: edit.finish(), | ||
82 | cursor_position: Some(cursor_position), | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> { | ||
87 | let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) { | ||
88 | LeafAtOffset::Between(l, r) => { | ||
89 | assert!(r == node); | ||
90 | l | ||
91 | } | ||
92 | LeafAtOffset::Single(n) => { | ||
93 | assert!(n == node); | ||
94 | return Some(""); | ||
95 | } | ||
96 | LeafAtOffset::None => unreachable!(), | ||
97 | }; | ||
98 | if ws.kind() != WHITESPACE { | ||
99 | return None; | ||
100 | } | ||
101 | let text = ws.leaf_text().unwrap(); | ||
102 | let pos = text.as_str().rfind('\n').map(|it| it + 1).unwrap_or(0); | ||
103 | Some(&text[pos..]) | ||
104 | } | ||
105 | |||
106 | pub fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { | ||
107 | let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
108 | if let_stmt.has_semi() { | ||
109 | return None; | ||
110 | } | ||
111 | if let Some(expr) = let_stmt.initializer() { | ||
112 | let expr_range = expr.syntax().range(); | ||
113 | if expr_range.contains(offset) && offset != expr_range.start() { | ||
114 | return None; | ||
115 | } | ||
116 | if file | ||
117 | .syntax() | ||
118 | .text() | ||
119 | .slice(offset..expr_range.start()) | ||
120 | .contains('\n') | ||
121 | { | ||
122 | return None; | ||
123 | } | ||
124 | } else { | ||
125 | return None; | ||
126 | } | ||
127 | let offset = let_stmt.syntax().range().end(); | ||
128 | let mut edit = TextEditBuilder::default(); | ||
129 | edit.insert(offset, ";".to_string()); | ||
130 | Some(LocalEdit { | ||
131 | label: "add semicolon".to_string(), | ||
132 | edit: edit.finish(), | ||
133 | cursor_position: None, | ||
134 | }) | ||
135 | } | ||
136 | |||
137 | pub fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { | ||
138 | let before_dot_offset = offset - TextUnit::of_char('.'); | ||
139 | |||
140 | let whitespace = find_leaf_at_offset(file.syntax(), before_dot_offset).left_biased()?; | ||
141 | |||
142 | // find whitespace just left of the dot | ||
143 | ast::Whitespace::cast(whitespace)?; | ||
144 | |||
145 | // make sure there is a method call | ||
146 | let method_call = whitespace | ||
147 | .siblings(Direction::Prev) | ||
148 | // first is whitespace | ||
149 | .skip(1) | ||
150 | .next()?; | ||
151 | |||
152 | ast::MethodCallExpr::cast(method_call)?; | ||
153 | |||
154 | // find how much the _method call is indented | ||
155 | let method_chain_indent = method_call | ||
156 | .parent()? | ||
157 | .siblings(Direction::Prev) | ||
158 | .skip(1) | ||
159 | .next()? | ||
160 | .leaf_text() | ||
161 | .map(|x| last_line_indent_in_whitespace(x))?; | ||
162 | |||
163 | let current_indent = TextUnit::of_str(last_line_indent_in_whitespace(whitespace.leaf_text()?)); | ||
164 | // TODO: indent is always 4 spaces now. A better heuristic could look on the previous line(s) | ||
165 | |||
166 | let target_indent = TextUnit::of_str(method_chain_indent) + TextUnit::from_usize(4); | ||
167 | |||
168 | let diff = target_indent - current_indent; | ||
169 | |||
170 | let indent = "".repeat(diff.to_usize()); | ||
171 | |||
172 | let cursor_position = offset + diff; | ||
173 | let mut edit = TextEditBuilder::default(); | ||
174 | edit.insert(before_dot_offset, indent); | ||
175 | Some(LocalEdit { | ||
176 | label: "indent dot".to_string(), | ||
177 | edit: edit.finish(), | ||
178 | cursor_position: Some(cursor_position), | ||
179 | }) | ||
180 | } | ||
181 | |||
182 | /// Finds the last line in the whitespace | ||
183 | fn last_line_indent_in_whitespace(ws: &str) -> &str { | ||
184 | ws.split('\n').last().unwrap_or("") | ||
185 | } | ||
186 | |||
187 | fn remove_newline( | ||
188 | edit: &mut TextEditBuilder, | ||
189 | node: &SyntaxNode, | ||
190 | node_text: &str, | ||
191 | offset: TextUnit, | ||
192 | ) { | ||
193 | if node.kind() != WHITESPACE || node_text.bytes().filter(|&b| b == b'\n').count() != 1 { | ||
194 | // The node is either the first or the last in the file | ||
195 | let suff = &node_text[TextRange::from_to( | ||
196 | offset - node.range().start() + TextUnit::of_char('\n'), | ||
197 | TextUnit::of_str(node_text), | ||
198 | )]; | ||
199 | let spaces = suff.bytes().take_while(|&b| b == b' ').count(); | ||
200 | |||
201 | edit.replace( | ||
202 | TextRange::offset_len(offset, ((spaces + 1) as u32).into()), | ||
203 | " ".to_string(), | ||
204 | ); | ||
205 | return; | ||
206 | } | ||
207 | |||
208 | // Special case that turns something like: | ||
209 | // | ||
210 | // ``` | ||
211 | // my_function({<|> | ||
212 | // <some-expr> | ||
213 | // }) | ||
214 | // ``` | ||
215 | // | ||
216 | // into `my_function(<some-expr>)` | ||
217 | if join_single_expr_block(edit, node).is_some() { | ||
218 | return; | ||
219 | } | ||
220 | // ditto for | ||
221 | // | ||
222 | // ``` | ||
223 | // use foo::{<|> | ||
224 | // bar | ||
225 | // }; | ||
226 | // ``` | ||
227 | if join_single_use_tree(edit, node).is_some() { | ||
228 | return; | ||
229 | } | ||
230 | |||
231 | // The node is between two other nodes | ||
232 | let prev = node.prev_sibling().unwrap(); | ||
233 | let next = node.next_sibling().unwrap(); | ||
234 | if is_trailing_comma(prev.kind(), next.kind()) { | ||
235 | // Removes: trailing comma, newline (incl. surrounding whitespace) | ||
236 | edit.delete(TextRange::from_to(prev.range().start(), node.range().end())); | ||
237 | } else if prev.kind() == COMMA && next.kind() == R_CURLY { | ||
238 | // Removes: comma, newline (incl. surrounding whitespace) | ||
239 | let space = if let Some(left) = prev.prev_sibling() { | ||
240 | compute_ws(left, next) | ||
241 | } else { | ||
242 | " " | ||
243 | }; | ||
244 | edit.replace( | ||
245 | TextRange::from_to(prev.range().start(), node.range().end()), | ||
246 | space.to_string(), | ||
247 | ); | ||
248 | } else if let (Some(_), Some(next)) = (ast::Comment::cast(prev), ast::Comment::cast(next)) { | ||
249 | // Removes: newline (incl. surrounding whitespace), start of the next comment | ||
250 | edit.delete(TextRange::from_to( | ||
251 | node.range().start(), | ||
252 | next.syntax().range().start() + TextUnit::of_str(next.prefix()), | ||
253 | )); | ||
254 | } else { | ||
255 | // Remove newline but add a computed amount of whitespace characters | ||
256 | edit.replace(node.range(), compute_ws(prev, next).to_string()); | ||
257 | } | ||
258 | } | ||
259 | |||
260 | fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { | ||
261 | match (left, right) { | ||
262 | (COMMA, R_PAREN) | (COMMA, R_BRACK) => true, | ||
263 | _ => false, | ||
264 | } | ||
265 | } | ||
266 | |||
267 | fn join_single_expr_block(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> { | ||
268 | let block = ast::Block::cast(node.parent()?)?; | ||
269 | let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?; | ||
270 | let expr = single_expr(block)?; | ||
271 | edit.replace( | ||
272 | block_expr.syntax().range(), | ||
273 | expr.syntax().text().to_string(), | ||
274 | ); | ||
275 | Some(()) | ||
276 | } | ||
277 | |||
278 | fn single_expr(block: &ast::Block) -> Option<&ast::Expr> { | ||
279 | let mut res = None; | ||
280 | for child in block.syntax().children() { | ||
281 | if let Some(expr) = ast::Expr::cast(child) { | ||
282 | if expr.syntax().text().contains('\n') { | ||
283 | return None; | ||
284 | } | ||
285 | if mem::replace(&mut res, Some(expr)).is_some() { | ||
286 | return None; | ||
287 | } | ||
288 | } else { | ||
289 | match child.kind() { | ||
290 | WHITESPACE | L_CURLY | R_CURLY => (), | ||
291 | _ => return None, | ||
292 | } | ||
293 | } | ||
294 | } | ||
295 | res | ||
296 | } | ||
297 | |||
298 | fn join_single_use_tree(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> { | ||
299 | let use_tree_list = ast::UseTreeList::cast(node.parent()?)?; | ||
300 | let (tree,) = use_tree_list.use_trees().collect_tuple()?; | ||
301 | edit.replace( | ||
302 | use_tree_list.syntax().range(), | ||
303 | tree.syntax().text().to_string(), | ||
304 | ); | ||
305 | Some(()) | ||
306 | } | ||
307 | |||
308 | fn compute_ws(left: &SyntaxNode, right: &SyntaxNode) -> &'static str { | ||
309 | match left.kind() { | ||
310 | L_PAREN | L_BRACK => return "", | ||
311 | L_CURLY => { | ||
312 | if let USE_TREE = right.kind() { | ||
313 | return ""; | ||
314 | } | ||
315 | } | ||
316 | _ => (), | ||
317 | } | ||
318 | match right.kind() { | ||
319 | R_PAREN | R_BRACK => return "", | ||
320 | R_CURLY => { | ||
321 | if let USE_TREE = left.kind() { | ||
322 | return ""; | ||
323 | } | ||
324 | } | ||
325 | DOT => return "", | ||
326 | _ => (), | ||
327 | } | ||
328 | " " | ||
329 | } | ||
330 | |||
331 | #[cfg(test)] | ||
332 | mod tests { | ||
333 | use super::*; | ||
334 | use crate::test_utils::{ | ||
335 | add_cursor, assert_eq_text, check_action, extract_offset, extract_range, | ||
336 | }; | ||
337 | |||
338 | fn check_join_lines(before: &str, after: &str) { | ||
339 | check_action(before, after, |file, offset| { | ||
340 | let range = TextRange::offset_len(offset, 0.into()); | ||
341 | let res = join_lines(file, range); | ||
342 | Some(res) | ||
343 | }) | ||
344 | } | ||
345 | |||
346 | #[test] | ||
347 | fn test_join_lines_comma() { | ||
348 | check_join_lines( | ||
349 | r" | ||
350 | fn foo() { | ||
351 | <|>foo(1, | ||
352 | ) | ||
353 | } | ||
354 | ", | ||
355 | r" | ||
356 | fn foo() { | ||
357 | <|>foo(1) | ||
358 | } | ||
359 | ", | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn test_join_lines_lambda_block() { | ||
365 | check_join_lines( | ||
366 | r" | ||
367 | pub fn reparse(&self, edit: &AtomTextEdit) -> File { | ||
368 | <|>self.incremental_reparse(edit).unwrap_or_else(|| { | ||
369 | self.full_reparse(edit) | ||
370 | }) | ||
371 | } | ||
372 | ", | ||
373 | r" | ||
374 | pub fn reparse(&self, edit: &AtomTextEdit) -> File { | ||
375 | <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) | ||
376 | } | ||
377 | ", | ||
378 | ); | ||
379 | } | ||
380 | |||
381 | #[test] | ||
382 | fn test_join_lines_block() { | ||
383 | check_join_lines( | ||
384 | r" | ||
385 | fn foo() { | ||
386 | foo(<|>{ | ||
387 | 92 | ||
388 | }) | ||
389 | }", | ||
390 | r" | ||
391 | fn foo() { | ||
392 | foo(<|>92) | ||
393 | }", | ||
394 | ); | ||
395 | } | ||
396 | |||
397 | #[test] | ||
398 | fn test_join_lines_use_items_left() { | ||
399 | // No space after the '{' | ||
400 | check_join_lines( | ||
401 | r" | ||
402 | <|>use ra_syntax::{ | ||
403 | TextUnit, TextRange, | ||
404 | };", | ||
405 | r" | ||
406 | <|>use ra_syntax::{TextUnit, TextRange, | ||
407 | };", | ||
408 | ); | ||
409 | } | ||
410 | |||
411 | #[test] | ||
412 | fn test_join_lines_use_items_right() { | ||
413 | // No space after the '}' | ||
414 | check_join_lines( | ||
415 | r" | ||
416 | use ra_syntax::{ | ||
417 | <|> TextUnit, TextRange | ||
418 | };", | ||
419 | r" | ||
420 | use ra_syntax::{ | ||
421 | <|> TextUnit, TextRange};", | ||
422 | ); | ||
423 | } | ||
424 | |||
425 | #[test] | ||
426 | fn test_join_lines_use_items_right_comma() { | ||
427 | // No space after the '}' | ||
428 | check_join_lines( | ||
429 | r" | ||
430 | use ra_syntax::{ | ||
431 | <|> TextUnit, TextRange, | ||
432 | };", | ||
433 | r" | ||
434 | use ra_syntax::{ | ||
435 | <|> TextUnit, TextRange};", | ||
436 | ); | ||
437 | } | ||
438 | |||
439 | #[test] | ||
440 | fn test_join_lines_use_tree() { | ||
441 | check_join_lines( | ||
442 | r" | ||
443 | use ra_syntax::{ | ||
444 | algo::<|>{ | ||
445 | find_leaf_at_offset, | ||
446 | }, | ||
447 | ast, | ||
448 | };", | ||
449 | r" | ||
450 | use ra_syntax::{ | ||
451 | algo::<|>find_leaf_at_offset, | ||
452 | ast, | ||
453 | };", | ||
454 | ); | ||
455 | } | ||
456 | |||
457 | #[test] | ||
458 | fn test_join_lines_normal_comments() { | ||
459 | check_join_lines( | ||
460 | r" | ||
461 | fn foo() { | ||
462 | // Hello<|> | ||
463 | // world! | ||
464 | } | ||
465 | ", | ||
466 | r" | ||
467 | fn foo() { | ||
468 | // Hello<|> world! | ||
469 | } | ||
470 | ", | ||
471 | ); | ||
472 | } | ||
473 | |||
474 | #[test] | ||
475 | fn test_join_lines_doc_comments() { | ||
476 | check_join_lines( | ||
477 | r" | ||
478 | fn foo() { | ||
479 | /// Hello<|> | ||
480 | /// world! | ||
481 | } | ||
482 | ", | ||
483 | r" | ||
484 | fn foo() { | ||
485 | /// Hello<|> world! | ||
486 | } | ||
487 | ", | ||
488 | ); | ||
489 | } | ||
490 | |||
491 | #[test] | ||
492 | fn test_join_lines_mod_comments() { | ||
493 | check_join_lines( | ||
494 | r" | ||
495 | fn foo() { | ||
496 | //! Hello<|> | ||
497 | //! world! | ||
498 | } | ||
499 | ", | ||
500 | r" | ||
501 | fn foo() { | ||
502 | //! Hello<|> world! | ||
503 | } | ||
504 | ", | ||
505 | ); | ||
506 | } | ||
507 | |||
508 | #[test] | ||
509 | fn test_join_lines_multiline_comments_1() { | ||
510 | check_join_lines( | ||
511 | r" | ||
512 | fn foo() { | ||
513 | // Hello<|> | ||
514 | /* world! */ | ||
515 | } | ||
516 | ", | ||
517 | r" | ||
518 | fn foo() { | ||
519 | // Hello<|> world! */ | ||
520 | } | ||
521 | ", | ||
522 | ); | ||
523 | } | ||
524 | |||
525 | #[test] | ||
526 | fn test_join_lines_multiline_comments_2() { | ||
527 | check_join_lines( | ||
528 | r" | ||
529 | fn foo() { | ||
530 | // The<|> | ||
531 | /* quick | ||
532 | brown | ||
533 | fox! */ | ||
534 | } | ||
535 | ", | ||
536 | r" | ||
537 | fn foo() { | ||
538 | // The<|> quick | ||
539 | brown | ||
540 | fox! */ | ||
541 | } | ||
542 | ", | ||
543 | ); | ||
544 | } | ||
545 | |||
546 | fn check_join_lines_sel(before: &str, after: &str) { | ||
547 | let (sel, before) = extract_range(before); | ||
548 | let file = SourceFile::parse(&before); | ||
549 | let result = join_lines(&file, sel); | ||
550 | let actual = result.edit.apply(&before); | ||
551 | assert_eq_text!(after, &actual); | ||
552 | } | ||
553 | |||
554 | #[test] | ||
555 | fn test_join_lines_selection_fn_args() { | ||
556 | check_join_lines_sel( | ||
557 | r" | ||
558 | fn foo() { | ||
559 | <|>foo(1, | ||
560 | 2, | ||
561 | 3, | ||
562 | <|>) | ||
563 | } | ||
564 | ", | ||
565 | r" | ||
566 | fn foo() { | ||
567 | foo(1, 2, 3) | ||
568 | } | ||
569 | ", | ||
570 | ); | ||
571 | } | ||
572 | |||
573 | #[test] | ||
574 | fn test_join_lines_selection_struct() { | ||
575 | check_join_lines_sel( | ||
576 | r" | ||
577 | struct Foo <|>{ | ||
578 | f: u32, | ||
579 | }<|> | ||
580 | ", | ||
581 | r" | ||
582 | struct Foo { f: u32 } | ||
583 | ", | ||
584 | ); | ||
585 | } | ||
586 | |||
587 | #[test] | ||
588 | fn test_join_lines_selection_dot_chain() { | ||
589 | check_join_lines_sel( | ||
590 | r" | ||
591 | fn foo() { | ||
592 | join(<|>type_params.type_params() | ||
593 | .filter_map(|it| it.name()) | ||
594 | .map(|it| it.text())<|>) | ||
595 | }", | ||
596 | r" | ||
597 | fn foo() { | ||
598 | join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) | ||
599 | }", | ||
600 | ); | ||
601 | } | ||
602 | |||
603 | #[test] | ||
604 | fn test_join_lines_selection_lambda_block_body() { | ||
605 | check_join_lines_sel( | ||
606 | r" | ||
607 | pub fn handle_find_matching_brace() { | ||
608 | params.offsets | ||
609 | .map(|offset| <|>{ | ||
610 | world.analysis().matching_brace(&file, offset).unwrap_or(offset) | ||
611 | }<|>) | ||
612 | .collect(); | ||
613 | }", | ||
614 | r" | ||
615 | pub fn handle_find_matching_brace() { | ||
616 | params.offsets | ||
617 | .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) | ||
618 | .collect(); | ||
619 | }", | ||
620 | ); | ||
621 | } | ||
622 | |||
623 | #[test] | ||
624 | fn test_on_eq_typed() { | ||
625 | fn do_check(before: &str, after: &str) { | ||
626 | let (offset, before) = extract_offset(before); | ||
627 | let file = SourceFile::parse(&before); | ||
628 | let result = on_eq_typed(&file, offset).unwrap(); | ||
629 | let actual = result.edit.apply(&before); | ||
630 | assert_eq_text!(after, &actual); | ||
631 | } | ||
632 | |||
633 | // do_check(r" | ||
634 | // fn foo() { | ||
635 | // let foo =<|> | ||
636 | // } | ||
637 | // ", r" | ||
638 | // fn foo() { | ||
639 | // let foo =; | ||
640 | // } | ||
641 | // "); | ||
642 | do_check( | ||
643 | r" | ||
644 | fn foo() { | ||
645 | let foo =<|> 1 + 1 | ||
646 | } | ||
647 | ", | ||
648 | r" | ||
649 | fn foo() { | ||
650 | let foo = 1 + 1; | ||
651 | } | ||
652 | ", | ||
653 | ); | ||
654 | // do_check(r" | ||
655 | // fn foo() { | ||
656 | // let foo =<|> | ||
657 | // let bar = 1; | ||
658 | // } | ||
659 | // ", r" | ||
660 | // fn foo() { | ||
661 | // let foo =; | ||
662 | // let bar = 1; | ||
663 | // } | ||
664 | // "); | ||
665 | } | ||
666 | |||
667 | #[test] | ||
668 | fn test_on_dot_typed() { | ||
669 | fn do_check(before: &str, after: &str) { | ||
670 | let (offset, before) = extract_offset(before); | ||
671 | let file = SourceFile::parse(&before); | ||
672 | if let Some(result) = on_eq_typed(&file, offset) { | ||
673 | let actual = result.edit.apply(&before); | ||
674 | assert_eq_text!(after, &actual); | ||
675 | }; | ||
676 | } | ||
677 | // indent if continuing chain call | ||
678 | do_check( | ||
679 | r" | ||
680 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
681 | self.child_impl(db, name) | ||
682 | .<|> | ||
683 | } | ||
684 | ", | ||
685 | r" | ||
686 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
687 | self.child_impl(db, name) | ||
688 | . | ||
689 | } | ||
690 | ", | ||
691 | ); | ||
692 | |||
693 | // do not indent if already indented | ||
694 | do_check( | ||
695 | r" | ||
696 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
697 | self.child_impl(db, name) | ||
698 | .<|> | ||
699 | } | ||
700 | ", | ||
701 | r" | ||
702 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
703 | self.child_impl(db, name) | ||
704 | . | ||
705 | } | ||
706 | ", | ||
707 | ); | ||
708 | |||
709 | // indent if the previous line is already indented | ||
710 | do_check( | ||
711 | r" | ||
712 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
713 | self.child_impl(db, name) | ||
714 | .first() | ||
715 | .<|> | ||
716 | } | ||
717 | ", | ||
718 | r" | ||
719 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
720 | self.child_impl(db, name) | ||
721 | .first() | ||
722 | . | ||
723 | } | ||
724 | ", | ||
725 | ); | ||
726 | |||
727 | // don't indent if indent matches previous line | ||
728 | do_check( | ||
729 | r" | ||
730 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
731 | self.child_impl(db, name) | ||
732 | .first() | ||
733 | .<|> | ||
734 | } | ||
735 | ", | ||
736 | r" | ||
737 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
738 | self.child_impl(db, name) | ||
739 | .first() | ||
740 | . | ||
741 | } | ||
742 | ", | ||
743 | ); | ||
744 | |||
745 | // don't indent if there is no method call on previous line | ||
746 | do_check( | ||
747 | r" | ||
748 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
749 | .<|> | ||
750 | } | ||
751 | ", | ||
752 | r" | ||
753 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
754 | . | ||
755 | } | ||
756 | ", | ||
757 | ); | ||
758 | |||
759 | // indent to match previous expr | ||
760 | do_check( | ||
761 | r" | ||
762 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
763 | self.child_impl(db, name) | ||
764 | .<|> | ||
765 | } | ||
766 | ", | ||
767 | r" | ||
768 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
769 | self.child_impl(db, name) | ||
770 | . | ||
771 | } | ||
772 | ", | ||
773 | ); | ||
774 | } | ||
775 | |||
776 | #[test] | ||
777 | fn test_on_enter() { | ||
778 | fn apply_on_enter(before: &str) -> Option<String> { | ||
779 | let (offset, before) = extract_offset(before); | ||
780 | let file = SourceFile::parse(&before); | ||
781 | let result = on_enter(&file, offset)?; | ||
782 | let actual = result.edit.apply(&before); | ||
783 | let actual = add_cursor(&actual, result.cursor_position.unwrap()); | ||
784 | Some(actual) | ||
785 | } | ||
786 | |||
787 | fn do_check(before: &str, after: &str) { | ||
788 | let actual = apply_on_enter(before).unwrap(); | ||
789 | assert_eq_text!(after, &actual); | ||
790 | } | ||
791 | |||
792 | fn do_check_noop(text: &str) { | ||
793 | assert!(apply_on_enter(text).is_none()) | ||
794 | } | ||
795 | |||
796 | do_check( | ||
797 | r" | ||
798 | /// Some docs<|> | ||
799 | fn foo() { | ||
800 | } | ||
801 | ", | ||
802 | r" | ||
803 | /// Some docs | ||
804 | /// <|> | ||
805 | fn foo() { | ||
806 | } | ||
807 | ", | ||
808 | ); | ||
809 | do_check( | ||
810 | r" | ||
811 | impl S { | ||
812 | /// Some<|> docs. | ||
813 | fn foo() {} | ||
814 | } | ||
815 | ", | ||
816 | r" | ||
817 | impl S { | ||
818 | /// Some | ||
819 | /// <|> docs. | ||
820 | fn foo() {} | ||
821 | } | ||
822 | ", | ||
823 | ); | ||
824 | do_check_noop(r"<|>//! docz"); | ||
825 | } | ||
826 | } | ||