1 //! `ra_assits` crate provides a bunch of code assists, aslo known as code
2 //! actions (in LSP) or intentions (in IntelliJ).
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
10 use itertools::Itertools;
12 use ra_text_edit::TextEdit;
13 use ra_syntax::{TextRange, TextUnit, SyntaxNode, Direction};
15 use hir::db::HirDatabase;
17 pub(crate) use crate::assist_ctx::{AssistCtx, Assist};
19 #[derive(Debug, Clone)]
20 pub struct AssistLabel {
21 /// Short description of the assist, as shown in the UI.
25 #[derive(Debug, Clone)]
26 pub struct AssistAction {
28 pub cursor_position: Option<TextUnit>,
29 pub target: Option<TextRange>,
32 /// Return all the assists applicable at the given position.
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>
38 H: HirDatabase + 'static,
40 AssistCtx::with_ctx(db, range, false, |ctx| {
43 .filter_map(|f| f(ctx.clone()))
45 Assist::Unresolved(labels) => labels,
46 Assist::Resolved(..) => unreachable!(),
52 /// Return all the assists applicable at the given position.
54 /// Assists are returned in the "resolved" state, that is with edit fully
56 pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
58 H: HirDatabase + 'static,
60 use std::cmp::Ordering;
62 AssistCtx::with_ctx(db, range, true, |ctx| {
63 let mut a = all_assists()
65 .filter_map(|f| f(ctx.clone()))
67 Assist::Resolved(labels_actions) => labels_actions,
68 Assist::Unresolved(..) => unreachable!(),
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,
84 mod change_visibility;
86 mod introduce_variable;
87 mod replace_if_let_with_match;
92 fn all_assists<DB: HirDatabase>() -> &'static [fn(AssistCtx<DB>) -> Option<Assist>] {
94 add_derive::add_derive,
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,
107 fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> {
108 node.siblings(direction).skip(1).find(|node| !node.kind().is_trivia())
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};
118 use crate::{AssistCtx, Assist};
120 pub(crate) fn check_assist(
121 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
125 check_assist_nth_action(assist, before, after, 0)
128 pub(crate) fn check_assist_range(
129 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
133 check_assist_range_nth_action(assist, before, after, 0)
136 pub(crate) fn check_assist_target(
137 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
141 check_assist_target_nth_action(assist, before, target, 0)
144 pub(crate) fn check_assist_range_target(
145 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
149 check_assist_range_target_nth_action(assist, before, target, 0)
152 pub(crate) fn check_assist_nth_action(
153 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
158 let (before_cursor_pos, before) = extract_offset(before);
159 let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
161 FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) };
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,
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 {
174 .apply_to_offset(before_cursor_pos)
175 .expect("cursor position is affected by the edit"),
178 let actual = add_cursor(&actual, actual_cursor_pos);
179 assert_eq_text!(after, &actual);
182 pub(crate) fn check_assist_range_nth_action(
183 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
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 };
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,
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);
203 assert_eq_text!(after, &actual);
206 pub(crate) fn check_assist_target_nth_action(
207 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
212 let (before_cursor_pos, before) = extract_offset(before);
213 let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
215 FileRange { file_id, range: TextRange::offset_len(before_cursor_pos, 0.into()) };
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,
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);
228 pub(crate) fn check_assist_range_target_nth_action(
229 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
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 };
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,
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);
249 pub(crate) fn check_assist_not_applicable(
250 assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
253 let (before_cursor_pos, before) = extract_offset(before);
254 let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
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());
264 use hir::mock::MockDatabase;
265 use ra_syntax::TextRange;
266 use ra_db::FileRange;
267 use test_utils::{extract_offset};
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);
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();
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]`");
284 fn assist_order_if_expr() {
286 pub fn test_some_range(a: int) -> bool {
293 let (before_cursor_pos, before) = extract_offset(before);
294 let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
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();
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");