]> git.lizzy.rs Git - rust.git/blob - crates/ra_assists/src/lib.rs
initial invert_if
[rust.git] / crates / ra_assists / src / lib.rs
1 //! `ra_assists` crate provides a bunch of code assists, also known as code
2 //! actions (in LSP) or intentions (in IntelliJ).
3 //!
4 //! An assist is a micro-refactoring, which is automatically activated in
5 //! certain context. For example, if the cursor is over `,`, a "swap `,`" assist
6 //! becomes available.
7
8 mod assist_ctx;
9 mod marks;
10 #[cfg(test)]
11 mod doc_tests;
12 #[cfg(test)]
13 mod test_db;
14
15 use hir::db::HirDatabase;
16 use ra_db::FileRange;
17 use ra_syntax::{TextRange, TextUnit};
18 use ra_text_edit::TextEdit;
19
20 pub(crate) use crate::assist_ctx::{Assist, AssistCtx};
21 pub use crate::assists::add_import::auto_import_text_edit;
22
23 /// Unique identifier of the assist, should not be shown to the user
24 /// directly.
25 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
26 pub struct AssistId(pub &'static str);
27
28 #[derive(Debug, Clone)]
29 pub struct AssistLabel {
30     /// Short description of the assist, as shown in the UI.
31     pub label: String,
32     pub id: AssistId,
33 }
34
35 #[derive(Debug, Clone)]
36 pub struct AssistAction {
37     pub edit: TextEdit,
38     pub cursor_position: Option<TextUnit>,
39     pub target: Option<TextRange>,
40 }
41
42 /// Return all the assists applicable at the given position.
43 ///
44 /// Assists are returned in the "unresolved" state, that is only labels are
45 /// returned, without actual edits.
46 pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel>
47 where
48     H: HirDatabase + 'static,
49 {
50     AssistCtx::with_ctx(db, range, false, |ctx| {
51         assists::all()
52             .iter()
53             .filter_map(|f| f(ctx.clone()))
54             .map(|a| match a {
55                 Assist::Unresolved { label } => label,
56                 Assist::Resolved { .. } => unreachable!(),
57             })
58             .collect()
59     })
60 }
61
62 /// Return all the assists applicable at the given position.
63 ///
64 /// Assists are returned in the "resolved" state, that is with edit fully
65 /// computed.
66 pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
67 where
68     H: HirDatabase + 'static,
69 {
70     use std::cmp::Ordering;
71
72     AssistCtx::with_ctx(db, range, true, |ctx| {
73         let mut a = assists::all()
74             .iter()
75             .filter_map(|f| f(ctx.clone()))
76             .map(|a| match a {
77                 Assist::Resolved { label, action } => (label, action),
78                 Assist::Unresolved { .. } => unreachable!(),
79             })
80             .collect::<Vec<_>>();
81         a.sort_by(|a, b| match (a.1.target, b.1.target) {
82             (Some(a), Some(b)) => a.len().cmp(&b.len()),
83             (Some(_), None) => Ordering::Less,
84             (None, Some(_)) => Ordering::Greater,
85             (None, None) => Ordering::Equal,
86         });
87         a
88     })
89 }
90
91 mod assists {
92     use crate::{Assist, AssistCtx};
93     use hir::db::HirDatabase;
94
95     mod add_derive;
96     mod add_explicit_type;
97     mod add_impl;
98     mod add_new;
99     mod apply_demorgan;
100     mod invert_if;
101     mod flip_comma;
102     mod flip_binexpr;
103     mod flip_trait_bound;
104     mod change_visibility;
105     mod fill_match_arms;
106     mod merge_match_arms;
107     mod introduce_variable;
108     mod inline_local_variable;
109     mod raw_string;
110     mod replace_if_let_with_match;
111     mod split_import;
112     mod remove_dbg;
113     pub(crate) mod add_import;
114     mod add_missing_impl_members;
115     mod move_guard;
116     mod move_bounds;
117     mod early_return;
118
119     pub(crate) fn all<DB: HirDatabase>() -> &'static [fn(AssistCtx<DB>) -> Option<Assist>] {
120         &[
121             add_derive::add_derive,
122             add_explicit_type::add_explicit_type,
123             add_impl::add_impl,
124             add_new::add_new,
125             apply_demorgan::apply_demorgan,
126             invert_if::invert_if,
127             change_visibility::change_visibility,
128             fill_match_arms::fill_match_arms,
129             merge_match_arms::merge_match_arms,
130             flip_comma::flip_comma,
131             flip_binexpr::flip_binexpr,
132             flip_trait_bound::flip_trait_bound,
133             introduce_variable::introduce_variable,
134             replace_if_let_with_match::replace_if_let_with_match,
135             split_import::split_import,
136             remove_dbg::remove_dbg,
137             add_import::add_import,
138             add_missing_impl_members::add_missing_impl_members,
139             add_missing_impl_members::add_missing_default_members,
140             inline_local_variable::inline_local_varialbe,
141             move_guard::move_guard_to_arm_body,
142             move_guard::move_arm_cond_to_match_guard,
143             move_bounds::move_bounds_to_where_clause,
144             raw_string::add_hash,
145             raw_string::make_raw_string,
146             raw_string::make_usual_string,
147             raw_string::remove_hash,
148             early_return::convert_to_guarded_return,
149         ]
150     }
151 }
152
153 #[cfg(test)]
154 mod helpers {
155     use ra_db::{fixture::WithFixture, FileRange};
156     use ra_syntax::TextRange;
157     use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range};
158
159     use crate::{test_db::TestDB, Assist, AssistCtx};
160
161     pub(crate) fn check_assist(
162         assist: fn(AssistCtx<TestDB>) -> Option<Assist>,
163         before: &str,
164         after: &str,
165     ) {
166         let (before_cursor_pos, before) = extract_offset(before);
167         let (db, file_id) = TestDB::with_single_file(&before);
168         let frange =
169             FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) };
170         let assist =
171             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
172         let action = match assist {
173             Assist::Unresolved { .. } => unreachable!(),
174             Assist::Resolved { action, .. } => action,
175         };
176
177         let actual = action.edit.apply(&before);
178         let actual_cursor_pos = match action.cursor_position {
179             None => action
180                 .edit
181                 .apply_to_offset(before_cursor_pos)
182                 .expect("cursor position is affected by the edit"),
183             Some(off) => off,
184         };
185         let actual = add_cursor(&actual, actual_cursor_pos);
186         assert_eq_text!(after, &actual);
187     }
188
189     pub(crate) fn check_assist_range(
190         assist: fn(AssistCtx<TestDB>) -> Option<Assist>,
191         before: &str,
192         after: &str,
193     ) {
194         let (range, before) = extract_range(before);
195         let (db, file_id) = TestDB::with_single_file(&before);
196         let frange = FileRange { file_id, range };
197         let assist =
198             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
199         let action = match assist {
200             Assist::Unresolved { .. } => unreachable!(),
201             Assist::Resolved { action, .. } => action,
202         };
203
204         let mut actual = action.edit.apply(&before);
205         if let Some(pos) = action.cursor_position {
206             actual = add_cursor(&actual, pos);
207         }
208         assert_eq_text!(after, &actual);
209     }
210
211     pub(crate) fn check_assist_target(
212         assist: fn(AssistCtx<TestDB>) -> Option<Assist>,
213         before: &str,
214         target: &str,
215     ) {
216         let (before_cursor_pos, before) = extract_offset(before);
217         let (db, file_id) = TestDB::with_single_file(&before);
218         let frange =
219             FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) };
220         let assist =
221             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
222         let action = match assist {
223             Assist::Unresolved { .. } => unreachable!(),
224             Assist::Resolved { action, .. } => action,
225         };
226
227         let range = action.target.expect("expected target on action");
228         assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target);
229     }
230
231     pub(crate) fn check_assist_range_target(
232         assist: fn(AssistCtx<TestDB>) -> Option<Assist>,
233         before: &str,
234         target: &str,
235     ) {
236         let (range, before) = extract_range(before);
237         let (db, file_id) = TestDB::with_single_file(&before);
238         let frange = FileRange { file_id, range };
239         let assist =
240             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
241         let action = match assist {
242             Assist::Unresolved { .. } => unreachable!(),
243             Assist::Resolved { action, .. } => action,
244         };
245
246         let range = action.target.expect("expected target on action");
247         assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target);
248     }
249
250     pub(crate) fn check_assist_not_applicable(
251         assist: fn(AssistCtx<TestDB>) -> Option<Assist>,
252         before: &str,
253     ) {
254         let (before_cursor_pos, before) = extract_offset(before);
255         let (db, file_id) = TestDB::with_single_file(&before);
256         let frange =
257             FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) };
258         let assist = AssistCtx::with_ctx(&db, frange, true, assist);
259         assert!(assist.is_none());
260     }
261
262     pub(crate) fn check_assist_range_not_applicable(
263         assist: fn(AssistCtx<TestDB>) -> Option<Assist>,
264         before: &str,
265     ) {
266         let (range, before) = extract_range(before);
267         let (db, file_id) = TestDB::with_single_file(&before);
268         let frange = FileRange { file_id, range };
269         let assist = AssistCtx::with_ctx(&db, frange, true, assist);
270         assert!(assist.is_none());
271     }
272 }
273
274 #[cfg(test)]
275 mod tests {
276     use ra_db::{fixture::WithFixture, FileRange};
277     use ra_syntax::TextRange;
278     use test_utils::{extract_offset, extract_range};
279
280     use crate::test_db::TestDB;
281
282     #[test]
283     fn assist_order_field_struct() {
284         let before = "struct Foo { <|>bar: u32 }";
285         let (before_cursor_pos, before) = extract_offset(before);
286         let (db, file_id) = TestDB::with_single_file(&before);
287         let frange =
288             FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) };
289         let assists = super::assists(&db, frange);
290         let mut assists = assists.iter();
291
292         assert_eq!(assists.next().expect("expected assist").0.label, "make pub(crate)");
293         assert_eq!(assists.next().expect("expected assist").0.label, "add `#[derive]`");
294     }
295
296     #[test]
297     fn assist_order_if_expr() {
298         let before = "
299         pub fn test_some_range(a: int) -> bool {
300             if let 2..6 = <|>5<|> {
301                 true
302             } else {
303                 false
304             }
305         }";
306         let (range, before) = extract_range(before);
307         let (db, file_id) = TestDB::with_single_file(&before);
308         let frange = FileRange { file_id, range };
309         let assists = super::assists(&db, frange);
310         let mut assists = assists.iter();
311
312         assert_eq!(assists.next().expect("expected assist").0.label, "introduce variable");
313         assert_eq!(assists.next().expect("expected assist").0.label, "replace with match");
314     }
315 }