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