aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src
diff options
context:
space:
mode:
authorKirill Bulatov <[email protected]>2019-12-24 00:19:09 +0000
committerKirill Bulatov <[email protected]>2020-01-26 22:16:29 +0000
commit316795e074dff8f627f8c70c85d236420ecfb3a6 (patch)
treeda6e266139563ef314d0563a01723ae2264609d2 /crates/ra_assists/src
parentd1330a4a65f0113c687716a5a679239af4df9c11 (diff)
Initial auto import action implementation
Diffstat (limited to 'crates/ra_assists/src')
-rw-r--r--crates/ra_assists/src/assist_ctx.rs2
-rw-r--r--crates/ra_assists/src/assists/auto_import.rs181
-rw-r--r--crates/ra_assists/src/doc_tests.rs4
-rw-r--r--crates/ra_assists/src/doc_tests/generated.rs19
-rw-r--r--crates/ra_assists/src/lib.rs126
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
170impl ActionBuilder { 169impl 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 @@
1use hir::db::HirDatabase;
2use ra_syntax::{
3 ast::{self, AstNode},
4 SmolStr, SyntaxElement,
5 SyntaxKind::{NAME_REF, USE_ITEM},
6 SyntaxNode,
7};
8
9use 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// ```
31pub(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
65fn 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
80fn 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)]
93mod 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};
11use crate::test_db::TestDB; 11use crate::test_db::TestDB;
12 12
13fn check(assist_id: &str, before: &str, after: &str) { 13fn 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]
218fn doctest_auto_import() {
219 check(
220 "auto_import",
221 r#####"
222fn main() {
223 let map = HashMap<|>::new();
224}
225"#####,
226 r#####"
227use std::collections::HashMap;
228
229fn main() {
230 let map = HashMap<|>::new();
231}
232"#####,
233 )
234}
235
236#[test]
218fn doctest_change_visibility() { 237fn 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;
14pub mod ast_transform; 14pub mod ast_transform;
15 15
16use either::Either; 16use either::Either;
17use hir::db::HirDatabase; 17use hir::{db::HirDatabase, InFile, ModPath, Module};
18use ra_db::FileRange; 18use ra_db::FileRange;
19use ra_syntax::{TextRange, TextUnit}; 19use ra_syntax::{ast::NameRef, TextRange, TextUnit};
20use ra_text_edit::TextEdit; 20use ra_text_edit::TextEdit;
21 21
22pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; 22pub(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.
86pub 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.
100pub fn assists_with_imports_locator<'a, H, F: 'a>(
101 db: &H,
102 range: FileRange,
103 mut imports_locator: F,
104) -> Vec<ResolvedAssist>
105where
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>
85where 134where
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
151fn 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
109mod assists { 161mod 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,