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