diff options
Diffstat (limited to 'crates/ra_assists/src/lib.rs')
-rw-r--r-- | crates/ra_assists/src/lib.rs | 195 |
1 files changed, 18 insertions, 177 deletions
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 0f94f5ee8..0473fd8c2 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -13,15 +13,14 @@ macro_rules! eprintln { | |||
13 | mod assist_ctx; | 13 | mod assist_ctx; |
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 hir::Semantics; | 20 | use hir::Semantics; |
21 | use ra_db::{FileId, FileRange}; | 21 | use ra_db::FileRange; |
22 | use ra_ide_db::RootDatabase; | 22 | use ra_ide_db::{source_change::SourceChange, RootDatabase}; |
23 | use ra_syntax::{TextRange, TextSize}; | 23 | use ra_syntax::TextRange; |
24 | use ra_text_edit::TextEdit; | ||
25 | 24 | ||
26 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; | 25 | pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; |
27 | 26 | ||
@@ -36,44 +35,31 @@ pub struct AssistLabel { | |||
36 | /// Short description of the assist, as shown in the UI. | 35 | /// Short description of the assist, as shown in the UI. |
37 | pub label: String, | 36 | pub label: String, |
38 | pub group: Option<GroupLabel>, | 37 | pub group: Option<GroupLabel>, |
38 | /// Target ranges are used to sort assists: the smaller the target range, | ||
39 | /// the more specific assist is, and so it should be sorted first. | ||
40 | pub target: TextRange, | ||
39 | } | 41 | } |
40 | 42 | ||
41 | #[derive(Clone, Debug)] | 43 | #[derive(Clone, Debug)] |
42 | pub struct GroupLabel(pub String); | 44 | pub struct GroupLabel(pub String); |
43 | 45 | ||
44 | impl AssistLabel { | 46 | impl AssistLabel { |
45 | pub(crate) fn new(id: AssistId, label: String, group: Option<GroupLabel>) -> AssistLabel { | 47 | pub(crate) fn new( |
48 | id: AssistId, | ||
49 | label: String, | ||
50 | group: Option<GroupLabel>, | ||
51 | target: TextRange, | ||
52 | ) -> AssistLabel { | ||
46 | // FIXME: make fields private, so that this invariant can't be broken | 53 | // FIXME: make fields private, so that this invariant can't be broken |
47 | assert!(label.starts_with(|c: char| c.is_uppercase())); | 54 | assert!(label.starts_with(|c: char| c.is_uppercase())); |
48 | AssistLabel { id, label, group } | 55 | AssistLabel { id, label, group, target } |
49 | } | 56 | } |
50 | } | 57 | } |
51 | 58 | ||
52 | #[derive(Debug, Clone)] | 59 | #[derive(Debug, Clone)] |
53 | pub struct AssistAction { | ||
54 | pub edit: TextEdit, | ||
55 | pub cursor_position: Option<TextSize>, | ||
56 | // FIXME: This belongs to `AssistLabel` | ||
57 | pub target: Option<TextRange>, | ||
58 | pub file: AssistFile, | ||
59 | } | ||
60 | |||
61 | #[derive(Debug, Clone)] | ||
62 | pub struct ResolvedAssist { | 60 | pub struct ResolvedAssist { |
63 | pub label: AssistLabel, | 61 | pub label: AssistLabel, |
64 | pub action: AssistAction, | 62 | pub source_change: SourceChange, |
65 | } | ||
66 | |||
67 | #[derive(Debug, Clone, Copy)] | ||
68 | pub enum AssistFile { | ||
69 | CurrentFile, | ||
70 | TargetFile(FileId), | ||
71 | } | ||
72 | |||
73 | impl Default for AssistFile { | ||
74 | fn default() -> Self { | ||
75 | Self::CurrentFile | ||
76 | } | ||
77 | } | 63 | } |
78 | 64 | ||
79 | /// Return all the assists applicable at the given position. | 65 | /// Return all the assists applicable at the given position. |
@@ -104,7 +90,7 @@ pub fn resolved_assists(db: &RootDatabase, range: FileRange) -> Vec<ResolvedAssi | |||
104 | .flat_map(|it| it.0) | 90 | .flat_map(|it| it.0) |
105 | .map(|it| it.into_resolved().unwrap()) | 91 | .map(|it| it.into_resolved().unwrap()) |
106 | .collect::<Vec<_>>(); | 92 | .collect::<Vec<_>>(); |
107 | a.sort_by_key(|it| it.action.target.map_or(TextSize::from(!0u32), |it| it.len())); | 93 | a.sort_by_key(|it| it.label.target.len()); |
108 | a | 94 | a |
109 | } | 95 | } |
110 | 96 | ||
@@ -143,6 +129,7 @@ mod handlers { | |||
143 | mod replace_qualified_name_with_use; | 129 | mod replace_qualified_name_with_use; |
144 | mod replace_unwrap_with_match; | 130 | mod replace_unwrap_with_match; |
145 | mod split_import; | 131 | mod split_import; |
132 | mod change_return_type_to_result; | ||
146 | mod add_from_impl_for_enum; | 133 | mod add_from_impl_for_enum; |
147 | mod reorder_fields; | 134 | mod reorder_fields; |
148 | mod unwrap_block; | 135 | mod unwrap_block; |
@@ -159,6 +146,7 @@ mod handlers { | |||
159 | add_new::add_new, | 146 | add_new::add_new, |
160 | apply_demorgan::apply_demorgan, | 147 | apply_demorgan::apply_demorgan, |
161 | auto_import::auto_import, | 148 | auto_import::auto_import, |
149 | change_return_type_to_result::change_return_type_to_result, | ||
162 | change_visibility::change_visibility, | 150 | change_visibility::change_visibility, |
163 | early_return::convert_to_guarded_return, | 151 | early_return::convert_to_guarded_return, |
164 | fill_match_arms::fill_match_arms, | 152 | fill_match_arms::fill_match_arms, |
@@ -194,150 +182,3 @@ mod handlers { | |||
194 | ] | 182 | ] |
195 | } | 183 | } |
196 | } | 184 | } |
197 | |||
198 | #[cfg(test)] | ||
199 | mod helpers { | ||
200 | use std::sync::Arc; | ||
201 | |||
202 | use hir::Semantics; | ||
203 | use ra_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt}; | ||
204 | use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase}; | ||
205 | use test_utils::{add_cursor, assert_eq_text, extract_range_or_offset, RangeOrOffset}; | ||
206 | |||
207 | use crate::{handlers::Handler, AssistCtx, AssistFile}; | ||
208 | |||
209 | pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { | ||
210 | let (mut db, file_id) = RootDatabase::with_single_file(text); | ||
211 | // FIXME: ideally, this should be done by the above `RootDatabase::with_single_file`, | ||
212 | // but it looks like this might need specialization? :( | ||
213 | db.set_local_roots(Arc::new(vec![db.file_source_root(file_id)])); | ||
214 | (db, file_id) | ||
215 | } | ||
216 | |||
217 | pub(crate) fn check_assist(assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
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: Handler, ra_fixture: &str, target: &str) { | ||
225 | check(assist, ra_fixture, ExpectedResult::Target(target)); | ||
226 | } | ||
227 | |||
228 | pub(crate) fn check_assist_not_applicable(assist: Handler, 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: Handler, 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 mut actual = 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 | action.edit.apply(&mut actual); | ||
270 | |||
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 | } | ||