aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api_light/src')
-rw-r--r--crates/ra_ide_api_light/src/assists.rs209
-rw-r--r--crates/ra_ide_api_light/src/assists/add_derive.rs84
-rw-r--r--crates/ra_ide_api_light/src/assists/add_impl.rs66
-rw-r--r--crates/ra_ide_api_light/src/assists/change_visibility.rs116
-rw-r--r--crates/ra_ide_api_light/src/assists/flip_comma.rs31
-rw-r--r--crates/ra_ide_api_light/src/assists/introduce_variable.rs144
-rw-r--r--crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs92
-rw-r--r--crates/ra_ide_api_light/src/assists/split_import.rs56
-rw-r--r--crates/ra_ide_api_light/src/diagnostics.rs266
-rw-r--r--crates/ra_ide_api_light/src/extend_selection.rs281
-rw-r--r--crates/ra_ide_api_light/src/folding_ranges.rs297
-rw-r--r--crates/ra_ide_api_light/src/lib.rs173
-rw-r--r--crates/ra_ide_api_light/src/line_index.rs399
-rw-r--r--crates/ra_ide_api_light/src/line_index_utils.rs363
-rw-r--r--crates/ra_ide_api_light/src/structure.rs129
-rw-r--r--crates/ra_ide_api_light/src/test_utils.rs41
-rw-r--r--crates/ra_ide_api_light/src/typing.rs826
17 files changed, 3573 insertions, 0 deletions
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
6mod flip_comma;
7mod add_derive;
8mod add_impl;
9mod introduce_variable;
10mod change_visibility;
11mod split_import;
12mod replace_if_let_with_match;
13
14use ra_text_edit::{TextEdit, TextEditBuilder};
15use 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};
20use itertools::Itertools;
21
22pub 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.
33pub 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)]
50pub struct LocalEdit {
51 pub label: String,
52 pub edit: TextEdit,
53 pub cursor_position: Option<TextUnit>,
54}
55
56fn 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)]
93pub struct AssistCtx<'a> {
94 source_file: &'a SourceFile,
95 range: TextRange,
96 should_compute_edit: bool,
97}
98
99#[derive(Debug)]
100pub enum Assist {
101 Applicable,
102 Edit(LocalEdit),
103}
104
105#[derive(Default)]
106struct AssistBuilder {
107 edit: TextEditBuilder,
108 cursor_position: Option<TextUnit>,
109}
110
111impl<'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
162impl 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
185fn 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
191fn reindent(text: &str, indent: &str) -> String {
192 let indent = format!("\n{}", indent);
193 text.lines().intersperse(&indent).collect()
194}
195
196#[cfg(test)]
197fn 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)]
205fn 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 @@
1use ra_syntax::{
2 ast::{self, AstNode, AttrsOwner},
3 SyntaxKind::{WHITESPACE, COMMENT},
4 TextUnit,
5};
6
7use crate::assists::{AssistCtx, Assist};
8
9pub 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.
31fn 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)]
40mod 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.
74struct Foo { a: i32<|>, }
75 ",
76 "
77/// `Foo` is a pretty important struct.
78/// It does stuff.
79#[derive(<|>)]
80struct 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 @@
1use join_to_string::join;
2use ra_syntax::{
3 ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner},
4 TextUnit,
5};
6
7use crate::assists::{AssistCtx, Assist};
8
9pub 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)]
43mod 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 @@
1use 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
7use crate::assists::{AssistCtx, Assist};
8
9pub 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
16fn 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
49fn 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)]
60mod 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 @@
1use ra_syntax::{
2 Direction,
3 SyntaxKind::COMMA,
4};
5
6use crate::assists::{non_trivia_sibling, AssistCtx, Assist};
7
8pub 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)]
19mod 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 @@
1use ra_syntax::{
2 ast::{self, AstNode},
3 SyntaxKind::WHITESPACE,
4 SyntaxNode, TextUnit,
5};
6
7use crate::assists::{AssistCtx, Assist};
8
9pub 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.
42fn 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)]
61mod 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 "
70fn foo() {
71 foo(<|>1 + 1<|>);
72}",
73 "
74fn 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 "
86fn foo() {
87 <|>1 + 1<|>;
88}",
89 "
90fn 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 "
101fn foo() {
102 <|>1<|> + 1;
103}",
104 "
105fn 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 "
117fn foo() {
118 bar(<|>1 + 1<|>)
119}",
120 "
121fn 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 "
133fn foo() {
134 <|>bar(1 + 1)<|>
135}",
136 "
137fn 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 @@
1use ra_syntax::{
2 AstNode, SyntaxKind::{L_CURLY, R_CURLY, WHITESPACE},
3 ast,
4};
5
6use crate::assists::{AssistCtx, Assist};
7
8pub 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
23fn 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
41fn 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
48fn 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)]
63mod 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 "
72impl VariantData {
73 pub fn is_struct(&self) -> bool {
74 if <|>let VariantData::Struct(..) = *self {
75 true
76 } else {
77 false
78 }
79 }
80} ",
81 "
82impl 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 @@
1use ra_syntax::{
2 TextUnit, AstNode, SyntaxKind::COLONCOLON,
3 ast,
4 algo::generate,
5};
6
7use crate::assists::{AssistCtx, Assist};
8
9pub 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)]
35mod 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 @@
1use itertools::Itertools;
2
3use ra_syntax::{
4 Location, SourceFile, SyntaxKind, TextRange, SyntaxNode,
5 ast::{self, AstNode},
6
7};
8use ra_text_edit::{TextEdit, TextEditBuilder};
9
10use crate::{Diagnostic, LocalEdit, Severity};
11
12pub 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
39fn 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
71fn 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
93fn 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)]
126mod 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#"
206struct A {
207 a: &'static str
208}
209
210fn main() {
211 let a = "haha";
212 A {
213 a: a
214 }
215}
216 "#,
217 r#"
218struct A {
219 a: &'static str
220}
221
222fn main() {
223 let a = "haha";
224 A {
225 a
226 }
227}
228 "#,
229 check_struct_shorthand_initialization,
230 );
231
232 check_apply(
233 r#"
234struct A {
235 a: &'static str,
236 b: &'static str
237}
238
239fn main() {
240 let a = "haha";
241 let b = "bb";
242 A {
243 a: a,
244 b
245 }
246}
247 "#,
248 r#"
249struct A {
250 a: &'static str,
251 b: &'static str
252}
253
254fn 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 @@
1use ra_syntax::{
2 Direction, SyntaxNode, TextRange, TextUnit,
3 algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset},
4 SyntaxKind::*,
5};
6
7pub 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
41fn 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
68fn 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
91fn 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
102fn 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
112fn 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)]
125mod 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#"
151impl 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#"
164struct A;
165
166/// bla
167/// bla
168struct 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#"
184fn 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/*
212foo
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#"
237fn main() { foo<|>+bar;}
238 "#,
239 &["foo", "foo+bar"],
240 );
241 do_check(
242 r#"
243fn 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#"
260impl 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#"
274fn 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 @@
1use rustc_hash::FxHashSet;
2
3use ra_syntax::{
4 ast, AstNode, Direction, SourceFile, SyntaxNode, TextRange,
5 SyntaxKind::{self, *},
6};
7
8#[derive(Debug, PartialEq, Eq)]
9pub enum FoldKind {
10 Comment,
11 Imports,
12 Block,
13}
14
15#[derive(Debug)]
16pub struct Fold {
17 pub range: TextRange,
18 pub kind: FoldKind,
19}
20
21pub 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
61fn 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
71fn 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
87fn 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
125fn 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)]
170mod 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
210fn 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
240fn 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;
251use std::vec;
252use std::io as iop;</fold>
253
254<fold>use std::mem;
255use std::f64;</fold>
256
257use std::collections::HashMap;
258// Some random comment
259use std::collections::VecDeque;
260
261fn 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;
272use std::vec;
273use std::io as iop;</fold>
274
275<fold>use std::mem;
276use std::f64;</fold>
277
278<fold>use std::collections::<fold>{
279 HashMap,
280 VecDeque,
281}</fold>;</fold>
282// Some random comment
283
284fn 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..40638eda8
--- /dev/null
+++ b/crates/ra_ide_api_light/src/lib.rs
@@ -0,0 +1,173 @@
1//! This crate provides thouse IDE features which use only a single file.
2//!
3//! This usually means functions which take sytnax tree as an input and produce
4//! an edit or some auxilarly info.
5
6pub mod assists;
7mod extend_selection;
8mod folding_ranges;
9mod line_index;
10mod line_index_utils;
11mod structure;
12#[cfg(test)]
13mod test_utils;
14mod typing;
15mod diagnostics;
16
17pub use self::{
18 assists::LocalEdit,
19 extend_selection::extend_selection,
20 folding_ranges::{folding_ranges, Fold, FoldKind},
21 line_index::{LineCol, LineIndex},
22 line_index_utils::translate_offset_with_edit,
23 structure::{file_structure, StructureNode},
24 typing::{join_lines, on_enter, on_dot_typed, on_eq_typed},
25 diagnostics::diagnostics
26};
27use ra_text_edit::TextEditBuilder;
28use ra_syntax::{
29 SourceFile, SyntaxNode, TextRange, TextUnit, Direction,
30 SyntaxKind::{self, *},
31 ast::{self, AstNode},
32 algo::find_leaf_at_offset,
33};
34use rustc_hash::FxHashSet;
35
36#[derive(Debug)]
37pub struct HighlightedRange {
38 pub range: TextRange,
39 pub tag: &'static str,
40}
41
42#[derive(Debug, Copy, Clone)]
43pub enum Severity {
44 Error,
45 WeakWarning,
46}
47
48#[derive(Debug)]
49pub struct Diagnostic {
50 pub range: TextRange,
51 pub msg: String,
52 pub severity: Severity,
53 pub fix: Option<LocalEdit>,
54}
55
56pub fn matching_brace(file: &SourceFile, offset: TextUnit) -> Option<TextUnit> {
57 const BRACES: &[SyntaxKind] = &[
58 L_CURLY, R_CURLY, L_BRACK, R_BRACK, L_PAREN, R_PAREN, L_ANGLE, R_ANGLE,
59 ];
60 let (brace_node, brace_idx) = find_leaf_at_offset(file.syntax(), offset)
61 .filter_map(|node| {
62 let idx = BRACES.iter().position(|&brace| brace == node.kind())?;
63 Some((node, idx))
64 })
65 .next()?;
66 let parent = brace_node.parent()?;
67 let matching_kind = BRACES[brace_idx ^ 1];
68 let matching_node = parent
69 .children()
70 .find(|node| node.kind() == matching_kind)?;
71 Some(matching_node.range().start())
72}
73
74pub fn highlight(root: &SyntaxNode) -> Vec<HighlightedRange> {
75 // Visited nodes to handle highlighting priorities
76 let mut highlighted = FxHashSet::default();
77 let mut res = Vec::new();
78 for node in root.descendants() {
79 if highlighted.contains(&node) {
80 continue;
81 }
82 let tag = match node.kind() {
83 COMMENT => "comment",
84 STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string",
85 ATTR => "attribute",
86 NAME_REF => "text",
87 NAME => "function",
88 INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal",
89 LIFETIME => "parameter",
90 k if k.is_keyword() => "keyword",
91 _ => {
92 if let Some(macro_call) = ast::MacroCall::cast(node) {
93 if let Some(path) = macro_call.path() {
94 if let Some(segment) = path.segment() {
95 if let Some(name_ref) = segment.name_ref() {
96 highlighted.insert(name_ref.syntax());
97 let range_start = name_ref.syntax().range().start();
98 let mut range_end = name_ref.syntax().range().end();
99 for sibling in path.syntax().siblings(Direction::Next) {
100 match sibling.kind() {
101 EXCL | IDENT => range_end = sibling.range().end(),
102 _ => (),
103 }
104 }
105 res.push(HighlightedRange {
106 range: TextRange::from_to(range_start, range_end),
107 tag: "macro",
108 })
109 }
110 }
111 }
112 }
113 continue;
114 }
115 };
116 res.push(HighlightedRange {
117 range: node.range(),
118 tag,
119 })
120 }
121 res
122}
123
124pub fn syntax_tree(file: &SourceFile) -> String {
125 ::ra_syntax::utils::dump_tree(file.syntax())
126}
127
128#[cfg(test)]
129mod tests {
130 use ra_syntax::AstNode;
131
132 use crate::test_utils::{add_cursor, assert_eq_dbg, assert_eq_text, extract_offset};
133
134 use super::*;
135
136 #[test]
137 fn test_highlighting() {
138 let file = SourceFile::parse(
139 r#"
140// comment
141fn main() {}
142 println!("Hello, {}!", 92);
143"#,
144 );
145 let hls = highlight(file.syntax());
146 assert_eq_dbg(
147 r#"[HighlightedRange { range: [1; 11), tag: "comment" },
148 HighlightedRange { range: [12; 14), tag: "keyword" },
149 HighlightedRange { range: [15; 19), tag: "function" },
150 HighlightedRange { range: [29; 37), tag: "macro" },
151 HighlightedRange { range: [38; 50), tag: "string" },
152 HighlightedRange { range: [52; 54), tag: "literal" }]"#,
153 &hls,
154 );
155 }
156
157 #[test]
158 fn test_matching_brace() {
159 fn do_check(before: &str, after: &str) {
160 let (pos, before) = extract_offset(before);
161 let file = SourceFile::parse(&before);
162 let new_pos = match matching_brace(&file, pos) {
163 None => pos,
164 Some(pos) => pos,
165 };
166 let actual = add_cursor(&before, new_pos);
167 assert_eq_text!(after, &actual);
168 }
169
170 do_check("struct Foo { a: i32, }<|>", "struct Foo <|>{ a: i32, }");
171 }
172
173}
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 @@
1use crate::TextUnit;
2use rustc_hash::FxHashMap;
3use superslice::Ext;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub 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)]
12pub struct LineCol {
13 pub line: u32,
14 pub col_utf16: u32,
15}
16
17#[derive(Clone, Debug, Hash, PartialEq, Eq)]
18pub(crate) struct Utf16Char {
19 pub(crate) start: TextUnit,
20 pub(crate) end: TextUnit,
21}
22
23impl Utf16Char {
24 fn len(&self) -> TextUnit {
25 self.end - self.start
26 }
27}
28
29impl 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
133pub 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)]
155mod 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)]
307mod 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 "
320const 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 "
330const 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 "
361const 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 @@
1use ra_text_edit::{AtomTextEdit, TextEdit};
2use ra_syntax::{TextUnit, TextRange};
3use crate::{LineIndex, LineCol, line_index::Utf16Char};
4
5#[derive(Debug, Clone)]
6enum Step {
7 Newline(TextUnit),
8 Utf16Char(TextRange),
9}
10
11#[derive(Debug)]
12struct LineIndexStepIter<'a> {
13 line_index: &'a LineIndex,
14 next_newline_idx: usize,
15 utf16_chars: Option<(TextUnit, std::slice::Iter<'a, Utf16Char>)>,
16}
17
18impl<'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
31impl<'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)]
57struct OffsetStepIter<'a> {
58 text: &'a str,
59 offset: TextUnit,
60}
61
62impl<'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)]
95enum NextSteps<'a> {
96 Use,
97 ReplaceMany(OffsetStepIter<'a>),
98 AddMany(OffsetStepIter<'a>),
99}
100
101#[derive(Debug)]
102struct TranslatedEdit<'a> {
103 delete: TextRange,
104 insert: &'a str,
105 diff: i64,
106}
107
108struct Edits<'a> {
109 edits: &'a [AtomTextEdit],
110 current: Option<TranslatedEdit<'a>>,
111 acc_diff: i64,
112}
113
114impl<'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)]
217struct RunningLineCol {
218 line: u32,
219 last_newline: TextUnit,
220 col_adjust: TextUnit,
221}
222
223impl 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
250pub 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)]
321mod 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 @@
1use crate::TextRange;
2
3use ra_syntax::{
4 algo::visit::{visitor, Visitor},
5 ast::{self, NameOwner},
6 AstNode, SourceFile, SyntaxKind, SyntaxNode, WalkEvent,
7};
8
9#[derive(Debug, Clone)]
10pub 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
18pub 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
41fn 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)]
88mod 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#"
96struct Foo {
97 x: i32
98}
99
100mod m {
101 fn bar() {}
102}
103
104enum E { X, Y(i32) }
105type T = ();
106static S: i32 = 92;
107const C: i32 = 92;
108
109impl E {}
110
111impl 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 @@
1use ra_syntax::{SourceFile, TextRange, TextUnit};
2
3use crate::LocalEdit;
4pub use test_utils::*;
5
6pub 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
26pub 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 @@
1use std::mem;
2
3use itertools::Itertools;
4use 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
12use crate::{LocalEdit, TextEditBuilder};
13
14pub 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
60pub 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
86fn 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
106pub 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
137pub 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
183fn last_line_indent_in_whitespace(ws: &str) -> &str {
184 ws.split('\n').last().unwrap_or("")
185}
186
187fn 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
260fn 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
267fn 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
278fn 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
298fn 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
308fn 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)]
332mod 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"
350fn foo() {
351 <|>foo(1,
352 )
353}
354",
355 r"
356fn foo() {
357 <|>foo(1)
358}
359",
360 );
361 }
362
363 #[test]
364 fn test_join_lines_lambda_block() {
365 check_join_lines(
366 r"
367pub fn reparse(&self, edit: &AtomTextEdit) -> File {
368 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
369 self.full_reparse(edit)
370 })
371}
372",
373 r"
374pub 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"
385fn foo() {
386 foo(<|>{
387 92
388 })
389}",
390 r"
391fn 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"
416use ra_syntax::{
417<|> TextUnit, TextRange
418};",
419 r"
420use 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"
430use ra_syntax::{
431<|> TextUnit, TextRange,
432};",
433 r"
434use ra_syntax::{
435<|> TextUnit, TextRange};",
436 );
437 }
438
439 #[test]
440 fn test_join_lines_use_tree() {
441 check_join_lines(
442 r"
443use ra_syntax::{
444 algo::<|>{
445 find_leaf_at_offset,
446 },
447 ast,
448};",
449 r"
450use 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"
461fn foo() {
462 // Hello<|>
463 // world!
464}
465",
466 r"
467fn foo() {
468 // Hello<|> world!
469}
470",
471 );
472 }
473
474 #[test]
475 fn test_join_lines_doc_comments() {
476 check_join_lines(
477 r"
478fn foo() {
479 /// Hello<|>
480 /// world!
481}
482",
483 r"
484fn foo() {
485 /// Hello<|> world!
486}
487",
488 );
489 }
490
491 #[test]
492 fn test_join_lines_mod_comments() {
493 check_join_lines(
494 r"
495fn foo() {
496 //! Hello<|>
497 //! world!
498}
499",
500 r"
501fn 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"
512fn foo() {
513 // Hello<|>
514 /* world! */
515}
516",
517 r"
518fn 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"
529fn foo() {
530 // The<|>
531 /* quick
532 brown
533 fox! */
534}
535",
536 r"
537fn 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"
558fn foo() {
559 <|>foo(1,
560 2,
561 3,
562 <|>)
563}
564 ",
565 r"
566fn 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"
577struct Foo <|>{
578 f: u32,
579}<|>
580 ",
581 r"
582struct Foo { f: u32 }
583 ",
584 );
585 }
586
587 #[test]
588 fn test_join_lines_selection_dot_chain() {
589 check_join_lines_sel(
590 r"
591fn foo() {
592 join(<|>type_params.type_params()
593 .filter_map(|it| it.name())
594 .map(|it| it.text())<|>)
595}",
596 r"
597fn 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"
607pub 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"
615pub 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"
644fn foo() {
645 let foo =<|> 1 + 1
646}
647",
648 r"
649fn 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<|>
799fn foo() {
800}
801",
802 r"
803/// Some docs
804/// <|>
805fn foo() {
806}
807",
808 );
809 do_check(
810 r"
811impl S {
812 /// Some<|> docs.
813 fn foo() {}
814}
815",
816 r"
817impl S {
818 /// Some
819 /// <|> docs.
820 fn foo() {}
821}
822",
823 );
824 do_check_noop(r"<|>//! docz");
825 }
826}