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