aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light/src/assists.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api_light/src/assists.rs')
-rw-r--r--crates/ra_ide_api_light/src/assists.rs215
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
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};
19use itertools::Itertools;
20
21use crate::formatting::leading_indent;
22
23pub 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.
34pub 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)]
51pub struct LocalEdit {
52 pub label: String,
53 pub edit: TextEdit,
54 pub cursor_position: Option<TextUnit>,
55}
56
57fn 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)]
94pub struct AssistCtx<'a> {
95 source_file: &'a SourceFile,
96 range: TextRange,
97 should_compute_edit: bool,
98}
99
100#[derive(Debug)]
101pub enum Assist {
102 Applicable,
103 Edit(LocalEdit),
104}
105
106#[derive(Default)]
107pub struct AssistBuilder {
108 edit: TextEditBuilder,
109 cursor_position: Option<TextUnit>,
110}
111
112impl<'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
159impl 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
189fn reindent(text: &str, indent: &str) -> String {
190 let indent = format!("\n{}", indent);
191 text.lines().intersperse(&indent).collect()
192}
193
194#[cfg(test)]
195fn 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)]
203fn 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)]
211fn 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}