aboutsummaryrefslogtreecommitdiff
path: root/crates/assists/src/assist_context.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/assists/src/assist_context.rs')
-rw-r--r--crates/assists/src/assist_context.rs291
1 files changed, 291 insertions, 0 deletions
diff --git a/crates/assists/src/assist_context.rs b/crates/assists/src/assist_context.rs
new file mode 100644
index 000000000..79574b9ac
--- /dev/null
+++ b/crates/assists/src/assist_context.rs
@@ -0,0 +1,291 @@
1//! See `AssistContext`
2
3use std::mem;
4
5use algo::find_covering_element;
6use base_db::{FileId, FileRange};
7use hir::Semantics;
8use ide_db::{
9 source_change::{SourceChange, SourceFileEdit},
10 RootDatabase,
11};
12use syntax::{
13 algo::{self, find_node_at_offset, SyntaxRewriter},
14 AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxToken, TextRange, TextSize,
15 TokenAtOffset,
16};
17use text_edit::{TextEdit, TextEditBuilder};
18
19use crate::{
20 assist_config::{AssistConfig, SnippetCap},
21 Assist, AssistId, AssistKind, GroupLabel, ResolvedAssist,
22};
23
24/// `AssistContext` allows to apply an assist or check if it could be applied.
25///
26/// Assists use a somewhat over-engineered approach, given the current needs.
27/// The assists workflow consists of two phases. In the first phase, a user asks
28/// for the list of available assists. In the second phase, the user picks a
29/// particular assist and it gets applied.
30///
31/// There are two peculiarities here:
32///
33/// * first, we ideally avoid computing more things then necessary to answer "is
34/// assist applicable" in the first phase.
35/// * second, when we are applying assist, we don't have a guarantee that there
36/// weren't any changes between the point when user asked for assists and when
37/// they applied a particular assist. So, when applying assist, we need to do
38/// all the checks from scratch.
39///
40/// To avoid repeating the same code twice for both "check" and "apply"
41/// functions, we use an approach reminiscent of that of Django's function based
42/// views dealing with forms. Each assist receives a runtime parameter,
43/// `resolve`. It first check if an edit is applicable (potentially computing
44/// info required to compute the actual edit). If it is applicable, and
45/// `resolve` is `true`, it then computes the actual edit.
46///
47/// So, to implement the original assists workflow, we can first apply each edit
48/// with `resolve = false`, and then applying the selected edit again, with
49/// `resolve = true` this time.
50///
51/// Note, however, that we don't actually use such two-phase logic at the
52/// moment, because the LSP API is pretty awkward in this place, and it's much
53/// easier to just compute the edit eagerly :-)
54pub(crate) struct AssistContext<'a> {
55 pub(crate) config: &'a AssistConfig,
56 pub(crate) sema: Semantics<'a, RootDatabase>,
57 pub(crate) frange: FileRange,
58 source_file: SourceFile,
59}
60
61impl<'a> AssistContext<'a> {
62 pub(crate) fn new(
63 sema: Semantics<'a, RootDatabase>,
64 config: &'a AssistConfig,
65 frange: FileRange,
66 ) -> AssistContext<'a> {
67 let source_file = sema.parse(frange.file_id);
68 AssistContext { config, sema, frange, source_file }
69 }
70
71 pub(crate) fn db(&self) -> &RootDatabase {
72 self.sema.db
73 }
74
75 pub(crate) fn source_file(&self) -> &SourceFile {
76 &self.source_file
77 }
78
79 // NB, this ignores active selection.
80 pub(crate) fn offset(&self) -> TextSize {
81 self.frange.range.start()
82 }
83
84 pub(crate) fn token_at_offset(&self) -> TokenAtOffset<SyntaxToken> {
85 self.source_file.syntax().token_at_offset(self.offset())
86 }
87 pub(crate) fn find_token_at_offset(&self, kind: SyntaxKind) -> Option<SyntaxToken> {
88 self.token_at_offset().find(|it| it.kind() == kind)
89 }
90 pub(crate) fn find_node_at_offset<N: AstNode>(&self) -> Option<N> {
91 find_node_at_offset(self.source_file.syntax(), self.offset())
92 }
93 pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> {
94 self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset())
95 }
96 pub(crate) fn covering_element(&self) -> SyntaxElement {
97 find_covering_element(self.source_file.syntax(), self.frange.range)
98 }
99 // FIXME: remove
100 pub(crate) fn covering_node_for_range(&self, range: TextRange) -> SyntaxElement {
101 find_covering_element(self.source_file.syntax(), range)
102 }
103}
104
105pub(crate) struct Assists {
106 resolve: bool,
107 file: FileId,
108 buf: Vec<(Assist, Option<SourceChange>)>,
109 allowed: Option<Vec<AssistKind>>,
110}
111
112impl Assists {
113 pub(crate) fn new_resolved(ctx: &AssistContext) -> Assists {
114 Assists {
115 resolve: true,
116 file: ctx.frange.file_id,
117 buf: Vec::new(),
118 allowed: ctx.config.allowed.clone(),
119 }
120 }
121
122 pub(crate) fn new_unresolved(ctx: &AssistContext) -> Assists {
123 Assists {
124 resolve: false,
125 file: ctx.frange.file_id,
126 buf: Vec::new(),
127 allowed: ctx.config.allowed.clone(),
128 }
129 }
130
131 pub(crate) fn finish_unresolved(self) -> Vec<Assist> {
132 assert!(!self.resolve);
133 self.finish()
134 .into_iter()
135 .map(|(label, edit)| {
136 assert!(edit.is_none());
137 label
138 })
139 .collect()
140 }
141
142 pub(crate) fn finish_resolved(self) -> Vec<ResolvedAssist> {
143 assert!(self.resolve);
144 self.finish()
145 .into_iter()
146 .map(|(label, edit)| ResolvedAssist { assist: label, source_change: edit.unwrap() })
147 .collect()
148 }
149
150 pub(crate) fn add(
151 &mut self,
152 id: AssistId,
153 label: impl Into<String>,
154 target: TextRange,
155 f: impl FnOnce(&mut AssistBuilder),
156 ) -> Option<()> {
157 if !self.is_allowed(&id) {
158 return None;
159 }
160 let label = Assist::new(id, label.into(), None, target);
161 self.add_impl(label, f)
162 }
163
164 pub(crate) fn add_group(
165 &mut self,
166 group: &GroupLabel,
167 id: AssistId,
168 label: impl Into<String>,
169 target: TextRange,
170 f: impl FnOnce(&mut AssistBuilder),
171 ) -> Option<()> {
172 if !self.is_allowed(&id) {
173 return None;
174 }
175
176 let label = Assist::new(id, label.into(), Some(group.clone()), target);
177 self.add_impl(label, f)
178 }
179
180 fn add_impl(&mut self, label: Assist, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> {
181 let source_change = if self.resolve {
182 let mut builder = AssistBuilder::new(self.file);
183 f(&mut builder);
184 Some(builder.finish())
185 } else {
186 None
187 };
188
189 self.buf.push((label, source_change));
190 Some(())
191 }
192
193 fn finish(mut self) -> Vec<(Assist, Option<SourceChange>)> {
194 self.buf.sort_by_key(|(label, _edit)| label.target.len());
195 self.buf
196 }
197
198 fn is_allowed(&self, id: &AssistId) -> bool {
199 match &self.allowed {
200 Some(allowed) => allowed.iter().any(|kind| kind.contains(id.1)),
201 None => true,
202 }
203 }
204}
205
206pub(crate) struct AssistBuilder {
207 edit: TextEditBuilder,
208 file_id: FileId,
209 is_snippet: bool,
210 change: SourceChange,
211}
212
213impl AssistBuilder {
214 pub(crate) fn new(file_id: FileId) -> AssistBuilder {
215 AssistBuilder {
216 edit: TextEdit::builder(),
217 file_id,
218 is_snippet: false,
219 change: SourceChange::default(),
220 }
221 }
222
223 pub(crate) fn edit_file(&mut self, file_id: FileId) {
224 self.file_id = file_id;
225 }
226
227 fn commit(&mut self) {
228 let edit = mem::take(&mut self.edit).finish();
229 if !edit.is_empty() {
230 let new_edit = SourceFileEdit { file_id: self.file_id, edit };
231 assert!(!self.change.source_file_edits.iter().any(|it| it.file_id == new_edit.file_id));
232 self.change.source_file_edits.push(new_edit);
233 }
234 }
235
236 /// Remove specified `range` of text.
237 pub(crate) fn delete(&mut self, range: TextRange) {
238 self.edit.delete(range)
239 }
240 /// Append specified `text` at the given `offset`
241 pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) {
242 self.edit.insert(offset, text.into())
243 }
244 /// Append specified `snippet` at the given `offset`
245 pub(crate) fn insert_snippet(
246 &mut self,
247 _cap: SnippetCap,
248 offset: TextSize,
249 snippet: impl Into<String>,
250 ) {
251 self.is_snippet = true;
252 self.insert(offset, snippet);
253 }
254 /// Replaces specified `range` of text with a given string.
255 pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
256 self.edit.replace(range, replace_with.into())
257 }
258 /// Replaces specified `range` of text with a given `snippet`.
259 pub(crate) fn replace_snippet(
260 &mut self,
261 _cap: SnippetCap,
262 range: TextRange,
263 snippet: impl Into<String>,
264 ) {
265 self.is_snippet = true;
266 self.replace(range, snippet);
267 }
268 pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) {
269 algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit)
270 }
271 pub(crate) fn rewrite(&mut self, rewriter: SyntaxRewriter) {
272 let node = rewriter.rewrite_root().unwrap();
273 let new = rewriter.rewrite(&node);
274 algo::diff(&node, &new).into_text_edit(&mut self.edit);
275 }
276
277 // FIXME: kill this API
278 /// Get access to the raw `TextEditBuilder`.
279 pub(crate) fn text_edit_builder(&mut self) -> &mut TextEditBuilder {
280 &mut self.edit
281 }
282
283 fn finish(mut self) -> SourceChange {
284 self.commit();
285 let mut change = mem::take(&mut self.change);
286 if self.is_snippet {
287 change.is_snippet = true;
288 }
289 change
290 }
291}