diff options
Diffstat (limited to 'crates/ra_ide_api_light/src/assists.rs')
-rw-r--r-- | crates/ra_ide_api_light/src/assists.rs | 215 |
1 files changed, 0 insertions, 215 deletions
diff --git a/crates/ra_ide_api_light/src/assists.rs b/crates/ra_ide_api_light/src/assists.rs deleted file mode 100644 index e578805f1..000000000 --- a/crates/ra_ide_api_light/src/assists.rs +++ /dev/null | |||
@@ -1,215 +0,0 @@ | |||
1 | //! This modules contains various "assists": suggestions for source code edits | ||
2 | //! which are likely to occur at a given cursor position. For example, if the | ||
3 | //! cursor is on the `,`, a possible assist is swapping the elements around the | ||
4 | //! comma. | ||
5 | |||
6 | mod flip_comma; | ||
7 | mod add_derive; | ||
8 | mod add_impl; | ||
9 | mod introduce_variable; | ||
10 | mod change_visibility; | ||
11 | mod split_import; | ||
12 | mod replace_if_let_with_match; | ||
13 | |||
14 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
15 | use ra_syntax::{ | ||
16 | Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode, | ||
17 | algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, | ||
18 | }; | ||
19 | use itertools::Itertools; | ||
20 | |||
21 | use crate::formatting::leading_indent; | ||
22 | |||
23 | pub use self::{ | ||
24 | flip_comma::flip_comma, | ||
25 | add_derive::add_derive, | ||
26 | add_impl::add_impl, | ||
27 | introduce_variable::introduce_variable, | ||
28 | change_visibility::change_visibility, | ||
29 | split_import::split_import, | ||
30 | replace_if_let_with_match::replace_if_let_with_match, | ||
31 | }; | ||
32 | |||
33 | /// Return all the assists applicable at the given position. | ||
34 | pub fn assists(file: &SourceFile, range: TextRange) -> Vec<LocalEdit> { | ||
35 | let ctx = AssistCtx::new(file, range); | ||
36 | [ | ||
37 | flip_comma, | ||
38 | add_derive, | ||
39 | add_impl, | ||
40 | introduce_variable, | ||
41 | change_visibility, | ||
42 | split_import, | ||
43 | replace_if_let_with_match, | ||
44 | ] | ||
45 | .iter() | ||
46 | .filter_map(|&assist| ctx.clone().apply(assist)) | ||
47 | .collect() | ||
48 | } | ||
49 | |||
50 | #[derive(Debug)] | ||
51 | pub struct LocalEdit { | ||
52 | pub label: String, | ||
53 | pub edit: TextEdit, | ||
54 | pub cursor_position: Option<TextUnit>, | ||
55 | } | ||
56 | |||
57 | fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { | ||
58 | node.siblings(direction) | ||
59 | .skip(1) | ||
60 | .find(|node| !node.kind().is_trivia()) | ||
61 | } | ||
62 | |||
63 | /// `AssistCtx` allows to apply an assist or check if it could be applied. | ||
64 | /// | ||
65 | /// Assists use a somewhat overengineered approach, given the current needs. The | ||
66 | /// assists workflow consists of two phases. In the first phase, a user asks for | ||
67 | /// the list of available assists. In the second phase, the user picks a | ||
68 | /// particular assist and it gets applied. | ||
69 | /// | ||
70 | /// There are two peculiarities here: | ||
71 | /// | ||
72 | /// * first, we ideally avoid computing more things then necessary to answer | ||
73 | /// "is assist applicable" in the first phase. | ||
74 | /// * second, when we are applying assist, we don't have a guarantee that there | ||
75 | /// weren't any changes between the point when user asked for assists and when | ||
76 | /// they applied a particular assist. So, when applying assist, we need to do | ||
77 | /// all the checks from scratch. | ||
78 | /// | ||
79 | /// To avoid repeating the same code twice for both "check" and "apply" | ||
80 | /// functions, we use an approach reminiscent of that of Django's function based | ||
81 | /// views dealing with forms. Each assist receives a runtime parameter, | ||
82 | /// `should_compute_edit`. It first check if an edit is applicable (potentially | ||
83 | /// computing info required to compute the actual edit). If it is applicable, | ||
84 | /// and `should_compute_edit` is `true`, it then computes the actual edit. | ||
85 | /// | ||
86 | /// So, to implement the original assists workflow, we can first apply each edit | ||
87 | /// with `should_compute_edit = false`, and then applying the selected edit | ||
88 | /// again, with `should_compute_edit = true` this time. | ||
89 | /// | ||
90 | /// Note, however, that we don't actually use such two-phase logic at the | ||
91 | /// moment, because the LSP API is pretty awkward in this place, and it's much | ||
92 | /// easier to just compute the edit eagerly :-) | ||
93 | #[derive(Debug, Clone)] | ||
94 | pub struct AssistCtx<'a> { | ||
95 | source_file: &'a SourceFile, | ||
96 | range: TextRange, | ||
97 | should_compute_edit: bool, | ||
98 | } | ||
99 | |||
100 | #[derive(Debug)] | ||
101 | pub enum Assist { | ||
102 | Applicable, | ||
103 | Edit(LocalEdit), | ||
104 | } | ||
105 | |||
106 | #[derive(Default)] | ||
107 | pub struct AssistBuilder { | ||
108 | edit: TextEditBuilder, | ||
109 | cursor_position: Option<TextUnit>, | ||
110 | } | ||
111 | |||
112 | impl<'a> AssistCtx<'a> { | ||
113 | pub fn new(source_file: &'a SourceFile, range: TextRange) -> AssistCtx { | ||
114 | AssistCtx { | ||
115 | source_file, | ||
116 | range, | ||
117 | should_compute_edit: false, | ||
118 | } | ||
119 | } | ||
120 | |||
121 | pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> { | ||
122 | self.should_compute_edit = true; | ||
123 | match assist(self) { | ||
124 | None => None, | ||
125 | Some(Assist::Edit(e)) => Some(e), | ||
126 | Some(Assist::Applicable) => unreachable!(), | ||
127 | } | ||
128 | } | ||
129 | |||
130 | pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool { | ||
131 | self.should_compute_edit = false; | ||
132 | match assist(self) { | ||
133 | None => false, | ||
134 | Some(Assist::Edit(_)) => unreachable!(), | ||
135 | Some(Assist::Applicable) => true, | ||
136 | } | ||
137 | } | ||
138 | |||
139 | fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> { | ||
140 | if !self.should_compute_edit { | ||
141 | return Some(Assist::Applicable); | ||
142 | } | ||
143 | let mut edit = AssistBuilder::default(); | ||
144 | f(&mut edit); | ||
145 | Some(edit.build(label)) | ||
146 | } | ||
147 | |||
148 | pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { | ||
149 | find_leaf_at_offset(self.source_file.syntax(), self.range.start()) | ||
150 | } | ||
151 | pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> { | ||
152 | find_node_at_offset(self.source_file.syntax(), self.range.start()) | ||
153 | } | ||
154 | pub(crate) fn covering_node(&self) -> &'a SyntaxNode { | ||
155 | find_covering_node(self.source_file.syntax(), self.range) | ||
156 | } | ||
157 | } | ||
158 | |||
159 | impl AssistBuilder { | ||
160 | fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { | ||
161 | self.edit.replace(range, replace_with.into()) | ||
162 | } | ||
163 | pub fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into<String>) { | ||
164 | let mut replace_with = replace_with.into(); | ||
165 | if let Some(indent) = leading_indent(node) { | ||
166 | replace_with = reindent(&replace_with, indent) | ||
167 | } | ||
168 | self.replace(node.range(), replace_with) | ||
169 | } | ||
170 | #[allow(unused)] | ||
171 | fn delete(&mut self, range: TextRange) { | ||
172 | self.edit.delete(range) | ||
173 | } | ||
174 | fn insert(&mut self, offset: TextUnit, text: impl Into<String>) { | ||
175 | self.edit.insert(offset, text.into()) | ||
176 | } | ||
177 | fn set_cursor(&mut self, offset: TextUnit) { | ||
178 | self.cursor_position = Some(offset) | ||
179 | } | ||
180 | pub fn build(self, label: impl Into<String>) -> Assist { | ||
181 | Assist::Edit(LocalEdit { | ||
182 | label: label.into(), | ||
183 | cursor_position: self.cursor_position, | ||
184 | edit: self.edit.finish(), | ||
185 | }) | ||
186 | } | ||
187 | } | ||
188 | |||
189 | fn reindent(text: &str, indent: &str) -> String { | ||
190 | let indent = format!("\n{}", indent); | ||
191 | text.lines().intersperse(&indent).collect() | ||
192 | } | ||
193 | |||
194 | #[cfg(test)] | ||
195 | fn check_assist(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) { | ||
196 | crate::test_utils::check_action(before, after, |file, off| { | ||
197 | let range = TextRange::offset_len(off, 0.into()); | ||
198 | AssistCtx::new(file, range).apply(assist) | ||
199 | }) | ||
200 | } | ||
201 | |||
202 | #[cfg(test)] | ||
203 | fn check_assist_not_applicable(assist: fn(AssistCtx) -> Option<Assist>, text: &str) { | ||
204 | crate::test_utils::check_action_not_applicable(text, |file, off| { | ||
205 | let range = TextRange::offset_len(off, 0.into()); | ||
206 | AssistCtx::new(file, range).apply(assist) | ||
207 | }) | ||
208 | } | ||
209 | |||
210 | #[cfg(test)] | ||
211 | fn check_assist_range(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) { | ||
212 | crate::test_utils::check_action_range(before, after, |file, range| { | ||
213 | AssistCtx::new(file, range).apply(assist) | ||
214 | }) | ||
215 | } | ||