aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light/src/assists.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2019-01-08 19:17:36 +0000
committerAleksey Kladov <[email protected]>2019-01-08 19:17:36 +0000
commit1967884d6836219ee78a754ca5c66ac781351559 (patch)
tree7594f37cd0a5200eb097b9d472c61f0223d01d05 /crates/ra_ide_api_light/src/assists.rs
parent4f4f7933b1b7ff34f8633b1686b18b2d1b994c47 (diff)
rename ra_editor -> ra_ide_api_light
Diffstat (limited to 'crates/ra_ide_api_light/src/assists.rs')
-rw-r--r--crates/ra_ide_api_light/src/assists.rs209
1 files changed, 209 insertions, 0 deletions
diff --git a/crates/ra_ide_api_light/src/assists.rs b/crates/ra_ide_api_light/src/assists.rs
new file mode 100644
index 000000000..83eabfc85
--- /dev/null
+++ b/crates/ra_ide_api_light/src/assists.rs
@@ -0,0 +1,209 @@
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
6mod flip_comma;
7mod add_derive;
8mod add_impl;
9mod introduce_variable;
10mod change_visibility;
11mod split_import;
12mod replace_if_let_with_match;
13
14use ra_text_edit::{TextEdit, TextEditBuilder};
15use 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};
20use itertools::Itertools;
21
22pub 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.
33pub 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)]
50pub struct LocalEdit {
51 pub label: String,
52 pub edit: TextEdit,
53 pub cursor_position: Option<TextUnit>,
54}
55
56fn 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)]
93pub struct AssistCtx<'a> {
94 source_file: &'a SourceFile,
95 range: TextRange,
96 should_compute_edit: bool,
97}
98
99#[derive(Debug)]
100pub enum Assist {
101 Applicable,
102 Edit(LocalEdit),
103}
104
105#[derive(Default)]
106struct AssistBuilder {
107 edit: TextEditBuilder,
108 cursor_position: Option<TextUnit>,
109}
110
111impl<'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
162impl 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
185fn 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
191fn reindent(text: &str, indent: &str) -> String {
192 let indent = format!("\n{}", indent);
193 text.lines().intersperse(&indent).collect()
194}
195
196#[cfg(test)]
197fn 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)]
205fn 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}