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