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