diff options
Diffstat (limited to 'crates/ra_assists')
-rw-r--r-- | crates/ra_assists/src/assist_ctx.rs | 2 | ||||
-rw-r--r-- | crates/ra_assists/src/assists/auto_import.rs | 181 | ||||
-rw-r--r-- | crates/ra_assists/src/doc_tests.rs | 4 | ||||
-rw-r--r-- | crates/ra_assists/src/doc_tests/generated.rs | 19 | ||||
-rw-r--r-- | crates/ra_assists/src/lib.rs | 126 |
5 files changed, 317 insertions, 15 deletions
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index 43f0d664b..2ab65ab99 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs | |||
@@ -101,7 +101,6 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> { | |||
101 | Some(assist) | 101 | Some(assist) |
102 | } | 102 | } |
103 | 103 | ||
104 | #[allow(dead_code)] // will be used for auto import assist with multiple actions | ||
105 | pub(crate) fn add_assist_group( | 104 | pub(crate) fn add_assist_group( |
106 | self, | 105 | self, |
107 | id: AssistId, | 106 | id: AssistId, |
@@ -168,7 +167,6 @@ pub(crate) struct ActionBuilder { | |||
168 | } | 167 | } |
169 | 168 | ||
170 | impl ActionBuilder { | 169 | impl ActionBuilder { |
171 | #[allow(dead_code)] | ||
172 | /// Adds a custom label to the action, if it needs to be different from the assist label | 170 | /// Adds a custom label to the action, if it needs to be different from the assist label |
173 | pub(crate) fn label(&mut self, label: impl Into<String>) { | 171 | pub(crate) fn label(&mut self, label: impl Into<String>) { |
174 | self.label = Some(label.into()) | 172 | self.label = Some(label.into()) |
diff --git a/crates/ra_assists/src/assists/auto_import.rs b/crates/ra_assists/src/assists/auto_import.rs new file mode 100644 index 000000000..fe226521e --- /dev/null +++ b/crates/ra_assists/src/assists/auto_import.rs | |||
@@ -0,0 +1,181 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | ||
3 | ast::{self, AstNode}, | ||
4 | SmolStr, SyntaxElement, | ||
5 | SyntaxKind::{NAME_REF, USE_ITEM}, | ||
6 | SyntaxNode, | ||
7 | }; | ||
8 | |||
9 | use crate::{ | ||
10 | assist_ctx::{ActionBuilder, Assist, AssistCtx}, | ||
11 | auto_import_text_edit, AssistId, ImportsLocator, | ||
12 | }; | ||
13 | |||
14 | // Assist: auto_import | ||
15 | // | ||
16 | // If the name is unresolved, provides all possible imports for it. | ||
17 | // | ||
18 | // ``` | ||
19 | // fn main() { | ||
20 | // let map = HashMap<|>::new(); | ||
21 | // } | ||
22 | // ``` | ||
23 | // -> | ||
24 | // ``` | ||
25 | // use std::collections::HashMap; | ||
26 | // | ||
27 | // fn main() { | ||
28 | // let map = HashMap<|>::new(); | ||
29 | // } | ||
30 | // ``` | ||
31 | pub(crate) fn auto_import<'a, F: ImportsLocator<'a>>( | ||
32 | ctx: AssistCtx<impl HirDatabase>, | ||
33 | imports_locator: &mut F, | ||
34 | ) -> Option<Assist> { | ||
35 | let path: ast::Path = ctx.find_node_at_offset()?; | ||
36 | let module = path.syntax().ancestors().find_map(ast::Module::cast); | ||
37 | let position = match module.and_then(|it| it.item_list()) { | ||
38 | Some(item_list) => item_list.syntax().clone(), | ||
39 | None => { | ||
40 | let current_file = path.syntax().ancestors().find_map(ast::SourceFile::cast)?; | ||
41 | current_file.syntax().clone() | ||
42 | } | ||
43 | }; | ||
44 | |||
45 | let module_with_name_to_import = ctx.source_analyzer(&position, None).module()?; | ||
46 | let name_to_import = hir::InFile { | ||
47 | file_id: ctx.frange.file_id.into(), | ||
48 | value: &find_applicable_name_ref(ctx.covering_element())?, | ||
49 | }; | ||
50 | |||
51 | let proposed_imports = | ||
52 | imports_locator.find_imports(name_to_import, module_with_name_to_import)?; | ||
53 | if proposed_imports.is_empty() { | ||
54 | return None; | ||
55 | } | ||
56 | |||
57 | ctx.add_assist_group(AssistId("auto_import"), "auto import", || { | ||
58 | proposed_imports | ||
59 | .into_iter() | ||
60 | .map(|import| import_to_action(import.to_string(), &position, &path)) | ||
61 | .collect() | ||
62 | }) | ||
63 | } | ||
64 | |||
65 | fn find_applicable_name_ref(element: SyntaxElement) -> Option<ast::NameRef> { | ||
66 | if element.ancestors().find(|ancestor| ancestor.kind() == USE_ITEM).is_some() { | ||
67 | None | ||
68 | } else if element.kind() == NAME_REF { | ||
69 | Some(element.as_node().cloned().and_then(ast::NameRef::cast)?) | ||
70 | } else { | ||
71 | let parent = element.parent()?; | ||
72 | if parent.kind() == NAME_REF { | ||
73 | Some(ast::NameRef::cast(parent)?) | ||
74 | } else { | ||
75 | None | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | fn import_to_action(import: String, position: &SyntaxNode, path: &ast::Path) -> ActionBuilder { | ||
81 | let mut action_builder = ActionBuilder::default(); | ||
82 | action_builder.label(format!("Import `{}`", &import)); | ||
83 | auto_import_text_edit( | ||
84 | position, | ||
85 | &path.syntax().clone(), | ||
86 | &[SmolStr::new(import)], | ||
87 | action_builder.text_edit_builder(), | ||
88 | ); | ||
89 | action_builder | ||
90 | } | ||
91 | |||
92 | #[cfg(test)] | ||
93 | mod tests { | ||
94 | use super::*; | ||
95 | use crate::helpers::{ | ||
96 | check_assist_with_imports_locator, check_assist_with_imports_locator_not_applicable, | ||
97 | }; | ||
98 | use hir::Name; | ||
99 | |||
100 | #[derive(Clone)] | ||
101 | struct TestImportsLocator<'a> { | ||
102 | import_path: &'a [Name], | ||
103 | } | ||
104 | |||
105 | impl<'a> TestImportsLocator<'a> { | ||
106 | fn new(import_path: &'a [Name]) -> Self { | ||
107 | TestImportsLocator { import_path } | ||
108 | } | ||
109 | } | ||
110 | |||
111 | impl<'a> ImportsLocator<'_> for TestImportsLocator<'_> { | ||
112 | fn find_imports( | ||
113 | &mut self, | ||
114 | _: hir::InFile<&ast::NameRef>, | ||
115 | _: hir::Module, | ||
116 | ) -> Option<Vec<hir::ModPath>> { | ||
117 | if self.import_path.is_empty() { | ||
118 | None | ||
119 | } else { | ||
120 | Some(vec![hir::ModPath { | ||
121 | kind: hir::PathKind::Plain, | ||
122 | segments: self.import_path.to_owned(), | ||
123 | }]) | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | |||
128 | #[test] | ||
129 | fn applicable_when_found_an_import() { | ||
130 | let import_path = &[hir::name::known::std, hir::name::known::ops, hir::name::known::Debug]; | ||
131 | let mut imports_locator = TestImportsLocator::new(import_path); | ||
132 | check_assist_with_imports_locator( | ||
133 | auto_import, | ||
134 | &mut imports_locator, | ||
135 | " | ||
136 | fn main() { | ||
137 | } | ||
138 | |||
139 | Debug<|>", | ||
140 | &format!( | ||
141 | " | ||
142 | use {}; | ||
143 | |||
144 | fn main() {{ | ||
145 | }} | ||
146 | |||
147 | Debug<|>", | ||
148 | import_path | ||
149 | .into_iter() | ||
150 | .map(|name| name.to_string()) | ||
151 | .collect::<Vec<String>>() | ||
152 | .join("::") | ||
153 | ), | ||
154 | ); | ||
155 | } | ||
156 | |||
157 | #[test] | ||
158 | fn not_applicable_when_no_imports_found() { | ||
159 | let mut imports_locator = TestImportsLocator::new(&[]); | ||
160 | check_assist_with_imports_locator_not_applicable( | ||
161 | auto_import, | ||
162 | &mut imports_locator, | ||
163 | " | ||
164 | fn main() { | ||
165 | } | ||
166 | |||
167 | Debug<|>", | ||
168 | ); | ||
169 | } | ||
170 | |||
171 | #[test] | ||
172 | fn not_applicable_in_import_statements() { | ||
173 | let import_path = &[hir::name::known::std, hir::name::known::ops, hir::name::known::Debug]; | ||
174 | let mut imports_locator = TestImportsLocator::new(import_path); | ||
175 | check_assist_with_imports_locator_not_applicable( | ||
176 | auto_import, | ||
177 | &mut imports_locator, | ||
178 | "use Debug<|>;", | ||
179 | ); | ||
180 | } | ||
181 | } | ||
diff --git a/crates/ra_assists/src/doc_tests.rs b/crates/ra_assists/src/doc_tests.rs index 5dc1ee233..65d51428b 100644 --- a/crates/ra_assists/src/doc_tests.rs +++ b/crates/ra_assists/src/doc_tests.rs | |||
@@ -11,6 +11,10 @@ use test_utils::{assert_eq_text, extract_range_or_offset}; | |||
11 | use crate::test_db::TestDB; | 11 | use crate::test_db::TestDB; |
12 | 12 | ||
13 | fn check(assist_id: &str, before: &str, after: &str) { | 13 | fn check(assist_id: &str, before: &str, after: &str) { |
14 | // FIXME we cannot get the imports search functionality here yet, but still need to generate a test and a doc for an assist | ||
15 | if assist_id == "auto_import" { | ||
16 | return; | ||
17 | } | ||
14 | let (selection, before) = extract_range_or_offset(before); | 18 | let (selection, before) = extract_range_or_offset(before); |
15 | let (db, file_id) = TestDB::with_single_file(&before); | 19 | let (db, file_id) = TestDB::with_single_file(&before); |
16 | let frange = FileRange { file_id, range: selection.into() }; | 20 | let frange = FileRange { file_id, range: selection.into() }; |
diff --git a/crates/ra_assists/src/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs index 7d84dc8fb..ec4587ce7 100644 --- a/crates/ra_assists/src/doc_tests/generated.rs +++ b/crates/ra_assists/src/doc_tests/generated.rs | |||
@@ -215,6 +215,25 @@ fn main() { | |||
215 | } | 215 | } |
216 | 216 | ||
217 | #[test] | 217 | #[test] |
218 | fn doctest_auto_import() { | ||
219 | check( | ||
220 | "auto_import", | ||
221 | r#####" | ||
222 | fn main() { | ||
223 | let map = HashMap<|>::new(); | ||
224 | } | ||
225 | "#####, | ||
226 | r#####" | ||
227 | use std::collections::HashMap; | ||
228 | |||
229 | fn main() { | ||
230 | let map = HashMap<|>::new(); | ||
231 | } | ||
232 | "#####, | ||
233 | ) | ||
234 | } | ||
235 | |||
236 | #[test] | ||
218 | fn doctest_change_visibility() { | 237 | fn doctest_change_visibility() { |
219 | check( | 238 | check( |
220 | "change_visibility", | 239 | "change_visibility", |
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 3337805a5..4029962f7 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -14,9 +14,9 @@ mod test_db; | |||
14 | pub mod ast_transform; | 14 | pub mod ast_transform; |
15 | 15 | ||
16 | use either::Either; | 16 | use either::Either; |
17 | use hir::db::HirDatabase; | 17 | use hir::{db::HirDatabase, InFile, ModPath, Module}; |
18 | use ra_db::FileRange; | 18 | use ra_db::FileRange; |
19 | use ra_syntax::{TextRange, TextUnit}; | 19 | use ra_syntax::{ast::NameRef, TextRange, TextUnit}; |
20 | use ra_text_edit::TextEdit; | 20 | use ra_text_edit::TextEdit; |
21 | 21 | ||
22 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; | 22 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; |
@@ -77,6 +77,55 @@ where | |||
77 | }) | 77 | }) |
78 | } | 78 | } |
79 | 79 | ||
80 | /// A functionality for locating imports for the given name. | ||
81 | /// | ||
82 | /// Currently has to be a trait with the real implementation provided by the ra_ide_api crate, | ||
83 | /// due to the search functionality located there. | ||
84 | /// Later, this trait should be removed completely and the search functionality moved to a separate crate, | ||
85 | /// accessible from the ra_assists crate. | ||
86 | pub trait ImportsLocator<'a> { | ||
87 | /// Finds all imports for the given name and the module that contains this name. | ||
88 | fn find_imports( | ||
89 | &mut self, | ||
90 | name_to_import: InFile<&NameRef>, | ||
91 | module_with_name_to_import: Module, | ||
92 | ) -> Option<Vec<ModPath>>; | ||
93 | } | ||
94 | |||
95 | /// Return all the assists applicable at the given position | ||
96 | /// and additional assists that need the imports locator functionality to work. | ||
97 | /// | ||
98 | /// Assists are returned in the "resolved" state, that is with edit fully | ||
99 | /// computed. | ||
100 | pub fn assists_with_imports_locator<'a, H, F: 'a>( | ||
101 | db: &H, | ||
102 | range: FileRange, | ||
103 | mut imports_locator: F, | ||
104 | ) -> Vec<ResolvedAssist> | ||
105 | where | ||
106 | H: HirDatabase + 'static, | ||
107 | F: ImportsLocator<'a>, | ||
108 | { | ||
109 | AssistCtx::with_ctx(db, range, true, |ctx| { | ||
110 | let mut assists = assists::all() | ||
111 | .iter() | ||
112 | .map(|f| f(ctx.clone())) | ||
113 | .chain( | ||
114 | assists::all_with_imports_locator() | ||
115 | .iter() | ||
116 | .map(|f| f(ctx.clone(), &mut imports_locator)), | ||
117 | ) | ||
118 | .filter_map(std::convert::identity) | ||
119 | .map(|a| match a { | ||
120 | Assist::Resolved { assist } => assist, | ||
121 | Assist::Unresolved { .. } => unreachable!(), | ||
122 | }) | ||
123 | .collect(); | ||
124 | sort_assists(&mut assists); | ||
125 | assists | ||
126 | }) | ||
127 | } | ||
128 | |||
80 | /// Return all the assists applicable at the given position. | 129 | /// Return all the assists applicable at the given position. |
81 | /// | 130 | /// |
82 | /// Assists are returned in the "resolved" state, that is with edit fully | 131 | /// Assists are returned in the "resolved" state, that is with edit fully |
@@ -85,8 +134,6 @@ pub fn assists<H>(db: &H, range: FileRange) -> Vec<ResolvedAssist> | |||
85 | where | 134 | where |
86 | H: HirDatabase + 'static, | 135 | H: HirDatabase + 'static, |
87 | { | 136 | { |
88 | use std::cmp::Ordering; | ||
89 | |||
90 | AssistCtx::with_ctx(db, range, true, |ctx| { | 137 | AssistCtx::with_ctx(db, range, true, |ctx| { |
91 | let mut a = assists::all() | 138 | let mut a = assists::all() |
92 | .iter() | 139 | .iter() |
@@ -95,19 +142,24 @@ where | |||
95 | Assist::Resolved { assist } => assist, | 142 | Assist::Resolved { assist } => assist, |
96 | Assist::Unresolved { .. } => unreachable!(), | 143 | Assist::Unresolved { .. } => unreachable!(), |
97 | }) | 144 | }) |
98 | .collect::<Vec<_>>(); | 145 | .collect(); |
99 | a.sort_by(|a, b| match (a.get_first_action().target, b.get_first_action().target) { | 146 | sort_assists(&mut a); |
100 | (Some(a), Some(b)) => a.len().cmp(&b.len()), | ||
101 | (Some(_), None) => Ordering::Less, | ||
102 | (None, Some(_)) => Ordering::Greater, | ||
103 | (None, None) => Ordering::Equal, | ||
104 | }); | ||
105 | a | 147 | a |
106 | }) | 148 | }) |
107 | } | 149 | } |
108 | 150 | ||
151 | fn sort_assists(assists: &mut Vec<ResolvedAssist>) { | ||
152 | use std::cmp::Ordering; | ||
153 | assists.sort_by(|a, b| match (a.get_first_action().target, b.get_first_action().target) { | ||
154 | (Some(a), Some(b)) => a.len().cmp(&b.len()), | ||
155 | (Some(_), None) => Ordering::Less, | ||
156 | (None, Some(_)) => Ordering::Greater, | ||
157 | (None, None) => Ordering::Equal, | ||
158 | }); | ||
159 | } | ||
160 | |||
109 | mod assists { | 161 | mod assists { |
110 | use crate::{Assist, AssistCtx}; | 162 | use crate::{Assist, AssistCtx, ImportsLocator}; |
111 | use hir::db::HirDatabase; | 163 | use hir::db::HirDatabase; |
112 | 164 | ||
113 | mod add_derive; | 165 | mod add_derive; |
@@ -116,6 +168,7 @@ mod assists { | |||
116 | mod add_custom_impl; | 168 | mod add_custom_impl; |
117 | mod add_new; | 169 | mod add_new; |
118 | mod apply_demorgan; | 170 | mod apply_demorgan; |
171 | mod auto_import; | ||
119 | mod invert_if; | 172 | mod invert_if; |
120 | mod flip_comma; | 173 | mod flip_comma; |
121 | mod flip_binexpr; | 174 | mod flip_binexpr; |
@@ -168,6 +221,11 @@ mod assists { | |||
168 | early_return::convert_to_guarded_return, | 221 | early_return::convert_to_guarded_return, |
169 | ] | 222 | ] |
170 | } | 223 | } |
224 | |||
225 | pub(crate) fn all_with_imports_locator<'a, DB: HirDatabase, F: ImportsLocator<'a>>( | ||
226 | ) -> &'a [fn(AssistCtx<DB>, &mut F) -> Option<Assist>] { | ||
227 | &[auto_import::auto_import] | ||
228 | } | ||
171 | } | 229 | } |
172 | 230 | ||
173 | #[cfg(test)] | 231 | #[cfg(test)] |
@@ -176,7 +234,7 @@ mod helpers { | |||
176 | use ra_syntax::TextRange; | 234 | use ra_syntax::TextRange; |
177 | use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; | 235 | use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; |
178 | 236 | ||
179 | use crate::{test_db::TestDB, Assist, AssistCtx}; | 237 | use crate::{test_db::TestDB, Assist, AssistCtx, ImportsLocator}; |
180 | 238 | ||
181 | pub(crate) fn check_assist( | 239 | pub(crate) fn check_assist( |
182 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | 240 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, |
@@ -206,6 +264,35 @@ mod helpers { | |||
206 | assert_eq_text!(after, &actual); | 264 | assert_eq_text!(after, &actual); |
207 | } | 265 | } |
208 | 266 | ||
267 | pub(crate) fn check_assist_with_imports_locator<'a, F: ImportsLocator<'a>>( | ||
268 | assist: fn(AssistCtx<TestDB>, &mut F) -> Option<Assist>, | ||
269 | imports_locator: &mut F, | ||
270 | before: &str, | ||
271 | after: &str, | ||
272 | ) { | ||
273 | let (before_cursor_pos, before) = extract_offset(before); | ||
274 | let (db, file_id) = TestDB::with_single_file(&before); | ||
275 | let frange = | ||
276 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; | ||
277 | let assist = AssistCtx::with_ctx(&db, frange, true, |ctx| assist(ctx, imports_locator)) | ||
278 | .expect("code action is not applicable"); | ||
279 | let action = match assist { | ||
280 | Assist::Unresolved { .. } => unreachable!(), | ||
281 | Assist::Resolved { assist } => assist.get_first_action(), | ||
282 | }; | ||
283 | |||
284 | let actual = action.edit.apply(&before); | ||
285 | let actual_cursor_pos = match action.cursor_position { | ||
286 | None => action | ||
287 | .edit | ||
288 | .apply_to_offset(before_cursor_pos) | ||
289 | .expect("cursor position is affected by the edit"), | ||
290 | Some(off) => off, | ||
291 | }; | ||
292 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
293 | assert_eq_text!(after, &actual); | ||
294 | } | ||
295 | |||
209 | pub(crate) fn check_assist_range( | 296 | pub(crate) fn check_assist_range( |
210 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | 297 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, |
211 | before: &str, | 298 | before: &str, |
@@ -279,6 +366,19 @@ mod helpers { | |||
279 | assert!(assist.is_none()); | 366 | assert!(assist.is_none()); |
280 | } | 367 | } |
281 | 368 | ||
369 | pub(crate) fn check_assist_with_imports_locator_not_applicable<'a, F: ImportsLocator<'a>>( | ||
370 | assist: fn(AssistCtx<TestDB>, &mut F) -> Option<Assist>, | ||
371 | imports_locator: &mut F, | ||
372 | before: &str, | ||
373 | ) { | ||
374 | let (before_cursor_pos, before) = extract_offset(before); | ||
375 | let (db, file_id) = TestDB::with_single_file(&before); | ||
376 | let frange = | ||
377 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; | ||
378 | let assist = AssistCtx::with_ctx(&db, frange, true, |ctx| assist(ctx, imports_locator)); | ||
379 | assert!(assist.is_none()); | ||
380 | } | ||
381 | |||
282 | pub(crate) fn check_assist_range_not_applicable( | 382 | pub(crate) fn check_assist_range_not_applicable( |
283 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | 383 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, |
284 | before: &str, | 384 | before: &str, |