aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_editor/src/assists.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_editor/src/assists.rs')
-rw-r--r--crates/ra_editor/src/assists.rs155
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;
9mod introduce_variable; 9mod introduce_variable;
10mod change_visibility; 10mod change_visibility;
11 11
12use ra_text_edit::TextEdit; 12use ra_text_edit::{TextEdit, TextEditBuilder};
13use ra_syntax::{Direction, SyntaxNodeRef, TextUnit}; 13use ra_syntax::{
14 Direction, SyntaxNodeRef, TextUnit, TextRange,SourceFileNode, AstNode,
15 algo::{find_leaf_at_offset, find_covering_node, LeafAtOffset},
16};
17
18use crate::find_node_at_offset;
14 19
15pub use self::{ 20pub 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.
29pub 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)]
24pub struct LocalEdit { 44pub 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)]
87pub struct AssistCtx<'a> {
88 source_file: &'a SourceFileNode,
89 range: TextRange,
90 should_compute_edit: bool,
91}
92
93#[derive(Debug)]
94pub enum Assist {
95 Applicable,
96 Edit(LocalEdit),
97}
98
99#[derive(Default)]
100struct AssistBuilder {
101 edit: TextEditBuilder,
102 cursor_position: Option<TextUnit>,
103}
104
105impl<'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
156impl 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)]
173fn 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)]
181fn 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}