diff options
Diffstat (limited to 'crates/ra_assists/src/lib.rs')
-rw-r--r-- | crates/ra_assists/src/lib.rs | 209 |
1 files changed, 85 insertions, 124 deletions
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 3337805a5..828a8e9e8 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -9,18 +9,16 @@ mod assist_ctx; | |||
9 | mod marks; | 9 | mod marks; |
10 | #[cfg(test)] | 10 | #[cfg(test)] |
11 | mod doc_tests; | 11 | mod doc_tests; |
12 | #[cfg(test)] | 12 | mod utils; |
13 | mod test_db; | ||
14 | pub mod ast_transform; | 13 | pub mod ast_transform; |
15 | 14 | ||
16 | use either::Either; | ||
17 | use hir::db::HirDatabase; | ||
18 | use ra_db::FileRange; | 15 | use ra_db::FileRange; |
16 | use ra_ide_db::RootDatabase; | ||
19 | use ra_syntax::{TextRange, TextUnit}; | 17 | use ra_syntax::{TextRange, TextUnit}; |
20 | use ra_text_edit::TextEdit; | 18 | use ra_text_edit::TextEdit; |
21 | 19 | ||
22 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; | 20 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx, AssistHandler}; |
23 | pub use crate::assists::add_import::auto_import_text_edit; | 21 | pub use crate::handlers::replace_qualified_name_with_use::insert_use_statement; |
24 | 22 | ||
25 | /// Unique identifier of the assist, should not be shown to the user | 23 | /// Unique identifier of the assist, should not be shown to the user |
26 | /// directly. | 24 | /// directly. |
@@ -34,81 +32,64 @@ pub struct AssistLabel { | |||
34 | pub id: AssistId, | 32 | pub id: AssistId, |
35 | } | 33 | } |
36 | 34 | ||
35 | #[derive(Clone, Debug)] | ||
36 | pub struct GroupLabel(pub String); | ||
37 | |||
38 | impl AssistLabel { | ||
39 | pub(crate) fn new(label: String, id: AssistId) -> AssistLabel { | ||
40 | // FIXME: make fields private, so that this invariant can't be broken | ||
41 | assert!(label.chars().nth(0).unwrap().is_uppercase()); | ||
42 | AssistLabel { label: label.into(), id } | ||
43 | } | ||
44 | } | ||
45 | |||
37 | #[derive(Debug, Clone)] | 46 | #[derive(Debug, Clone)] |
38 | pub struct AssistAction { | 47 | pub struct AssistAction { |
39 | pub label: Option<String>, | ||
40 | pub edit: TextEdit, | 48 | pub edit: TextEdit, |
41 | pub cursor_position: Option<TextUnit>, | 49 | pub cursor_position: Option<TextUnit>, |
50 | // FIXME: This belongs to `AssistLabel` | ||
42 | pub target: Option<TextRange>, | 51 | pub target: Option<TextRange>, |
43 | } | 52 | } |
44 | 53 | ||
45 | #[derive(Debug, Clone)] | 54 | #[derive(Debug, Clone)] |
46 | pub struct ResolvedAssist { | 55 | pub struct ResolvedAssist { |
47 | pub label: AssistLabel, | 56 | pub label: AssistLabel, |
48 | pub action_data: Either<AssistAction, Vec<AssistAction>>, | 57 | pub group_label: Option<GroupLabel>, |
49 | } | 58 | pub action: AssistAction, |
50 | |||
51 | impl ResolvedAssist { | ||
52 | pub fn get_first_action(&self) -> AssistAction { | ||
53 | match &self.action_data { | ||
54 | Either::Left(action) => action.clone(), | ||
55 | Either::Right(actions) => actions[0].clone(), | ||
56 | } | ||
57 | } | ||
58 | } | 59 | } |
59 | 60 | ||
60 | /// Return all the assists applicable at the given position. | 61 | /// Return all the assists applicable at the given position. |
61 | /// | 62 | /// |
62 | /// Assists are returned in the "unresolved" state, that is only labels are | 63 | /// Assists are returned in the "unresolved" state, that is only labels are |
63 | /// returned, without actual edits. | 64 | /// returned, without actual edits. |
64 | pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel> | 65 | pub fn unresolved_assists(db: &RootDatabase, range: FileRange) -> Vec<AssistLabel> { |
65 | where | 66 | let ctx = AssistCtx::new(db, range, false); |
66 | H: HirDatabase + 'static, | 67 | handlers::all() |
67 | { | 68 | .iter() |
68 | AssistCtx::with_ctx(db, range, false, |ctx| { | 69 | .filter_map(|f| f(ctx.clone())) |
69 | assists::all() | 70 | .flat_map(|it| it.0) |
70 | .iter() | 71 | .map(|a| a.label) |
71 | .filter_map(|f| f(ctx.clone())) | 72 | .collect() |
72 | .map(|a| match a { | ||
73 | Assist::Unresolved { label } => label, | ||
74 | Assist::Resolved { .. } => unreachable!(), | ||
75 | }) | ||
76 | .collect() | ||
77 | }) | ||
78 | } | 73 | } |
79 | 74 | ||
80 | /// Return all the assists applicable at the given position. | 75 | /// Return all the assists applicable at the given position. |
81 | /// | 76 | /// |
82 | /// Assists are returned in the "resolved" state, that is with edit fully | 77 | /// Assists are returned in the "resolved" state, that is with edit fully |
83 | /// computed. | 78 | /// computed. |
84 | pub fn assists<H>(db: &H, range: FileRange) -> Vec<ResolvedAssist> | 79 | pub fn resolved_assists(db: &RootDatabase, range: FileRange) -> Vec<ResolvedAssist> { |
85 | where | 80 | let ctx = AssistCtx::new(db, range, true); |
86 | H: HirDatabase + 'static, | 81 | let mut a = handlers::all() |
87 | { | 82 | .iter() |
88 | use std::cmp::Ordering; | 83 | .filter_map(|f| f(ctx.clone())) |
89 | 84 | .flat_map(|it| it.0) | |
90 | AssistCtx::with_ctx(db, range, true, |ctx| { | 85 | .map(|it| it.into_resolved().unwrap()) |
91 | let mut a = assists::all() | 86 | .collect::<Vec<_>>(); |
92 | .iter() | 87 | a.sort_by_key(|it| it.action.target.map_or(TextUnit::from(!0u32), |it| it.len())); |
93 | .filter_map(|f| f(ctx.clone())) | 88 | a |
94 | .map(|a| match a { | ||
95 | Assist::Resolved { assist } => assist, | ||
96 | Assist::Unresolved { .. } => unreachable!(), | ||
97 | }) | ||
98 | .collect::<Vec<_>>(); | ||
99 | a.sort_by(|a, b| match (a.get_first_action().target, b.get_first_action().target) { | ||
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 | ||
106 | }) | ||
107 | } | 89 | } |
108 | 90 | ||
109 | mod assists { | 91 | mod handlers { |
110 | use crate::{Assist, AssistCtx}; | 92 | use crate::AssistHandler; |
111 | use hir::db::HirDatabase; | ||
112 | 93 | ||
113 | mod add_derive; | 94 | mod add_derive; |
114 | mod add_explicit_type; | 95 | mod add_explicit_type; |
@@ -116,6 +97,7 @@ mod assists { | |||
116 | mod add_custom_impl; | 97 | mod add_custom_impl; |
117 | mod add_new; | 98 | mod add_new; |
118 | mod apply_demorgan; | 99 | mod apply_demorgan; |
100 | mod auto_import; | ||
119 | mod invert_if; | 101 | mod invert_if; |
120 | mod flip_comma; | 102 | mod flip_comma; |
121 | mod flip_binexpr; | 103 | mod flip_binexpr; |
@@ -129,13 +111,13 @@ mod assists { | |||
129 | mod replace_if_let_with_match; | 111 | mod replace_if_let_with_match; |
130 | mod split_import; | 112 | mod split_import; |
131 | mod remove_dbg; | 113 | mod remove_dbg; |
132 | pub(crate) mod add_import; | 114 | pub(crate) mod replace_qualified_name_with_use; |
133 | mod add_missing_impl_members; | 115 | mod add_missing_impl_members; |
134 | mod move_guard; | 116 | mod move_guard; |
135 | mod move_bounds; | 117 | mod move_bounds; |
136 | mod early_return; | 118 | mod early_return; |
137 | 119 | ||
138 | pub(crate) fn all<DB: HirDatabase>() -> &'static [fn(AssistCtx<DB>) -> Option<Assist>] { | 120 | pub(crate) fn all() -> &'static [AssistHandler] { |
139 | &[ | 121 | &[ |
140 | add_derive::add_derive, | 122 | add_derive::add_derive, |
141 | add_explicit_type::add_explicit_type, | 123 | add_explicit_type::add_explicit_type, |
@@ -154,7 +136,7 @@ mod assists { | |||
154 | replace_if_let_with_match::replace_if_let_with_match, | 136 | replace_if_let_with_match::replace_if_let_with_match, |
155 | split_import::split_import, | 137 | split_import::split_import, |
156 | remove_dbg::remove_dbg, | 138 | remove_dbg::remove_dbg, |
157 | add_import::add_import, | 139 | replace_qualified_name_with_use::replace_qualified_name_with_use, |
158 | add_missing_impl_members::add_missing_impl_members, | 140 | add_missing_impl_members::add_missing_impl_members, |
159 | add_missing_impl_members::add_missing_default_members, | 141 | add_missing_impl_members::add_missing_default_members, |
160 | inline_local_variable::inline_local_variable, | 142 | inline_local_variable::inline_local_variable, |
@@ -166,33 +148,39 @@ mod assists { | |||
166 | raw_string::make_usual_string, | 148 | raw_string::make_usual_string, |
167 | raw_string::remove_hash, | 149 | raw_string::remove_hash, |
168 | early_return::convert_to_guarded_return, | 150 | early_return::convert_to_guarded_return, |
151 | auto_import::auto_import, | ||
169 | ] | 152 | ] |
170 | } | 153 | } |
171 | } | 154 | } |
172 | 155 | ||
173 | #[cfg(test)] | 156 | #[cfg(test)] |
174 | mod helpers { | 157 | mod helpers { |
175 | use ra_db::{fixture::WithFixture, FileRange}; | 158 | use std::sync::Arc; |
159 | |||
160 | use ra_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt}; | ||
161 | use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase}; | ||
176 | use ra_syntax::TextRange; | 162 | use ra_syntax::TextRange; |
177 | use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; | 163 | use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; |
178 | 164 | ||
179 | use crate::{test_db::TestDB, Assist, AssistCtx}; | 165 | use crate::{AssistCtx, AssistHandler}; |
166 | |||
167 | pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { | ||
168 | let (mut db, file_id) = RootDatabase::with_single_file(text); | ||
169 | // FIXME: ideally, this should be done by the above `RootDatabase::with_single_file`, | ||
170 | // but it looks like this might need specialization? :( | ||
171 | let local_roots = vec![db.file_source_root(file_id)]; | ||
172 | db.set_local_roots(Arc::new(local_roots)); | ||
173 | (db, file_id) | ||
174 | } | ||
180 | 175 | ||
181 | pub(crate) fn check_assist( | 176 | pub(crate) fn check_assist(assist: AssistHandler, before: &str, after: &str) { |
182 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | ||
183 | before: &str, | ||
184 | after: &str, | ||
185 | ) { | ||
186 | let (before_cursor_pos, before) = extract_offset(before); | 177 | let (before_cursor_pos, before) = extract_offset(before); |
187 | let (db, file_id) = TestDB::with_single_file(&before); | 178 | let (db, file_id) = with_single_file(&before); |
188 | let frange = | 179 | let frange = |
189 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; | 180 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; |
190 | let assist = | 181 | let assist = |
191 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | 182 | assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); |
192 | let action = match assist { | 183 | let action = assist.0[0].action.clone().unwrap(); |
193 | Assist::Unresolved { .. } => unreachable!(), | ||
194 | Assist::Resolved { assist } => assist.get_first_action(), | ||
195 | }; | ||
196 | 184 | ||
197 | let actual = action.edit.apply(&before); | 185 | let actual = action.edit.apply(&before); |
198 | let actual_cursor_pos = match action.cursor_position { | 186 | let actual_cursor_pos = match action.cursor_position { |
@@ -206,20 +194,13 @@ mod helpers { | |||
206 | assert_eq_text!(after, &actual); | 194 | assert_eq_text!(after, &actual); |
207 | } | 195 | } |
208 | 196 | ||
209 | pub(crate) fn check_assist_range( | 197 | pub(crate) fn check_assist_range(assist: AssistHandler, before: &str, after: &str) { |
210 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | ||
211 | before: &str, | ||
212 | after: &str, | ||
213 | ) { | ||
214 | let (range, before) = extract_range(before); | 198 | let (range, before) = extract_range(before); |
215 | let (db, file_id) = TestDB::with_single_file(&before); | 199 | let (db, file_id) = with_single_file(&before); |
216 | let frange = FileRange { file_id, range }; | 200 | let frange = FileRange { file_id, range }; |
217 | let assist = | 201 | let assist = |
218 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | 202 | assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); |
219 | let action = match assist { | 203 | let action = assist.0[0].action.clone().unwrap(); |
220 | Assist::Unresolved { .. } => unreachable!(), | ||
221 | Assist::Resolved { assist } => assist.get_first_action(), | ||
222 | }; | ||
223 | 204 | ||
224 | let mut actual = action.edit.apply(&before); | 205 | let mut actual = action.edit.apply(&before); |
225 | if let Some(pos) = action.cursor_position { | 206 | if let Some(pos) = action.cursor_position { |
@@ -228,85 +209,65 @@ mod helpers { | |||
228 | assert_eq_text!(after, &actual); | 209 | assert_eq_text!(after, &actual); |
229 | } | 210 | } |
230 | 211 | ||
231 | pub(crate) fn check_assist_target( | 212 | pub(crate) fn check_assist_target(assist: AssistHandler, before: &str, target: &str) { |
232 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | ||
233 | before: &str, | ||
234 | target: &str, | ||
235 | ) { | ||
236 | let (before_cursor_pos, before) = extract_offset(before); | 213 | let (before_cursor_pos, before) = extract_offset(before); |
237 | let (db, file_id) = TestDB::with_single_file(&before); | 214 | let (db, file_id) = with_single_file(&before); |
238 | let frange = | 215 | let frange = |
239 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; | 216 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; |
240 | let assist = | 217 | let assist = |
241 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | 218 | assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); |
242 | let action = match assist { | 219 | let action = assist.0[0].action.clone().unwrap(); |
243 | Assist::Unresolved { .. } => unreachable!(), | ||
244 | Assist::Resolved { assist } => assist.get_first_action(), | ||
245 | }; | ||
246 | 220 | ||
247 | let range = action.target.expect("expected target on action"); | 221 | let range = action.target.expect("expected target on action"); |
248 | assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target); | 222 | assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target); |
249 | } | 223 | } |
250 | 224 | ||
251 | pub(crate) fn check_assist_range_target( | 225 | pub(crate) fn check_assist_range_target(assist: AssistHandler, before: &str, target: &str) { |
252 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | ||
253 | before: &str, | ||
254 | target: &str, | ||
255 | ) { | ||
256 | let (range, before) = extract_range(before); | 226 | let (range, before) = extract_range(before); |
257 | let (db, file_id) = TestDB::with_single_file(&before); | 227 | let (db, file_id) = with_single_file(&before); |
258 | let frange = FileRange { file_id, range }; | 228 | let frange = FileRange { file_id, range }; |
259 | let assist = | 229 | let assist = |
260 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | 230 | assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); |
261 | let action = match assist { | 231 | let action = assist.0[0].action.clone().unwrap(); |
262 | Assist::Unresolved { .. } => unreachable!(), | ||
263 | Assist::Resolved { assist } => assist.get_first_action(), | ||
264 | }; | ||
265 | 232 | ||
266 | let range = action.target.expect("expected target on action"); | 233 | let range = action.target.expect("expected target on action"); |
267 | assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target); | 234 | assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target); |
268 | } | 235 | } |
269 | 236 | ||
270 | pub(crate) fn check_assist_not_applicable( | 237 | pub(crate) fn check_assist_not_applicable(assist: AssistHandler, before: &str) { |
271 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | ||
272 | before: &str, | ||
273 | ) { | ||
274 | let (before_cursor_pos, before) = extract_offset(before); | 238 | let (before_cursor_pos, before) = extract_offset(before); |
275 | let (db, file_id) = TestDB::with_single_file(&before); | 239 | let (db, file_id) = with_single_file(&before); |
276 | let frange = | 240 | let frange = |
277 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; | 241 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; |
278 | let assist = AssistCtx::with_ctx(&db, frange, true, assist); | 242 | let assist = assist(AssistCtx::new(&db, frange, true)); |
279 | assert!(assist.is_none()); | 243 | assert!(assist.is_none()); |
280 | } | 244 | } |
281 | 245 | ||
282 | pub(crate) fn check_assist_range_not_applicable( | 246 | pub(crate) fn check_assist_range_not_applicable(assist: AssistHandler, before: &str) { |
283 | assist: fn(AssistCtx<TestDB>) -> Option<Assist>, | ||
284 | before: &str, | ||
285 | ) { | ||
286 | let (range, before) = extract_range(before); | 247 | let (range, before) = extract_range(before); |
287 | let (db, file_id) = TestDB::with_single_file(&before); | 248 | let (db, file_id) = with_single_file(&before); |
288 | let frange = FileRange { file_id, range }; | 249 | let frange = FileRange { file_id, range }; |
289 | let assist = AssistCtx::with_ctx(&db, frange, true, assist); | 250 | let assist = assist(AssistCtx::new(&db, frange, true)); |
290 | assert!(assist.is_none()); | 251 | assert!(assist.is_none()); |
291 | } | 252 | } |
292 | } | 253 | } |
293 | 254 | ||
294 | #[cfg(test)] | 255 | #[cfg(test)] |
295 | mod tests { | 256 | mod tests { |
296 | use ra_db::{fixture::WithFixture, FileRange}; | 257 | use ra_db::FileRange; |
297 | use ra_syntax::TextRange; | 258 | use ra_syntax::TextRange; |
298 | use test_utils::{extract_offset, extract_range}; | 259 | use test_utils::{extract_offset, extract_range}; |
299 | 260 | ||
300 | use crate::test_db::TestDB; | 261 | use crate::{helpers, resolved_assists}; |
301 | 262 | ||
302 | #[test] | 263 | #[test] |
303 | fn assist_order_field_struct() { | 264 | fn assist_order_field_struct() { |
304 | let before = "struct Foo { <|>bar: u32 }"; | 265 | let before = "struct Foo { <|>bar: u32 }"; |
305 | let (before_cursor_pos, before) = extract_offset(before); | 266 | let (before_cursor_pos, before) = extract_offset(before); |
306 | let (db, file_id) = TestDB::with_single_file(&before); | 267 | let (db, file_id) = helpers::with_single_file(&before); |
307 | let frange = | 268 | let frange = |
308 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; | 269 | FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) }; |
309 | let assists = super::assists(&db, frange); | 270 | let assists = resolved_assists(&db, frange); |
310 | let mut assists = assists.iter(); | 271 | let mut assists = assists.iter(); |
311 | 272 | ||
312 | assert_eq!( | 273 | assert_eq!( |
@@ -327,9 +288,9 @@ mod tests { | |||
327 | } | 288 | } |
328 | }"; | 289 | }"; |
329 | let (range, before) = extract_range(before); | 290 | let (range, before) = extract_range(before); |
330 | let (db, file_id) = TestDB::with_single_file(&before); | 291 | let (db, file_id) = helpers::with_single_file(&before); |
331 | let frange = FileRange { file_id, range }; | 292 | let frange = FileRange { file_id, range }; |
332 | let assists = super::assists(&db, frange); | 293 | let assists = resolved_assists(&db, frange); |
333 | let mut assists = assists.iter(); | 294 | let mut assists = assists.iter(); |
334 | 295 | ||
335 | assert_eq!(assists.next().expect("expected assist").label.label, "Extract into variable"); | 296 | assert_eq!(assists.next().expect("expected assist").label.label, "Extract into variable"); |