aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src/lib.rs')
-rw-r--r--crates/ra_assists/src/lib.rs293
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
13mod assist_ctx; 13mod assist_context;
14mod marks; 14mod marks;
15#[cfg(test)] 15#[cfg(test)]
16mod doc_tests; 16mod tests;
17pub mod utils; 17pub mod utils;
18pub mod ast_transform; 18pub mod ast_transform;
19 19
20use ra_db::{FileId, FileRange};
21use ra_ide_db::RootDatabase;
22use ra_syntax::{TextRange, TextSize};
23use ra_text_edit::TextEdit;
24
25pub(crate) use crate::assist_ctx::{Assist, AssistCtx, AssistHandler};
26use hir::Semantics; 20use hir::Semantics;
21use ra_db::FileRange;
22use ra_ide_db::{source_change::SourceChange, RootDatabase};
23use ra_syntax::TextRange;
24
25pub(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)]
31pub struct AssistId(pub &'static str); 30pub struct AssistId(pub &'static str);
32 31
33#[derive(Debug, Clone)]
34pub 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)]
41pub struct GroupLabel(pub String); 33pub struct GroupLabel(pub String);
42 34
43impl 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)]
52pub struct AssistAction { 36pub 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)]
61pub struct ResolvedAssist { 47pub 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)] 52impl Assist {
68pub 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> {
73impl 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.
83pub 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>,
98pub 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
111mod handlers { 93mod 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)]
195mod 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)]
301mod 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}