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