diff options
Diffstat (limited to 'crates/ra_assists/src/lib.rs')
-rw-r--r-- | crates/ra_assists/src/lib.rs | 307 |
1 files changed, 78 insertions, 229 deletions
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 64bd87afb..464bc03dd 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -10,119 +10,113 @@ macro_rules! eprintln { | |||
10 | ($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; | 10 | ($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; |
11 | } | 11 | } |
12 | 12 | ||
13 | mod assist_ctx; | 13 | mod assist_config; |
14 | mod marks; | 14 | mod assist_context; |
15 | #[cfg(test)] | 15 | #[cfg(test)] |
16 | mod doc_tests; | 16 | mod tests; |
17 | pub mod utils; | 17 | pub mod utils; |
18 | pub mod ast_transform; | 18 | pub mod ast_transform; |
19 | 19 | ||
20 | use ra_db::{FileId, FileRange}; | ||
21 | use ra_ide_db::RootDatabase; | ||
22 | use ra_syntax::{TextRange, TextSize}; | ||
23 | use ra_text_edit::TextEdit; | ||
24 | |||
25 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx, AssistHandler}; | ||
26 | use hir::Semantics; | 20 | use hir::Semantics; |
21 | use ra_db::FileRange; | ||
22 | use ra_ide_db::{source_change::SourceChange, RootDatabase}; | ||
23 | use ra_syntax::TextRange; | ||
24 | |||
25 | pub(crate) use crate::assist_context::{AssistContext, Assists}; | ||
26 | |||
27 | pub use assist_config::AssistConfig; | ||
27 | 28 | ||
28 | /// Unique identifier of the assist, should not be shown to the user | 29 | /// Unique identifier of the assist, should not be shown to the user |
29 | /// directly. | 30 | /// directly. |
30 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 31 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
31 | pub struct AssistId(pub &'static str); | 32 | pub struct AssistId(pub &'static str); |
32 | 33 | ||
33 | #[derive(Debug, Clone)] | ||
34 | pub struct AssistLabel { | ||
35 | /// Short description of the assist, as shown in the UI. | ||
36 | pub label: String, | ||
37 | pub id: AssistId, | ||
38 | } | ||
39 | |||
40 | #[derive(Clone, Debug)] | 34 | #[derive(Clone, Debug)] |
41 | pub struct GroupLabel(pub String); | 35 | pub struct GroupLabel(pub String); |
42 | 36 | ||
43 | impl AssistLabel { | ||
44 | pub(crate) fn new(label: String, id: AssistId) -> AssistLabel { | ||
45 | // FIXME: make fields private, so that this invariant can't be broken | ||
46 | assert!(label.starts_with(|c: char| c.is_uppercase())); | ||
47 | AssistLabel { label, id } | ||
48 | } | ||
49 | } | ||
50 | |||
51 | #[derive(Debug, Clone)] | 37 | #[derive(Debug, Clone)] |
52 | pub struct AssistAction { | 38 | pub struct Assist { |
53 | pub edit: TextEdit, | 39 | pub id: AssistId, |
54 | pub cursor_position: Option<TextSize>, | 40 | /// Short description of the assist, as shown in the UI. |
55 | // FIXME: This belongs to `AssistLabel` | 41 | pub label: String, |
56 | pub target: Option<TextRange>, | 42 | pub group: Option<GroupLabel>, |
57 | pub file: AssistFile, | 43 | /// Target ranges are used to sort assists: the smaller the target range, |
44 | /// the more specific assist is, and so it should be sorted first. | ||
45 | pub target: TextRange, | ||
58 | } | 46 | } |
59 | 47 | ||
60 | #[derive(Debug, Clone)] | 48 | #[derive(Debug, Clone)] |
61 | pub struct ResolvedAssist { | 49 | pub struct ResolvedAssist { |
62 | pub label: AssistLabel, | 50 | pub assist: Assist, |
63 | pub group_label: Option<GroupLabel>, | 51 | pub source_change: SourceChange, |
64 | pub action: AssistAction, | ||
65 | } | 52 | } |
66 | 53 | ||
67 | #[derive(Debug, Clone, Copy)] | 54 | impl Assist { |
68 | pub enum AssistFile { | 55 | /// Return all the assists applicable at the given position. |
69 | CurrentFile, | 56 | /// |
70 | TargetFile(FileId), | 57 | /// Assists are returned in the "unresolved" state, that is only labels are |
71 | } | 58 | /// returned, without actual edits. |
72 | 59 | pub fn unresolved(db: &RootDatabase, config: &AssistConfig, range: FileRange) -> Vec<Assist> { | |
73 | impl Default for AssistFile { | 60 | let sema = Semantics::new(db); |
74 | fn default() -> Self { | 61 | let ctx = AssistContext::new(sema, config, range); |
75 | Self::CurrentFile | 62 | let mut acc = Assists::new_unresolved(&ctx); |
63 | handlers::all().iter().for_each(|handler| { | ||
64 | handler(&mut acc, &ctx); | ||
65 | }); | ||
66 | acc.finish_unresolved() | ||
76 | } | 67 | } |
77 | } | ||
78 | 68 | ||
79 | /// Return all the assists applicable at the given position. | 69 | /// Return all the assists applicable at the given position. |
80 | /// | 70 | /// |
81 | /// Assists are returned in the "unresolved" state, that is only labels are | 71 | /// Assists are returned in the "resolved" state, that is with edit fully |
82 | /// returned, without actual edits. | 72 | /// computed. |
83 | pub fn unresolved_assists(db: &RootDatabase, range: FileRange) -> Vec<AssistLabel> { | 73 | pub fn resolved( |
84 | let sema = Semantics::new(db); | 74 | db: &RootDatabase, |
85 | let ctx = AssistCtx::new(&sema, range, false); | 75 | config: &AssistConfig, |
86 | handlers::all() | 76 | range: FileRange, |
87 | .iter() | 77 | ) -> Vec<ResolvedAssist> { |
88 | .filter_map(|f| f(ctx.clone())) | 78 | let sema = Semantics::new(db); |
89 | .flat_map(|it| it.0) | 79 | let ctx = AssistContext::new(sema, config, range); |
90 | .map(|a| a.label) | 80 | let mut acc = Assists::new_resolved(&ctx); |
91 | .collect() | 81 | handlers::all().iter().for_each(|handler| { |
92 | } | 82 | handler(&mut acc, &ctx); |
83 | }); | ||
84 | acc.finish_resolved() | ||
85 | } | ||
93 | 86 | ||
94 | /// Return all the assists applicable at the given position. | 87 | pub(crate) fn new( |
95 | /// | 88 | id: AssistId, |
96 | /// Assists are returned in the "resolved" state, that is with edit fully | 89 | label: String, |
97 | /// computed. | 90 | group: Option<GroupLabel>, |
98 | pub fn resolved_assists(db: &RootDatabase, range: FileRange) -> Vec<ResolvedAssist> { | 91 | target: TextRange, |
99 | let sema = Semantics::new(db); | 92 | ) -> Assist { |
100 | let ctx = AssistCtx::new(&sema, range, true); | 93 | // FIXME: make fields private, so that this invariant can't be broken |
101 | let mut a = handlers::all() | 94 | assert!(label.starts_with(|c: char| c.is_uppercase())); |
102 | .iter() | 95 | Assist { id, label, group, target } |
103 | .filter_map(|f| f(ctx.clone())) | 96 | } |
104 | .flat_map(|it| it.0) | ||
105 | .map(|it| it.into_resolved().unwrap()) | ||
106 | .collect::<Vec<_>>(); | ||
107 | a.sort_by_key(|it| it.action.target.map_or(TextSize::from(!0u32), |it| it.len())); | ||
108 | a | ||
109 | } | 97 | } |
110 | 98 | ||
111 | mod handlers { | 99 | mod handlers { |
112 | use crate::AssistHandler; | 100 | use crate::{AssistContext, Assists}; |
101 | |||
102 | pub(crate) type Handler = fn(&mut Assists, &AssistContext) -> Option<()>; | ||
113 | 103 | ||
114 | mod add_custom_impl; | 104 | mod add_custom_impl; |
115 | mod add_derive; | 105 | mod add_derive; |
116 | mod add_explicit_type; | 106 | mod add_explicit_type; |
107 | mod add_from_impl_for_enum; | ||
117 | mod add_function; | 108 | mod add_function; |
118 | mod add_impl; | 109 | mod add_impl; |
119 | mod add_missing_impl_members; | 110 | mod add_missing_impl_members; |
120 | mod add_new; | 111 | mod add_new; |
112 | mod add_turbo_fish; | ||
121 | mod apply_demorgan; | 113 | mod apply_demorgan; |
122 | mod auto_import; | 114 | mod auto_import; |
115 | mod change_return_type_to_result; | ||
123 | mod change_visibility; | 116 | mod change_visibility; |
124 | mod early_return; | 117 | mod early_return; |
125 | mod fill_match_arms; | 118 | mod fill_match_arms; |
119 | mod fix_visibility; | ||
126 | mod flip_binexpr; | 120 | mod flip_binexpr; |
127 | mod flip_comma; | 121 | mod flip_comma; |
128 | mod flip_trait_bound; | 122 | mod flip_trait_bound; |
@@ -136,28 +130,32 @@ mod handlers { | |||
136 | mod raw_string; | 130 | mod raw_string; |
137 | mod remove_dbg; | 131 | mod remove_dbg; |
138 | mod remove_mut; | 132 | mod remove_mut; |
133 | mod reorder_fields; | ||
139 | mod replace_if_let_with_match; | 134 | mod replace_if_let_with_match; |
140 | mod replace_let_with_if_let; | 135 | mod replace_let_with_if_let; |
141 | mod replace_qualified_name_with_use; | 136 | mod replace_qualified_name_with_use; |
142 | mod replace_unwrap_with_match; | 137 | mod replace_unwrap_with_match; |
143 | mod split_import; | 138 | mod split_import; |
144 | mod add_from_impl_for_enum; | 139 | mod unwrap_block; |
145 | mod reorder_fields; | ||
146 | 140 | ||
147 | pub(crate) fn all() -> &'static [AssistHandler] { | 141 | pub(crate) fn all() -> &'static [Handler] { |
148 | &[ | 142 | &[ |
149 | // These are alphabetic for the foolish consistency | 143 | // These are alphabetic for the foolish consistency |
150 | add_custom_impl::add_custom_impl, | 144 | add_custom_impl::add_custom_impl, |
151 | add_derive::add_derive, | 145 | add_derive::add_derive, |
152 | add_explicit_type::add_explicit_type, | 146 | add_explicit_type::add_explicit_type, |
147 | add_from_impl_for_enum::add_from_impl_for_enum, | ||
153 | add_function::add_function, | 148 | add_function::add_function, |
154 | add_impl::add_impl, | 149 | add_impl::add_impl, |
155 | add_new::add_new, | 150 | add_new::add_new, |
151 | add_turbo_fish::add_turbo_fish, | ||
156 | apply_demorgan::apply_demorgan, | 152 | apply_demorgan::apply_demorgan, |
157 | auto_import::auto_import, | 153 | auto_import::auto_import, |
154 | change_return_type_to_result::change_return_type_to_result, | ||
158 | change_visibility::change_visibility, | 155 | change_visibility::change_visibility, |
159 | early_return::convert_to_guarded_return, | 156 | early_return::convert_to_guarded_return, |
160 | fill_match_arms::fill_match_arms, | 157 | fill_match_arms::fill_match_arms, |
158 | fix_visibility::fix_visibility, | ||
161 | flip_binexpr::flip_binexpr, | 159 | flip_binexpr::flip_binexpr, |
162 | flip_comma::flip_comma, | 160 | flip_comma::flip_comma, |
163 | flip_trait_bound::flip_trait_bound, | 161 | flip_trait_bound::flip_trait_bound, |
@@ -175,167 +173,18 @@ mod handlers { | |||
175 | raw_string::remove_hash, | 173 | raw_string::remove_hash, |
176 | remove_dbg::remove_dbg, | 174 | remove_dbg::remove_dbg, |
177 | remove_mut::remove_mut, | 175 | remove_mut::remove_mut, |
176 | reorder_fields::reorder_fields, | ||
178 | replace_if_let_with_match::replace_if_let_with_match, | 177 | replace_if_let_with_match::replace_if_let_with_match, |
179 | replace_let_with_if_let::replace_let_with_if_let, | 178 | replace_let_with_if_let::replace_let_with_if_let, |
180 | replace_qualified_name_with_use::replace_qualified_name_with_use, | 179 | replace_qualified_name_with_use::replace_qualified_name_with_use, |
181 | replace_unwrap_with_match::replace_unwrap_with_match, | 180 | replace_unwrap_with_match::replace_unwrap_with_match, |
182 | split_import::split_import, | 181 | split_import::split_import, |
183 | add_from_impl_for_enum::add_from_impl_for_enum, | 182 | unwrap_block::unwrap_block, |
184 | // These are manually sorted for better priorities | 183 | // These are manually sorted for better priorities |
185 | add_missing_impl_members::add_missing_impl_members, | 184 | add_missing_impl_members::add_missing_impl_members, |
186 | add_missing_impl_members::add_missing_default_members, | 185 | add_missing_impl_members::add_missing_default_members, |
187 | reorder_fields::reorder_fields, | 186 | // Are you sure you want to add new assist here, and not to the |
187 | // sorted list above? | ||
188 | ] | 188 | ] |
189 | } | 189 | } |
190 | } | 190 | } |
191 | |||
192 | #[cfg(test)] | ||
193 | mod helpers { | ||
194 | use std::sync::Arc; | ||
195 | |||
196 | use ra_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt}; | ||
197 | use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase}; | ||
198 | use test_utils::{add_cursor, assert_eq_text, extract_range_or_offset, RangeOrOffset}; | ||
199 | |||
200 | use crate::{AssistCtx, AssistFile, AssistHandler}; | ||
201 | use hir::Semantics; | ||
202 | |||
203 | pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { | ||
204 | let (mut db, file_id) = RootDatabase::with_single_file(text); | ||
205 | // FIXME: ideally, this should be done by the above `RootDatabase::with_single_file`, | ||
206 | // but it looks like this might need specialization? :( | ||
207 | db.set_local_roots(Arc::new(vec![db.file_source_root(file_id)])); | ||
208 | (db, file_id) | ||
209 | } | ||
210 | |||
211 | pub(crate) fn check_assist( | ||
212 | assist: AssistHandler, | ||
213 | ra_fixture_before: &str, | ||
214 | ra_fixture_after: &str, | ||
215 | ) { | ||
216 | check(assist, ra_fixture_before, ExpectedResult::After(ra_fixture_after)); | ||
217 | } | ||
218 | |||
219 | // FIXME: instead of having a separate function here, maybe use | ||
220 | // `extract_ranges` and mark the target as `<target> </target>` in the | ||
221 | // fixuture? | ||
222 | pub(crate) fn check_assist_target(assist: AssistHandler, ra_fixture: &str, target: &str) { | ||
223 | check(assist, ra_fixture, ExpectedResult::Target(target)); | ||
224 | } | ||
225 | |||
226 | pub(crate) fn check_assist_not_applicable(assist: AssistHandler, ra_fixture: &str) { | ||
227 | check(assist, ra_fixture, ExpectedResult::NotApplicable); | ||
228 | } | ||
229 | |||
230 | enum ExpectedResult<'a> { | ||
231 | NotApplicable, | ||
232 | After(&'a str), | ||
233 | Target(&'a str), | ||
234 | } | ||
235 | |||
236 | fn check(assist: AssistHandler, before: &str, expected: ExpectedResult) { | ||
237 | let (text_without_caret, file_with_caret_id, range_or_offset, db) = | ||
238 | if before.contains("//-") { | ||
239 | let (mut db, position) = RootDatabase::with_position(before); | ||
240 | db.set_local_roots(Arc::new(vec![db.file_source_root(position.file_id)])); | ||
241 | ( | ||
242 | db.file_text(position.file_id).as_ref().to_owned(), | ||
243 | position.file_id, | ||
244 | RangeOrOffset::Offset(position.offset), | ||
245 | db, | ||
246 | ) | ||
247 | } else { | ||
248 | let (range_or_offset, text_without_caret) = extract_range_or_offset(before); | ||
249 | let (db, file_id) = with_single_file(&text_without_caret); | ||
250 | (text_without_caret, file_id, range_or_offset, db) | ||
251 | }; | ||
252 | |||
253 | let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() }; | ||
254 | |||
255 | let sema = Semantics::new(&db); | ||
256 | let assist_ctx = AssistCtx::new(&sema, frange, true); | ||
257 | |||
258 | match (assist(assist_ctx), expected) { | ||
259 | (Some(assist), ExpectedResult::After(after)) => { | ||
260 | let action = assist.0[0].action.clone().unwrap(); | ||
261 | |||
262 | let assisted_file_text = if let AssistFile::TargetFile(file_id) = action.file { | ||
263 | db.file_text(file_id).as_ref().to_owned() | ||
264 | } else { | ||
265 | text_without_caret | ||
266 | }; | ||
267 | |||
268 | let mut actual = action.edit.apply(&assisted_file_text); | ||
269 | match action.cursor_position { | ||
270 | None => { | ||
271 | if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset { | ||
272 | let off = action | ||
273 | .edit | ||
274 | .apply_to_offset(before_cursor_pos) | ||
275 | .expect("cursor position is affected by the edit"); | ||
276 | actual = add_cursor(&actual, off) | ||
277 | } | ||
278 | } | ||
279 | Some(off) => actual = add_cursor(&actual, off), | ||
280 | }; | ||
281 | |||
282 | assert_eq_text!(after, &actual); | ||
283 | } | ||
284 | (Some(assist), ExpectedResult::Target(target)) => { | ||
285 | let action = assist.0[0].action.clone().unwrap(); | ||
286 | let range = action.target.expect("expected target on action"); | ||
287 | assert_eq_text!(&text_without_caret[range], target); | ||
288 | } | ||
289 | (Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"), | ||
290 | (None, ExpectedResult::After(_)) | (None, ExpectedResult::Target(_)) => { | ||
291 | panic!("code action is not applicable") | ||
292 | } | ||
293 | (None, ExpectedResult::NotApplicable) => (), | ||
294 | }; | ||
295 | } | ||
296 | } | ||
297 | |||
298 | #[cfg(test)] | ||
299 | mod tests { | ||
300 | use ra_db::FileRange; | ||
301 | use ra_syntax::TextRange; | ||
302 | use test_utils::{extract_offset, extract_range}; | ||
303 | |||
304 | use crate::{helpers, resolved_assists}; | ||
305 | |||
306 | #[test] | ||
307 | fn assist_order_field_struct() { | ||
308 | let before = "struct Foo { <|>bar: u32 }"; | ||
309 | let (before_cursor_pos, before) = extract_offset(before); | ||
310 | let (db, file_id) = helpers::with_single_file(&before); | ||
311 | let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) }; | ||
312 | let assists = resolved_assists(&db, frange); | ||
313 | let mut assists = assists.iter(); | ||
314 | |||
315 | assert_eq!( | ||
316 | assists.next().expect("expected assist").label.label, | ||
317 | "Change visibility to pub(crate)" | ||
318 | ); | ||
319 | assert_eq!(assists.next().expect("expected assist").label.label, "Add `#[derive]`"); | ||
320 | } | ||
321 | |||
322 | #[test] | ||
323 | fn assist_order_if_expr() { | ||
324 | let before = " | ||
325 | pub fn test_some_range(a: int) -> bool { | ||
326 | if let 2..6 = <|>5<|> { | ||
327 | true | ||
328 | } else { | ||
329 | false | ||
330 | } | ||
331 | }"; | ||
332 | let (range, before) = extract_range(before); | ||
333 | let (db, file_id) = helpers::with_single_file(&before); | ||
334 | let frange = FileRange { file_id, range }; | ||
335 | let assists = resolved_assists(&db, frange); | ||
336 | let mut assists = assists.iter(); | ||
337 | |||
338 | assert_eq!(assists.next().expect("expected assist").label.label, "Extract into variable"); | ||
339 | assert_eq!(assists.next().expect("expected assist").label.label, "Replace with match"); | ||
340 | } | ||
341 | } | ||