aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/assist_ctx.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src/assist_ctx.rs')
-rw-r--r--crates/ra_assists/src/assist_ctx.rs265
1 files changed, 0 insertions, 265 deletions
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs
deleted file mode 100644
index cbf1963b7..000000000
--- a/crates/ra_assists/src/assist_ctx.rs
+++ /dev/null
@@ -1,265 +0,0 @@
1//! This module defines `AssistCtx` -- the API surface that is exposed to assists.
2use hir::Semantics;
3use ra_db::FileRange;
4use ra_fmt::{leading_indent, reindent};
5use ra_ide_db::RootDatabase;
6use ra_syntax::{
7 algo::{self, find_covering_element, find_node_at_offset, SyntaxRewriter},
8 AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize,
9 TokenAtOffset,
10};
11use ra_text_edit::TextEditBuilder;
12
13use crate::{AssistAction, AssistFile, AssistId, AssistLabel, GroupLabel, ResolvedAssist};
14
15#[derive(Clone, Debug)]
16pub(crate) struct Assist(pub(crate) Vec<AssistInfo>);
17
18#[derive(Clone, Debug)]
19pub(crate) struct AssistInfo {
20 pub(crate) label: AssistLabel,
21 pub(crate) group_label: Option<GroupLabel>,
22 pub(crate) action: Option<AssistAction>,
23}
24
25impl AssistInfo {
26 fn new(label: AssistLabel) -> AssistInfo {
27 AssistInfo { label, group_label: None, action: None }
28 }
29
30 fn resolved(self, action: AssistAction) -> AssistInfo {
31 AssistInfo { action: Some(action), ..self }
32 }
33
34 fn with_group(self, group_label: GroupLabel) -> AssistInfo {
35 AssistInfo { group_label: Some(group_label), ..self }
36 }
37
38 pub(crate) fn into_resolved(self) -> Option<ResolvedAssist> {
39 let label = self.label;
40 self.action.map(|action| ResolvedAssist { label, action })
41 }
42}
43
44/// `AssistCtx` allows to apply an assist or check if it could be applied.
45///
46/// Assists use a somewhat over-engineered approach, given the current needs. The
47/// assists workflow consists of two phases. In the first phase, a user asks for
48/// the list of available assists. In the second phase, the user picks a
49/// particular assist and it gets applied.
50///
51/// There are two peculiarities here:
52///
53/// * first, we ideally avoid computing more things then necessary to answer
54/// "is assist applicable" in the first phase.
55/// * second, when we are applying assist, we don't have a guarantee that there
56/// weren't any changes between the point when user asked for assists and when
57/// they applied a particular assist. So, when applying assist, we need to do
58/// all the checks from scratch.
59///
60/// To avoid repeating the same code twice for both "check" and "apply"
61/// functions, we use an approach reminiscent of that of Django's function based
62/// views dealing with forms. Each assist receives a runtime parameter,
63/// `should_compute_edit`. It first check if an edit is applicable (potentially
64/// computing info required to compute the actual edit). If it is applicable,
65/// and `should_compute_edit` is `true`, it then computes the actual edit.
66///
67/// So, to implement the original assists workflow, we can first apply each edit
68/// with `should_compute_edit = false`, and then applying the selected edit
69/// again, with `should_compute_edit = true` this time.
70///
71/// Note, however, that we don't actually use such two-phase logic at the
72/// moment, because the LSP API is pretty awkward in this place, and it's much
73/// easier to just compute the edit eagerly :-)
74#[derive(Clone)]
75pub(crate) struct AssistCtx<'a> {
76 pub(crate) sema: &'a Semantics<'a, RootDatabase>,
77 pub(crate) db: &'a RootDatabase,
78 pub(crate) frange: FileRange,
79 source_file: SourceFile,
80 should_compute_edit: bool,
81}
82
83impl<'a> AssistCtx<'a> {
84 pub fn new(
85 sema: &'a Semantics<'a, RootDatabase>,
86 frange: FileRange,
87 should_compute_edit: bool,
88 ) -> AssistCtx<'a> {
89 let source_file = sema.parse(frange.file_id);
90 AssistCtx { sema, db: sema.db, frange, source_file, should_compute_edit }
91 }
92
93 pub(crate) fn add_assist(
94 self,
95 id: AssistId,
96 label: impl Into<String>,
97 target: TextRange,
98 f: impl FnOnce(&mut ActionBuilder),
99 ) -> Option<Assist> {
100 let label = AssistLabel::new(id, label.into(), None, target);
101
102 let mut info = AssistInfo::new(label);
103 if self.should_compute_edit {
104 let action = {
105 let mut edit = ActionBuilder::new(&self);
106 f(&mut edit);
107 edit.build()
108 };
109 info = info.resolved(action)
110 };
111
112 Some(Assist(vec![info]))
113 }
114
115 pub(crate) fn add_assist_group(self, group_name: impl Into<String>) -> AssistGroup<'a> {
116 let group = GroupLabel(group_name.into());
117 AssistGroup { ctx: self, group, assists: Vec::new() }
118 }
119
120 pub(crate) fn token_at_offset(&self) -> TokenAtOffset<SyntaxToken> {
121 self.source_file.syntax().token_at_offset(self.frange.range.start())
122 }
123
124 pub(crate) fn find_token_at_offset(&self, kind: SyntaxKind) -> Option<SyntaxToken> {
125 self.token_at_offset().find(|it| it.kind() == kind)
126 }
127
128 pub(crate) fn find_node_at_offset<N: AstNode>(&self) -> Option<N> {
129 find_node_at_offset(self.source_file.syntax(), self.frange.range.start())
130 }
131
132 pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> {
133 self.sema
134 .find_node_at_offset_with_descend(self.source_file.syntax(), self.frange.range.start())
135 }
136
137 pub(crate) fn covering_element(&self) -> SyntaxElement {
138 find_covering_element(self.source_file.syntax(), self.frange.range)
139 }
140 pub(crate) fn covering_node_for_range(&self, range: TextRange) -> SyntaxElement {
141 find_covering_element(self.source_file.syntax(), range)
142 }
143}
144
145pub(crate) struct AssistGroup<'a> {
146 ctx: AssistCtx<'a>,
147 group: GroupLabel,
148 assists: Vec<AssistInfo>,
149}
150
151impl<'a> AssistGroup<'a> {
152 pub(crate) fn add_assist(
153 &mut self,
154 id: AssistId,
155 label: impl Into<String>,
156 target: TextRange,
157 f: impl FnOnce(&mut ActionBuilder),
158 ) {
159 let label = AssistLabel::new(id, label.into(), Some(self.group.clone()), target);
160
161 let mut info = AssistInfo::new(label).with_group(self.group.clone());
162 if self.ctx.should_compute_edit {
163 let action = {
164 let mut edit = ActionBuilder::new(&self.ctx);
165 f(&mut edit);
166 edit.build()
167 };
168 info = info.resolved(action)
169 };
170
171 self.assists.push(info)
172 }
173
174 pub(crate) fn finish(self) -> Option<Assist> {
175 if self.assists.is_empty() {
176 None
177 } else {
178 Some(Assist(self.assists))
179 }
180 }
181}
182
183pub(crate) struct ActionBuilder<'a, 'b> {
184 edit: TextEditBuilder,
185 cursor_position: Option<TextSize>,
186 file: AssistFile,
187 ctx: &'a AssistCtx<'b>,
188}
189
190impl<'a, 'b> ActionBuilder<'a, 'b> {
191 fn new(ctx: &'a AssistCtx<'b>) -> Self {
192 Self {
193 edit: TextEditBuilder::default(),
194 cursor_position: None,
195 file: AssistFile::default(),
196 ctx,
197 }
198 }
199
200 pub(crate) fn ctx(&self) -> &AssistCtx<'b> {
201 &self.ctx
202 }
203
204 /// Replaces specified `range` of text with a given string.
205 pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
206 self.edit.replace(range, replace_with.into())
207 }
208
209 /// Replaces specified `node` of text with a given string, reindenting the
210 /// string to maintain `node`'s existing indent.
211 // FIXME: remove in favor of ra_syntax::edit::IndentLevel::increase_indent
212 pub(crate) fn replace_node_and_indent(
213 &mut self,
214 node: &SyntaxNode,
215 replace_with: impl Into<String>,
216 ) {
217 let mut replace_with = replace_with.into();
218 if let Some(indent) = leading_indent(node) {
219 replace_with = reindent(&replace_with, &indent)
220 }
221 self.replace(node.text_range(), replace_with)
222 }
223
224 /// Remove specified `range` of text.
225 #[allow(unused)]
226 pub(crate) fn delete(&mut self, range: TextRange) {
227 self.edit.delete(range)
228 }
229
230 /// Append specified `text` at the given `offset`
231 pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) {
232 self.edit.insert(offset, text.into())
233 }
234
235 /// Specify desired position of the cursor after the assist is applied.
236 pub(crate) fn set_cursor(&mut self, offset: TextSize) {
237 self.cursor_position = Some(offset)
238 }
239
240 /// Get access to the raw `TextEditBuilder`.
241 pub(crate) fn text_edit_builder(&mut self) -> &mut TextEditBuilder {
242 &mut self.edit
243 }
244
245 pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) {
246 algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit)
247 }
248 pub(crate) fn rewrite(&mut self, rewriter: SyntaxRewriter) {
249 let node = rewriter.rewrite_root().unwrap();
250 let new = rewriter.rewrite(&node);
251 algo::diff(&node, &new).into_text_edit(&mut self.edit)
252 }
253
254 pub(crate) fn set_file(&mut self, assist_file: AssistFile) {
255 self.file = assist_file
256 }
257
258 fn build(self) -> AssistAction {
259 let edit = self.edit.finish();
260 if edit.is_empty() && self.cursor_position.is_none() {
261 panic!("Only call `add_assist` if the assist can be applied")
262 }
263 AssistAction { edit, cursor_position: self.cursor_position, file: self.file }
264 }
265}