aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_editor/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_editor/src')
-rw-r--r--crates/ra_editor/src/assists.rs185
-rw-r--r--crates/ra_editor/src/assists/add_derive.rs84
-rw-r--r--crates/ra_editor/src/assists/add_impl.rs66
-rw-r--r--crates/ra_editor/src/assists/change_visibility.rs88
-rw-r--r--crates/ra_editor/src/assists/flip_comma.rs31
-rw-r--r--crates/ra_editor/src/assists/introduce_variable.rs144
-rw-r--r--crates/ra_editor/src/code_actions.rs415
-rw-r--r--crates/ra_editor/src/diagnostics.rs6
-rw-r--r--crates/ra_editor/src/extend_selection.rs36
-rw-r--r--crates/ra_editor/src/lib.rs76
-rw-r--r--crates/ra_editor/src/structure.rs129
-rw-r--r--crates/ra_editor/src/symbols.rs246
-rw-r--r--crates/ra_editor/src/typing.rs8
13 files changed, 767 insertions, 747 deletions
diff --git a/crates/ra_editor/src/assists.rs b/crates/ra_editor/src/assists.rs
new file mode 100644
index 000000000..cc40ee4c8
--- /dev/null
+++ b/crates/ra_editor/src/assists.rs
@@ -0,0 +1,185 @@
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;
11
12use ra_text_edit::{TextEdit, TextEditBuilder};
13use ra_syntax::{
14 Direction, SyntaxNodeRef, TextUnit, TextRange,SourceFileNode, AstNode,
15 algo::{find_leaf_at_offset, find_covering_node, LeafAtOffset},
16};
17
18use crate::find_node_at_offset;
19
20pub use self::{
21 flip_comma::flip_comma,
22 add_derive::add_derive,
23 add_impl::add_impl,
24 introduce_variable::introduce_variable,
25 change_visibility::change_visibility,
26};
27
28/// Return all the assists applicable at the given position.
29pub fn assists(file: &SourceFileNode, range: TextRange) -> Vec<LocalEdit> {
30 let ctx = AssistCtx::new(file, range);
31 [
32 flip_comma,
33 add_derive,
34 add_impl,
35 introduce_variable,
36 change_visibility,
37 ]
38 .iter()
39 .filter_map(|&assist| ctx.clone().apply(assist))
40 .collect()
41}
42
43#[derive(Debug)]
44pub struct LocalEdit {
45 pub label: String,
46 pub edit: TextEdit,
47 pub cursor_position: Option<TextUnit>,
48}
49
50fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option<SyntaxNodeRef> {
51 node.siblings(direction)
52 .skip(1)
53 .find(|node| !node.kind().is_trivia())
54}
55
56/// `AssistCtx` allows to apply an assist or check if it could be applied.
57///
58/// Assists use a somewhat overengeneered approach, given the current needs. The
59/// assists workflow consists of two phases. In the first phase, a user asks for
60/// the list of available assists. In the second phase, the user picks a
61/// particular assist and it gets applied.
62///
63/// There are two peculiarities here:
64///
65/// * first, we ideally avoid computing more things then neccessary to answer
66/// "is assist applicable" in the first phase.
67/// * second, when we are appling assist, we don't have a gurantee that there
68/// weren't any changes between the point when user asked for assists and when
69/// they applied a particular assist. So, when applying assist, we need to do
70/// all the checks from scratch.
71///
72/// To avoid repeating the same code twice for both "check" and "apply"
73/// functions, we use an approach remeniscent of that of Django's function based
74/// views dealing with forms. Each assist receives a runtime parameter,
75/// `should_compute_edit`. It first check if an edit is applicable (potentially
76/// computing info required to compute the actual edit). If it is applicable,
77/// and `should_compute_edit` is `true`, it then computes the actual edit.
78///
79/// So, to implement the original assists workflow, we can first apply each edit
80/// with `should_compute_edit = false`, and then applying the selected edit
81/// again, with `should_compute_edit = true` this time.
82///
83/// Note, however, that we don't actually use such two-phase logic at the
84/// moment, because the LSP API is pretty awkward in this place, and it's much
85/// easier to just compute the edit eagarly :-)
86#[derive(Debug, Clone)]
87pub struct AssistCtx<'a> {
88 source_file: &'a SourceFileNode,
89 range: TextRange,
90 should_compute_edit: bool,
91}
92
93#[derive(Debug)]
94pub enum Assist {
95 Applicable,
96 Edit(LocalEdit),
97}
98
99#[derive(Default)]
100struct AssistBuilder {
101 edit: TextEditBuilder,
102 cursor_position: Option<TextUnit>,
103}
104
105impl<'a> AssistCtx<'a> {
106 pub fn new(source_file: &'a SourceFileNode, range: TextRange) -> AssistCtx {
107 AssistCtx {
108 source_file,
109 range,
110 should_compute_edit: false,
111 }
112 }
113
114 pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> {
115 self.should_compute_edit = true;
116 match assist(self) {
117 None => None,
118 Some(Assist::Edit(e)) => Some(e),
119 Some(Assist::Applicable) => unreachable!(),
120 }
121 }
122
123 pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool {
124 self.should_compute_edit = false;
125 match assist(self) {
126 None => false,
127 Some(Assist::Edit(_)) => unreachable!(),
128 Some(Assist::Applicable) => true,
129 }
130 }
131
132 fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> {
133 if !self.should_compute_edit {
134 return Some(Assist::Applicable);
135 }
136 let mut edit = AssistBuilder::default();
137 f(&mut edit);
138 Some(Assist::Edit(LocalEdit {
139 label: label.into(),
140 edit: edit.edit.finish(),
141 cursor_position: edit.cursor_position,
142 }))
143 }
144
145 pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<SyntaxNodeRef<'a>> {
146 find_leaf_at_offset(self.source_file.syntax(), self.range.start())
147 }
148 pub(crate) fn node_at_offset<N: AstNode<'a>>(&self) -> Option<N> {
149 find_node_at_offset(self.source_file.syntax(), self.range.start())
150 }
151 pub(crate) fn covering_node(&self) -> SyntaxNodeRef<'a> {
152 find_covering_node(self.source_file.syntax(), self.range)
153 }
154}
155
156impl AssistBuilder {
157 fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
158 self.edit.replace(range, replace_with.into())
159 }
160 #[allow(unused)]
161 fn delete(&mut self, range: TextRange) {
162 self.edit.delete(range)
163 }
164 fn insert(&mut self, offset: TextUnit, text: impl Into<String>) {
165 self.edit.insert(offset, text.into())
166 }
167 fn set_cursor(&mut self, offset: TextUnit) {
168 self.cursor_position = Some(offset)
169 }
170}
171
172#[cfg(test)]
173fn check_assist(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) {
174 crate::test_utils::check_action(before, after, |file, off| {
175 let range = TextRange::offset_len(off, 0.into());
176 AssistCtx::new(file, range).apply(assist)
177 })
178}
179
180#[cfg(test)]
181fn check_assist_range(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) {
182 crate::test_utils::check_action_range(before, after, |file, range| {
183 AssistCtx::new(file, range).apply(assist)
184 })
185}
diff --git a/crates/ra_editor/src/assists/add_derive.rs b/crates/ra_editor/src/assists/add_derive.rs
new file mode 100644
index 000000000..1e2cd4f30
--- /dev/null
+++ b/crates/ra_editor/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_editor/src/assists/add_impl.rs b/crates/ra_editor/src/assists/add_impl.rs
new file mode 100644
index 000000000..9353e2717
--- /dev/null
+++ b/crates/ra_editor/src/assists/add_impl.rs
@@ -0,0 +1,66 @@
1use join_to_string::join;
2use ra_syntax::{
3 ast::{self, AstNode, 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_editor/src/assists/change_visibility.rs b/crates/ra_editor/src/assists/change_visibility.rs
new file mode 100644
index 000000000..ac75f635e
--- /dev/null
+++ b/crates/ra_editor/src/assists/change_visibility.rs
@@ -0,0 +1,88 @@
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 let offset = if let Some(keyword) = ctx.leaf_at_offset().find(|leaf| match leaf.kind() {
11 FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true,
12 _ => false,
13 }) {
14 let parent = keyword.parent()?;
15 let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF];
16 // Parent is not a definition, can't add visibility
17 if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) {
18 return None;
19 }
20 // Already have visibility, do nothing
21 if parent.children().any(|child| child.kind() == VISIBILITY) {
22 return None;
23 }
24 parent.range().start()
25 } else {
26 let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?;
27 let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?;
28 if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() {
29 return None;
30 }
31 field.syntax().range().start()
32 };
33
34 ctx.build("make pub(crate)", |edit| {
35 edit.insert(offset, "pub(crate) ");
36 edit.set_cursor(offset);
37 })
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use crate::assists::check_assist;
44
45 #[test]
46 fn change_visibility_adds_pub_crate_to_items() {
47 check_assist(
48 change_visibility,
49 "<|>fn foo() {}",
50 "<|>pub(crate) fn foo() {}",
51 );
52 check_assist(
53 change_visibility,
54 "f<|>n foo() {}",
55 "<|>pub(crate) fn foo() {}",
56 );
57 check_assist(
58 change_visibility,
59 "<|>struct Foo {}",
60 "<|>pub(crate) struct Foo {}",
61 );
62 check_assist(
63 change_visibility,
64 "<|>mod foo {}",
65 "<|>pub(crate) mod foo {}",
66 );
67 check_assist(
68 change_visibility,
69 "<|>trait Foo {}",
70 "<|>pub(crate) trait Foo {}",
71 );
72 check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}");
73 check_assist(
74 change_visibility,
75 "unsafe f<|>n foo() {}",
76 "<|>pub(crate) unsafe fn foo() {}",
77 );
78 }
79
80 #[test]
81 fn change_visibility_works_with_struct_fields() {
82 check_assist(
83 change_visibility,
84 "struct S { <|>field: u32 }",
85 "struct S { <|>pub(crate) field: u32 }",
86 )
87 }
88}
diff --git a/crates/ra_editor/src/assists/flip_comma.rs b/crates/ra_editor/src/assists/flip_comma.rs
new file mode 100644
index 000000000..a343413cc
--- /dev/null
+++ b/crates/ra_editor/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_editor/src/assists/introduce_variable.rs b/crates/ra_editor/src/assists/introduce_variable.rs
new file mode 100644
index 000000000..782861023
--- /dev/null
+++ b/crates/ra_editor/src/assists/introduce_variable.rs
@@ -0,0 +1,144 @@
1use ra_syntax::{
2 ast::{self, AstNode},
3 SyntaxKind::WHITESPACE,
4 SyntaxNodeRef, 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<SyntaxNodeRef> {
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_editor/src/code_actions.rs b/crates/ra_editor/src/code_actions.rs
deleted file mode 100644
index 7615f37a6..000000000
--- a/crates/ra_editor/src/code_actions.rs
+++ /dev/null
@@ -1,415 +0,0 @@
1use join_to_string::join;
2
3use ra_syntax::{
4 algo::{find_covering_node, find_leaf_at_offset},
5 ast::{self, AstNode, AttrsOwner, NameOwner, TypeParamsOwner},
6 Direction, SourceFileNode,
7 SyntaxKind::{COMMA, WHITESPACE, COMMENT, VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF},
8 SyntaxNodeRef, TextRange, TextUnit,
9};
10
11use crate::{find_node_at_offset, TextEdit, TextEditBuilder};
12
13#[derive(Debug)]
14pub struct LocalEdit {
15 pub label: String,
16 pub edit: TextEdit,
17 pub cursor_position: Option<TextUnit>,
18}
19
20pub fn flip_comma<'a>(
21 file: &'a SourceFileNode,
22 offset: TextUnit,
23) -> Option<impl FnOnce() -> LocalEdit + 'a> {
24 let syntax = file.syntax();
25
26 let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?;
27 let prev = non_trivia_sibling(comma, Direction::Prev)?;
28 let next = non_trivia_sibling(comma, Direction::Next)?;
29 Some(move || {
30 let mut edit = TextEditBuilder::new();
31 edit.replace(prev.range(), next.text().to_string());
32 edit.replace(next.range(), prev.text().to_string());
33 LocalEdit {
34 label: "flip comma".to_string(),
35 edit: edit.finish(),
36 cursor_position: None,
37 }
38 })
39}
40
41pub fn add_derive<'a>(
42 file: &'a SourceFileNode,
43 offset: TextUnit,
44) -> Option<impl FnOnce() -> LocalEdit + 'a> {
45 let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?;
46 let node_start = derive_insertion_offset(nominal)?;
47 return Some(move || {
48 let derive_attr = nominal
49 .attrs()
50 .filter_map(|x| x.as_call())
51 .filter(|(name, _arg)| name == "derive")
52 .map(|(_name, arg)| arg)
53 .next();
54 let mut edit = TextEditBuilder::new();
55 let offset = match derive_attr {
56 None => {
57 edit.insert(node_start, "#[derive()]\n".to_string());
58 node_start + TextUnit::of_str("#[derive(")
59 }
60 Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'),
61 };
62 LocalEdit {
63 label: "add `#[derive]`".to_string(),
64 edit: edit.finish(),
65 cursor_position: Some(offset),
66 }
67 });
68
69 // Insert `derive` after doc comments.
70 fn derive_insertion_offset(nominal: ast::NominalDef) -> Option<TextUnit> {
71 let non_ws_child = nominal
72 .syntax()
73 .children()
74 .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?;
75 Some(non_ws_child.range().start())
76 }
77}
78
79pub fn add_impl<'a>(
80 file: &'a SourceFileNode,
81 offset: TextUnit,
82) -> Option<impl FnOnce() -> LocalEdit + 'a> {
83 let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?;
84 let name = nominal.name()?;
85
86 Some(move || {
87 let type_params = nominal.type_param_list();
88 let mut edit = TextEditBuilder::new();
89 let start_offset = nominal.syntax().range().end();
90 let mut buf = String::new();
91 buf.push_str("\n\nimpl");
92 if let Some(type_params) = type_params {
93 type_params.syntax().text().push_to(&mut buf);
94 }
95 buf.push_str(" ");
96 buf.push_str(name.text().as_str());
97 if let Some(type_params) = type_params {
98 let lifetime_params = type_params
99 .lifetime_params()
100 .filter_map(|it| it.lifetime())
101 .map(|it| it.text());
102 let type_params = type_params
103 .type_params()
104 .filter_map(|it| it.name())
105 .map(|it| it.text());
106 join(lifetime_params.chain(type_params))
107 .surround_with("<", ">")
108 .to_buf(&mut buf);
109 }
110 buf.push_str(" {\n");
111 let offset = start_offset + TextUnit::of_str(&buf);
112 buf.push_str("\n}");
113 edit.insert(start_offset, buf);
114 LocalEdit {
115 label: "add impl".to_string(),
116 edit: edit.finish(),
117 cursor_position: Some(offset),
118 }
119 })
120}
121
122pub fn introduce_variable<'a>(
123 file: &'a SourceFileNode,
124 range: TextRange,
125) -> Option<impl FnOnce() -> LocalEdit + 'a> {
126 let node = find_covering_node(file.syntax(), range);
127 let expr = node.ancestors().filter_map(ast::Expr::cast).next()?;
128
129 let anchor_stmt = anchor_stmt(expr)?;
130 let indent = anchor_stmt.prev_sibling()?;
131 if indent.kind() != WHITESPACE {
132 return None;
133 }
134 return Some(move || {
135 let mut buf = String::new();
136 let mut edit = TextEditBuilder::new();
137
138 buf.push_str("let var_name = ");
139 expr.syntax().text().push_to(&mut buf);
140 let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) {
141 Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax())
142 } else {
143 false
144 };
145 if is_full_stmt {
146 edit.replace(expr.syntax().range(), buf);
147 } else {
148 buf.push_str(";");
149 indent.text().push_to(&mut buf);
150 edit.replace(expr.syntax().range(), "var_name".to_string());
151 edit.insert(anchor_stmt.range().start(), buf);
152 }
153 let cursor_position = anchor_stmt.range().start() + TextUnit::of_str("let ");
154 LocalEdit {
155 label: "introduce variable".to_string(),
156 edit: edit.finish(),
157 cursor_position: Some(cursor_position),
158 }
159 });
160
161 /// Statement or last in the block expression, which will follow
162 /// the freshly introduced var.
163 fn anchor_stmt(expr: ast::Expr) -> Option<SyntaxNodeRef> {
164 expr.syntax().ancestors().find(|&node| {
165 if ast::Stmt::cast(node).is_some() {
166 return true;
167 }
168 if let Some(expr) = node
169 .parent()
170 .and_then(ast::Block::cast)
171 .and_then(|it| it.expr())
172 {
173 if expr.syntax() == node {
174 return true;
175 }
176 }
177 false
178 })
179 }
180}
181
182pub fn make_pub_crate<'a>(
183 file: &'a SourceFileNode,
184 offset: TextUnit,
185) -> Option<impl FnOnce() -> LocalEdit + 'a> {
186 let syntax = file.syntax();
187
188 let keyword = find_leaf_at_offset(syntax, offset).find(|leaf| match leaf.kind() {
189 FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true,
190 _ => false,
191 })?;
192 let parent = keyword.parent()?;
193 let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF];
194 let node_start = parent.range().start();
195 Some(move || {
196 let mut edit = TextEditBuilder::new();
197
198 if !def_kws.iter().any(|&def_kw| def_kw == parent.kind())
199 || parent.children().any(|child| child.kind() == VISIBILITY)
200 {
201 return LocalEdit {
202 label: "make pub crate".to_string(),
203 edit: edit.finish(),
204 cursor_position: Some(offset),
205 };
206 }
207
208 edit.insert(node_start, "pub(crate) ".to_string());
209 LocalEdit {
210 label: "make pub crate".to_string(),
211 edit: edit.finish(),
212 cursor_position: Some(node_start),
213 }
214 })
215}
216
217fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option<SyntaxNodeRef> {
218 node.siblings(direction)
219 .skip(1)
220 .find(|node| !node.kind().is_trivia())
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::test_utils::{check_action, check_action_range};
227
228 #[test]
229 fn test_swap_comma() {
230 check_action(
231 "fn foo(x: i32,<|> y: Result<(), ()>) {}",
232 "fn foo(y: Result<(), ()>,<|> x: i32) {}",
233 |file, off| flip_comma(file, off).map(|f| f()),
234 )
235 }
236
237 #[test]
238 fn add_derive_new() {
239 check_action(
240 "struct Foo { a: i32, <|>}",
241 "#[derive(<|>)]\nstruct Foo { a: i32, }",
242 |file, off| add_derive(file, off).map(|f| f()),
243 );
244 check_action(
245 "struct Foo { <|> a: i32, }",
246 "#[derive(<|>)]\nstruct Foo { a: i32, }",
247 |file, off| add_derive(file, off).map(|f| f()),
248 );
249 }
250
251 #[test]
252 fn add_derive_existing() {
253 check_action(
254 "#[derive(Clone)]\nstruct Foo { a: i32<|>, }",
255 "#[derive(Clone<|>)]\nstruct Foo { a: i32, }",
256 |file, off| add_derive(file, off).map(|f| f()),
257 );
258 }
259
260 #[test]
261 fn add_derive_new_with_doc_comment() {
262 check_action(
263 "
264/// `Foo` is a pretty important struct.
265/// It does stuff.
266struct Foo { a: i32<|>, }
267 ",
268 "
269/// `Foo` is a pretty important struct.
270/// It does stuff.
271#[derive(<|>)]
272struct Foo { a: i32, }
273 ",
274 |file, off| add_derive(file, off).map(|f| f()),
275 );
276 }
277
278 #[test]
279 fn test_add_impl() {
280 check_action(
281 "struct Foo {<|>}\n",
282 "struct Foo {}\n\nimpl Foo {\n<|>\n}\n",
283 |file, off| add_impl(file, off).map(|f| f()),
284 );
285 check_action(
286 "struct Foo<T: Clone> {<|>}",
287 "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}",
288 |file, off| add_impl(file, off).map(|f| f()),
289 );
290 check_action(
291 "struct Foo<'a, T: Foo<'a>> {<|>}",
292 "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}",
293 |file, off| add_impl(file, off).map(|f| f()),
294 );
295 }
296
297 #[test]
298 fn test_introduce_var_simple() {
299 check_action_range(
300 "
301fn foo() {
302 foo(<|>1 + 1<|>);
303}",
304 "
305fn foo() {
306 let <|>var_name = 1 + 1;
307 foo(var_name);
308}",
309 |file, range| introduce_variable(file, range).map(|f| f()),
310 );
311 }
312
313 #[test]
314 fn test_introduce_var_expr_stmt() {
315 check_action_range(
316 "
317fn foo() {
318 <|>1 + 1<|>;
319}",
320 "
321fn foo() {
322 let <|>var_name = 1 + 1;
323}",
324 |file, range| introduce_variable(file, range).map(|f| f()),
325 );
326 }
327
328 #[test]
329 fn test_introduce_var_part_of_expr_stmt() {
330 check_action_range(
331 "
332fn foo() {
333 <|>1<|> + 1;
334}",
335 "
336fn foo() {
337 let <|>var_name = 1;
338 var_name + 1;
339}",
340 |file, range| introduce_variable(file, range).map(|f| f()),
341 );
342 }
343
344 #[test]
345 fn test_introduce_var_last_expr() {
346 check_action_range(
347 "
348fn foo() {
349 bar(<|>1 + 1<|>)
350}",
351 "
352fn foo() {
353 let <|>var_name = 1 + 1;
354 bar(var_name)
355}",
356 |file, range| introduce_variable(file, range).map(|f| f()),
357 );
358 }
359
360 #[test]
361 fn test_introduce_var_last_full_expr() {
362 check_action_range(
363 "
364fn foo() {
365 <|>bar(1 + 1)<|>
366}",
367 "
368fn foo() {
369 let <|>var_name = bar(1 + 1);
370 var_name
371}",
372 |file, range| introduce_variable(file, range).map(|f| f()),
373 );
374 }
375
376 #[test]
377 fn test_make_pub_crate() {
378 check_action(
379 "<|>fn foo() {}",
380 "<|>pub(crate) fn foo() {}",
381 |file, off| make_pub_crate(file, off).map(|f| f()),
382 );
383 check_action(
384 "f<|>n foo() {}",
385 "<|>pub(crate) fn foo() {}",
386 |file, off| make_pub_crate(file, off).map(|f| f()),
387 );
388 check_action(
389 "<|>struct Foo {}",
390 "<|>pub(crate) struct Foo {}",
391 |file, off| make_pub_crate(file, off).map(|f| f()),
392 );
393 check_action("<|>mod foo {}", "<|>pub(crate) mod foo {}", |file, off| {
394 make_pub_crate(file, off).map(|f| f())
395 });
396 check_action(
397 "<|>trait Foo {}",
398 "<|>pub(crate) trait Foo {}",
399 |file, off| make_pub_crate(file, off).map(|f| f()),
400 );
401 check_action("m<|>od {}", "<|>pub(crate) mod {}", |file, off| {
402 make_pub_crate(file, off).map(|f| f())
403 });
404 check_action(
405 "pub(crate) f<|>n foo() {}",
406 "pub(crate) f<|>n foo() {}",
407 |file, off| make_pub_crate(file, off).map(|f| f()),
408 );
409 check_action(
410 "unsafe f<|>n foo() {}",
411 "<|>pub(crate) unsafe fn foo() {}",
412 |file, off| make_pub_crate(file, off).map(|f| f()),
413 );
414 }
415}
diff --git a/crates/ra_editor/src/diagnostics.rs b/crates/ra_editor/src/diagnostics.rs
index 1b336cfe2..199b0e502 100644
--- a/crates/ra_editor/src/diagnostics.rs
+++ b/crates/ra_editor/src/diagnostics.rs
@@ -57,7 +57,7 @@ fn check_unnecessary_braces_in_use_statement(
57 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(single_use_tree) 57 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(single_use_tree)
58 .unwrap_or_else(|| { 58 .unwrap_or_else(|| {
59 let to_replace = single_use_tree.syntax().text().to_string(); 59 let to_replace = single_use_tree.syntax().text().to_string();
60 let mut edit_builder = TextEditBuilder::new(); 60 let mut edit_builder = TextEditBuilder::default();
61 edit_builder.delete(range); 61 edit_builder.delete(range);
62 edit_builder.insert(range.start(), to_replace); 62 edit_builder.insert(range.start(), to_replace);
63 edit_builder.finish() 63 edit_builder.finish()
@@ -93,7 +93,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
93 let start = use_tree_list_node.prev_sibling()?.range().start(); 93 let start = use_tree_list_node.prev_sibling()?.range().start();
94 let end = use_tree_list_node.range().end(); 94 let end = use_tree_list_node.range().end();
95 let range = TextRange::from_to(start, end); 95 let range = TextRange::from_to(start, end);
96 let mut edit_builder = TextEditBuilder::new(); 96 let mut edit_builder = TextEditBuilder::default();
97 edit_builder.delete(range); 97 edit_builder.delete(range);
98 return Some(edit_builder.finish()); 98 return Some(edit_builder.finish());
99 } 99 }
@@ -111,7 +111,7 @@ fn check_struct_shorthand_initialization(
111 let field_name = name_ref.syntax().text().to_string(); 111 let field_name = name_ref.syntax().text().to_string();
112 let field_expr = expr.syntax().text().to_string(); 112 let field_expr = expr.syntax().text().to_string();
113 if field_name == field_expr { 113 if field_name == field_expr {
114 let mut edit_builder = TextEditBuilder::new(); 114 let mut edit_builder = TextEditBuilder::default();
115 edit_builder.delete(named_field.syntax().range()); 115 edit_builder.delete(named_field.syntax().range());
116 edit_builder.insert(named_field.syntax().range().start(), field_name); 116 edit_builder.insert(named_field.syntax().range().start(), field_name);
117 let edit = edit_builder.finish(); 117 let edit = edit_builder.finish();
diff --git a/crates/ra_editor/src/extend_selection.rs b/crates/ra_editor/src/extend_selection.rs
index 4665a336a..7a423852b 100644
--- a/crates/ra_editor/src/extend_selection.rs
+++ b/crates/ra_editor/src/extend_selection.rs
@@ -1,16 +1,12 @@
1use ra_syntax::{ 1use ra_syntax::{
2 algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset}, 2 algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset},
3 Direction, SourceFileNode, 3 Direction,
4 SyntaxKind::*, 4 SyntaxKind::*,
5 SyntaxNodeRef, TextRange, TextUnit, 5 SyntaxNodeRef, TextRange, TextUnit,
6}; 6};
7 7
8pub fn extend_selection(file: &SourceFileNode, range: TextRange) -> Option<TextRange> { 8pub fn extend_selection(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange> {
9 let syntax = file.syntax(); 9 let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING];
10 extend(syntax.borrowed(), range)
11}
12
13pub(crate) fn extend(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange> {
14 if range.is_empty() { 10 if range.is_empty() {
15 let offset = range.start(); 11 let offset = range.start();
16 let mut leaves = find_leaf_at_offset(root, offset); 12 let mut leaves = find_leaf_at_offset(root, offset);
@@ -20,8 +16,8 @@ pub(crate) fn extend(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange>
20 let leaf_range = match leaves { 16 let leaf_range = match leaves {
21 LeafAtOffset::None => return None, 17 LeafAtOffset::None => return None,
22 LeafAtOffset::Single(l) => { 18 LeafAtOffset::Single(l) => {
23 if l.kind() == COMMENT { 19 if string_kinds.contains(&l.kind()) {
24 extend_single_word_in_comment(l, offset).unwrap_or_else(|| l.range()) 20 extend_single_word_in_comment_or_string(l, offset).unwrap_or_else(|| l.range())
25 } else { 21 } else {
26 l.range() 22 l.range()
27 } 23 }
@@ -31,7 +27,7 @@ pub(crate) fn extend(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange>
31 return Some(leaf_range); 27 return Some(leaf_range);
32 }; 28 };
33 let node = find_covering_node(root, range); 29 let node = find_covering_node(root, range);
34 if node.kind() == COMMENT && range == node.range() { 30 if string_kinds.contains(&node.kind()) && range == node.range() {
35 if let Some(range) = extend_comments(node) { 31 if let Some(range) = extend_comments(node) {
36 return Some(range); 32 return Some(range);
37 } 33 }
@@ -43,7 +39,10 @@ pub(crate) fn extend(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange>
43 } 39 }
44} 40}
45 41
46fn extend_single_word_in_comment(leaf: SyntaxNodeRef, offset: TextUnit) -> Option<TextRange> { 42fn extend_single_word_in_comment_or_string(
43 leaf: SyntaxNodeRef,
44 offset: TextUnit,
45) -> Option<TextRange> {
47 let text: &str = leaf.leaf_text()?; 46 let text: &str = leaf.leaf_text()?;
48 let cursor_position: u32 = (offset - leaf.range().start()).into(); 47 let cursor_position: u32 = (offset - leaf.range().start()).into();
49 48
@@ -126,6 +125,7 @@ fn adj_comments(node: SyntaxNodeRef, dir: Direction) -> SyntaxNodeRef {
126#[cfg(test)] 125#[cfg(test)]
127mod tests { 126mod tests {
128 use super::*; 127 use super::*;
128 use ra_syntax::SourceFileNode;
129 use test_utils::extract_offset; 129 use test_utils::extract_offset;
130 130
131 fn do_check(before: &str, afters: &[&str]) { 131 fn do_check(before: &str, afters: &[&str]) {
@@ -133,7 +133,7 @@ mod tests {
133 let file = SourceFileNode::parse(&before); 133 let file = SourceFileNode::parse(&before);
134 let mut range = TextRange::offset_len(cursor, 0.into()); 134 let mut range = TextRange::offset_len(cursor, 0.into());
135 for &after in afters { 135 for &after in afters {
136 range = extend_selection(&file, range).unwrap(); 136 range = extend_selection(file.syntax(), range).unwrap();
137 let actual = &before[range]; 137 let actual = &before[range];
138 assert_eq!(after, actual); 138 assert_eq!(after, actual);
139 } 139 }
@@ -266,4 +266,16 @@ impl S {
266 &["hello", "// hello world"], 266 &["hello", "// hello world"],
267 ); 267 );
268 } 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 }
269} 281}
diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_editor/src/lib.rs
index a65637d52..ac283e2e0 100644
--- a/crates/ra_editor/src/lib.rs
+++ b/crates/ra_editor/src/lib.rs
@@ -1,28 +1,28 @@
1mod code_actions; 1pub mod assists;
2mod extend_selection; 2mod extend_selection;
3mod folding_ranges; 3mod folding_ranges;
4mod line_index; 4mod line_index;
5mod line_index_utils; 5mod line_index_utils;
6mod symbols; 6mod structure;
7#[cfg(test)] 7#[cfg(test)]
8mod test_utils; 8mod test_utils;
9mod typing; 9mod typing;
10mod diagnostics; 10mod diagnostics;
11 11
12pub use self::{ 12pub use self::{
13 code_actions::{add_derive, add_impl, flip_comma, introduce_variable, make_pub_crate, LocalEdit}, 13 assists::LocalEdit,
14 extend_selection::extend_selection, 14 extend_selection::extend_selection,
15 folding_ranges::{folding_ranges, Fold, FoldKind}, 15 folding_ranges::{folding_ranges, Fold, FoldKind},
16 line_index::{LineCol, LineIndex}, 16 line_index::{LineCol, LineIndex},
17 line_index_utils::translate_offset_with_edit, 17 line_index_utils::translate_offset_with_edit,
18 symbols::{file_structure, file_symbols, FileSymbol, StructureNode}, 18 structure::{file_structure, StructureNode},
19 typing::{join_lines, on_enter, on_eq_typed}, 19 typing::{join_lines, on_enter, on_eq_typed},
20 diagnostics::diagnostics 20 diagnostics::diagnostics
21}; 21};
22use ra_text_edit::{TextEdit, TextEditBuilder}; 22use ra_text_edit::TextEditBuilder;
23use ra_syntax::{ 23use ra_syntax::{
24 algo::find_leaf_at_offset, 24 algo::find_leaf_at_offset,
25 ast::{self, AstNode, NameOwner}, 25 ast::{self, AstNode},
26 SourceFileNode, 26 SourceFileNode,
27 SyntaxKind::{self, *}, 27 SyntaxKind::{self, *},
28 SyntaxNodeRef, TextRange, TextUnit, Direction, 28 SyntaxNodeRef, TextRange, TextUnit, Direction,
@@ -49,18 +49,6 @@ pub struct Diagnostic {
49 pub fix: Option<LocalEdit>, 49 pub fix: Option<LocalEdit>,
50} 50}
51 51
52#[derive(Debug)]
53pub struct Runnable {
54 pub range: TextRange,
55 pub kind: RunnableKind,
56}
57
58#[derive(Debug)]
59pub enum RunnableKind {
60 Test { name: String },
61 Bin,
62}
63
64pub fn matching_brace(file: &SourceFileNode, offset: TextUnit) -> Option<TextUnit> { 52pub fn matching_brace(file: &SourceFileNode, offset: TextUnit) -> Option<TextUnit> {
65 const BRACES: &[SyntaxKind] = &[ 53 const BRACES: &[SyntaxKind] = &[
66 L_CURLY, R_CURLY, L_BRACK, R_BRACK, L_PAREN, R_PAREN, L_ANGLE, R_ANGLE, 54 L_CURLY, R_CURLY, L_BRACK, R_BRACK, L_PAREN, R_PAREN, L_ANGLE, R_ANGLE,
@@ -79,11 +67,11 @@ pub fn matching_brace(file: &SourceFileNode, offset: TextUnit) -> Option<TextUni
79 Some(matching_node.range().start()) 67 Some(matching_node.range().start())
80} 68}
81 69
82pub fn highlight(file: &SourceFileNode) -> Vec<HighlightedRange> { 70pub fn highlight(root: SyntaxNodeRef) -> Vec<HighlightedRange> {
83 // Visited nodes to handle highlighting priorities 71 // Visited nodes to handle highlighting priorities
84 let mut highlighted = FxHashSet::default(); 72 let mut highlighted = FxHashSet::default();
85 let mut res = Vec::new(); 73 let mut res = Vec::new();
86 for node in file.syntax().descendants() { 74 for node in root.descendants() {
87 if highlighted.contains(&node) { 75 if highlighted.contains(&node) {
88 continue; 76 continue;
89 } 77 }
@@ -133,29 +121,6 @@ pub fn syntax_tree(file: &SourceFileNode) -> String {
133 ::ra_syntax::utils::dump_tree(file.syntax()) 121 ::ra_syntax::utils::dump_tree(file.syntax())
134} 122}
135 123
136pub fn runnables(file: &SourceFileNode) -> Vec<Runnable> {
137 file.syntax()
138 .descendants()
139 .filter_map(ast::FnDef::cast)
140 .filter_map(|f| {
141 let name = f.name()?.text();
142 let kind = if name == "main" {
143 RunnableKind::Bin
144 } else if f.has_atom_attr("test") {
145 RunnableKind::Test {
146 name: name.to_string(),
147 }
148 } else {
149 return None;
150 };
151 Some(Runnable {
152 range: f.syntax().range(),
153 kind,
154 })
155 })
156 .collect()
157}
158
159pub fn find_node_at_offset<'a, N: AstNode<'a>>( 124pub fn find_node_at_offset<'a, N: AstNode<'a>>(
160 syntax: SyntaxNodeRef<'a>, 125 syntax: SyntaxNodeRef<'a>,
161 offset: TextUnit, 126 offset: TextUnit,
@@ -178,7 +143,7 @@ fn main() {}
178 println!("Hello, {}!", 92); 143 println!("Hello, {}!", 92);
179"#, 144"#,
180 ); 145 );
181 let hls = highlight(&file); 146 let hls = highlight(file.syntax());
182 assert_eq_dbg( 147 assert_eq_dbg(
183 r#"[HighlightedRange { range: [1; 11), tag: "comment" }, 148 r#"[HighlightedRange { range: [1; 11), tag: "comment" },
184 HighlightedRange { range: [12; 14), tag: "keyword" }, 149 HighlightedRange { range: [12; 14), tag: "keyword" },
@@ -191,29 +156,6 @@ fn main() {}
191 } 156 }
192 157
193 #[test] 158 #[test]
194 fn test_runnables() {
195 let file = SourceFileNode::parse(
196 r#"
197fn main() {}
198
199#[test]
200fn test_foo() {}
201
202#[test]
203#[ignore]
204fn test_foo() {}
205"#,
206 );
207 let runnables = runnables(&file);
208 assert_eq_dbg(
209 r#"[Runnable { range: [1; 13), kind: Bin },
210 Runnable { range: [15; 39), kind: Test { name: "test_foo" } },
211 Runnable { range: [41; 75), kind: Test { name: "test_foo" } }]"#,
212 &runnables,
213 )
214 }
215
216 #[test]
217 fn test_matching_brace() { 159 fn test_matching_brace() {
218 fn do_check(before: &str, after: &str) { 160 fn do_check(before: &str, after: &str) {
219 let (pos, before) = extract_offset(before); 161 let (pos, before) = extract_offset(before);
diff --git a/crates/ra_editor/src/structure.rs b/crates/ra_editor/src/structure.rs
new file mode 100644
index 000000000..2292b1ddf
--- /dev/null
+++ b/crates/ra_editor/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, SourceFileNode, SyntaxKind, SyntaxNodeRef, 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: &SourceFileNode) -> 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: SyntaxNodeRef) -> Option<StructureNode> {
42 fn decl<'a, N: NameOwner<'a>>(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::ImplItem| {
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 = SourceFileNode::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_ITEM },
125 StructureNode { parent: None, label: "impl fmt::Debug for E", navigation_range: [160; 161), node_range: [140; 164), kind: IMPL_ITEM }]"#,
126 &structure,
127 )
128 }
129}
diff --git a/crates/ra_editor/src/symbols.rs b/crates/ra_editor/src/symbols.rs
deleted file mode 100644
index 9e25decfb..000000000
--- a/crates/ra_editor/src/symbols.rs
+++ /dev/null
@@ -1,246 +0,0 @@
1use crate::TextRange;
2
3use ra_syntax::{
4 algo::visit::{visitor, Visitor},
5 ast::{self, DocCommentsOwner, NameOwner},
6 AstNode, SourceFileNode, SmolStr, SyntaxKind, SyntaxNodeRef, 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
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct FileSymbol {
20 pub name: SmolStr,
21 pub node_range: TextRange,
22 pub kind: SyntaxKind,
23}
24
25impl FileSymbol {
26 pub fn docs(&self, file: &SourceFileNode) -> Option<String> {
27 file.syntax()
28 .descendants()
29 .filter(|node| node.kind() == self.kind && node.range() == self.node_range)
30 .filter_map(|node: SyntaxNodeRef| {
31 fn doc_comments<'a, N: DocCommentsOwner<'a>>(node: N) -> Option<String> {
32 let comments = node.doc_comment_text();
33 if comments.is_empty() {
34 None
35 } else {
36 Some(comments)
37 }
38 }
39
40 visitor()
41 .visit(doc_comments::<ast::FnDef>)
42 .visit(doc_comments::<ast::StructDef>)
43 .visit(doc_comments::<ast::EnumDef>)
44 .visit(doc_comments::<ast::TraitDef>)
45 .visit(doc_comments::<ast::Module>)
46 .visit(doc_comments::<ast::TypeDef>)
47 .visit(doc_comments::<ast::ConstDef>)
48 .visit(doc_comments::<ast::StaticDef>)
49 .accept(node)?
50 })
51 .nth(0)
52 }
53 /// Get a description of this node.
54 ///
55 /// e.g. `struct Name`, `enum Name`, `fn Name`
56 pub fn description(&self, file: &SourceFileNode) -> Option<String> {
57 // TODO: After type inference is done, add type information to improve the output
58 file.syntax()
59 .descendants()
60 .filter(|node| node.kind() == self.kind && node.range() == self.node_range)
61 .filter_map(|node: SyntaxNodeRef| {
62 // TODO: Refactor to be have less repetition
63 visitor()
64 .visit(|node: ast::FnDef| {
65 let mut string = "fn ".to_string();
66 node.name()?.syntax().text().push_to(&mut string);
67 Some(string)
68 })
69 .visit(|node: ast::StructDef| {
70 let mut string = "struct ".to_string();
71 node.name()?.syntax().text().push_to(&mut string);
72 Some(string)
73 })
74 .visit(|node: ast::EnumDef| {
75 let mut string = "enum ".to_string();
76 node.name()?.syntax().text().push_to(&mut string);
77 Some(string)
78 })
79 .visit(|node: ast::TraitDef| {
80 let mut string = "trait ".to_string();
81 node.name()?.syntax().text().push_to(&mut string);
82 Some(string)
83 })
84 .visit(|node: ast::Module| {
85 let mut string = "mod ".to_string();
86 node.name()?.syntax().text().push_to(&mut string);
87 Some(string)
88 })
89 .visit(|node: ast::TypeDef| {
90 let mut string = "type ".to_string();
91 node.name()?.syntax().text().push_to(&mut string);
92 Some(string)
93 })
94 .visit(|node: ast::ConstDef| {
95 let mut string = "const ".to_string();
96 node.name()?.syntax().text().push_to(&mut string);
97 Some(string)
98 })
99 .visit(|node: ast::StaticDef| {
100 let mut string = "static ".to_string();
101 node.name()?.syntax().text().push_to(&mut string);
102 Some(string)
103 })
104 .accept(node)?
105 })
106 .nth(0)
107 }
108}
109
110pub fn file_symbols(file: &SourceFileNode) -> Vec<FileSymbol> {
111 file.syntax().descendants().filter_map(to_symbol).collect()
112}
113
114fn to_symbol(node: SyntaxNodeRef) -> Option<FileSymbol> {
115 fn decl<'a, N: NameOwner<'a>>(node: N) -> Option<FileSymbol> {
116 let name = node.name()?;
117 Some(FileSymbol {
118 name: name.text(),
119 node_range: node.syntax().range(),
120 kind: node.syntax().kind(),
121 })
122 }
123 visitor()
124 .visit(decl::<ast::FnDef>)
125 .visit(decl::<ast::StructDef>)
126 .visit(decl::<ast::EnumDef>)
127 .visit(decl::<ast::TraitDef>)
128 .visit(decl::<ast::Module>)
129 .visit(decl::<ast::TypeDef>)
130 .visit(decl::<ast::ConstDef>)
131 .visit(decl::<ast::StaticDef>)
132 .accept(node)?
133}
134
135pub fn file_structure(file: &SourceFileNode) -> Vec<StructureNode> {
136 let mut res = Vec::new();
137 let mut stack = Vec::new();
138
139 for event in file.syntax().preorder() {
140 match event {
141 WalkEvent::Enter(node) => {
142 if let Some(mut symbol) = structure_node(node) {
143 symbol.parent = stack.last().map(|&n| n);
144 stack.push(res.len());
145 res.push(symbol);
146 }
147 }
148 WalkEvent::Leave(node) => {
149 if structure_node(node).is_some() {
150 stack.pop().unwrap();
151 }
152 }
153 }
154 }
155 res
156}
157
158fn structure_node(node: SyntaxNodeRef) -> Option<StructureNode> {
159 fn decl<'a, N: NameOwner<'a>>(node: N) -> Option<StructureNode> {
160 let name = node.name()?;
161 Some(StructureNode {
162 parent: None,
163 label: name.text().to_string(),
164 navigation_range: name.syntax().range(),
165 node_range: node.syntax().range(),
166 kind: node.syntax().kind(),
167 })
168 }
169
170 visitor()
171 .visit(decl::<ast::FnDef>)
172 .visit(decl::<ast::StructDef>)
173 .visit(decl::<ast::NamedFieldDef>)
174 .visit(decl::<ast::EnumDef>)
175 .visit(decl::<ast::TraitDef>)
176 .visit(decl::<ast::Module>)
177 .visit(decl::<ast::TypeDef>)
178 .visit(decl::<ast::ConstDef>)
179 .visit(decl::<ast::StaticDef>)
180 .visit(|im: ast::ImplItem| {
181 let target_type = im.target_type()?;
182 let target_trait = im.target_trait();
183 let label = match target_trait {
184 None => format!("impl {}", target_type.syntax().text()),
185 Some(t) => format!(
186 "impl {} for {}",
187 t.syntax().text(),
188 target_type.syntax().text(),
189 ),
190 };
191
192 let node = StructureNode {
193 parent: None,
194 label,
195 navigation_range: target_type.syntax().range(),
196 node_range: im.syntax().range(),
197 kind: im.syntax().kind(),
198 };
199 Some(node)
200 })
201 .accept(node)?
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use test_utils::assert_eq_dbg;
208
209 #[test]
210 fn test_file_structure() {
211 let file = SourceFileNode::parse(
212 r#"
213struct Foo {
214 x: i32
215}
216
217mod m {
218 fn bar() {}
219}
220
221enum E { X, Y(i32) }
222type T = ();
223static S: i32 = 92;
224const C: i32 = 92;
225
226impl E {}
227
228impl fmt::Debug for E {}
229"#,
230 );
231 let symbols = file_structure(&file);
232 assert_eq_dbg(
233 r#"[StructureNode { parent: None, label: "Foo", navigation_range: [8; 11), node_range: [1; 26), kind: STRUCT_DEF },
234 StructureNode { parent: Some(0), label: "x", navigation_range: [18; 19), node_range: [18; 24), kind: NAMED_FIELD_DEF },
235 StructureNode { parent: None, label: "m", navigation_range: [32; 33), node_range: [28; 53), kind: MODULE },
236 StructureNode { parent: Some(2), label: "bar", navigation_range: [43; 46), node_range: [40; 51), kind: FN_DEF },
237 StructureNode { parent: None, label: "E", navigation_range: [60; 61), node_range: [55; 75), kind: ENUM_DEF },
238 StructureNode { parent: None, label: "T", navigation_range: [81; 82), node_range: [76; 88), kind: TYPE_DEF },
239 StructureNode { parent: None, label: "S", navigation_range: [96; 97), node_range: [89; 108), kind: STATIC_DEF },
240 StructureNode { parent: None, label: "C", navigation_range: [115; 116), node_range: [109; 127), kind: CONST_DEF },
241 StructureNode { parent: None, label: "impl E", navigation_range: [134; 135), node_range: [129; 138), kind: IMPL_ITEM },
242 StructureNode { parent: None, label: "impl fmt::Debug for E", navigation_range: [160; 161), node_range: [140; 164), kind: IMPL_ITEM }]"#,
243 &symbols,
244 )
245 }
246}
diff --git a/crates/ra_editor/src/typing.rs b/crates/ra_editor/src/typing.rs
index 21d068a7b..dd3d0f260 100644
--- a/crates/ra_editor/src/typing.rs
+++ b/crates/ra_editor/src/typing.rs
@@ -21,7 +21,7 @@ pub fn join_lines(file: &SourceFileNode, range: TextRange) -> LocalEdit {
21 None => { 21 None => {
22 return LocalEdit { 22 return LocalEdit {
23 label: "join lines".to_string(), 23 label: "join lines".to_string(),
24 edit: TextEditBuilder::new().finish(), 24 edit: TextEditBuilder::default().finish(),
25 cursor_position: None, 25 cursor_position: None,
26 }; 26 };
27 } 27 }
@@ -33,7 +33,7 @@ pub fn join_lines(file: &SourceFileNode, range: TextRange) -> LocalEdit {
33 }; 33 };
34 34
35 let node = find_covering_node(file.syntax(), range); 35 let node = find_covering_node(file.syntax(), range);
36 let mut edit = TextEditBuilder::new(); 36 let mut edit = TextEditBuilder::default();
37 for node in node.descendants() { 37 for node in node.descendants() {
38 let text = match node.leaf_text() { 38 let text = match node.leaf_text() {
39 Some(text) => text, 39 Some(text) => text,
@@ -76,7 +76,7 @@ pub fn on_enter(file: &SourceFileNode, offset: TextUnit) -> Option<LocalEdit> {
76 let indent = node_indent(file, comment.syntax())?; 76 let indent = node_indent(file, comment.syntax())?;
77 let inserted = format!("\n{}{} ", indent, prefix); 77 let inserted = format!("\n{}{} ", indent, prefix);
78 let cursor_position = offset + TextUnit::of_str(&inserted); 78 let cursor_position = offset + TextUnit::of_str(&inserted);
79 let mut edit = TextEditBuilder::new(); 79 let mut edit = TextEditBuilder::default();
80 edit.insert(offset, inserted); 80 edit.insert(offset, inserted);
81 Some(LocalEdit { 81 Some(LocalEdit {
82 label: "on enter".to_string(), 82 label: "on enter".to_string(),
@@ -127,7 +127,7 @@ pub fn on_eq_typed(file: &SourceFileNode, offset: TextUnit) -> Option<LocalEdit>
127 return None; 127 return None;
128 } 128 }
129 let offset = let_stmt.syntax().range().end(); 129 let offset = let_stmt.syntax().range().end();
130 let mut edit = TextEditBuilder::new(); 130 let mut edit = TextEditBuilder::default();
131 edit.insert(offset, ";".to_string()); 131 edit.insert(offset, ";".to_string());
132 Some(LocalEdit { 132 Some(LocalEdit {
133 label: "add semicolon".to_string(), 133 label: "add semicolon".to_string(),