1use hir::{EditionedFileId, FileRange, Semantics};
4use ide_db::{FileId, RootDatabase, label::Label};
5use syntax::Edition;
6use syntax::{
7 AstNode, AstToken, Direction, SourceFile, SyntaxElement, SyntaxKind, SyntaxToken, TextRange,
8 TextSize, TokenAtOffset,
9 algo::{self, find_node_at_offset, find_node_at_range},
10};
11
12use crate::{
13 Assist, AssistId, AssistKind, AssistResolveStrategy, GroupLabel, assist_config::AssistConfig,
14};
15
16pub(crate) use ide_db::source_change::{SourceChangeBuilder, TreeMutator};
17
18pub(crate) struct AssistContext<'a> {
49 pub(crate) config: &'a AssistConfig,
50 pub(crate) sema: Semantics<'a, RootDatabase>,
51 frange: FileRange,
52 trimmed_range: TextRange,
53 source_file: SourceFile,
54 token_at_offset: TokenAtOffset<SyntaxToken>,
56 covering_element: SyntaxElement,
58}
59
60impl<'a> AssistContext<'a> {
61 pub(crate) fn new(
62 sema: Semantics<'a, RootDatabase>,
63 config: &'a AssistConfig,
64 frange: FileRange,
65 ) -> AssistContext<'a> {
66 let source_file = sema.parse(frange.file_id);
67
68 let start = frange.range.start();
69 let end = frange.range.end();
70 let left = source_file.syntax().token_at_offset(start);
71 let right = source_file.syntax().token_at_offset(end);
72 let left =
73 left.right_biased().and_then(|t| algo::skip_whitespace_token(t, Direction::Next));
74 let right =
75 right.left_biased().and_then(|t| algo::skip_whitespace_token(t, Direction::Prev));
76 let left = left.map(|t| t.text_range().start().clamp(start, end));
77 let right = right.map(|t| t.text_range().end().clamp(start, end));
78
79 let trimmed_range = match (left, right) {
80 (Some(left), Some(right)) if left <= right => TextRange::new(left, right),
81 _ => frange.range,
83 };
84 let token_at_offset = source_file.syntax().token_at_offset(frange.range.start());
85 let covering_element = source_file.syntax().covering_element(trimmed_range);
86
87 AssistContext {
88 config,
89 sema,
90 frange,
91 source_file,
92 trimmed_range,
93 token_at_offset,
94 covering_element,
95 }
96 }
97
98 pub(crate) fn db(&self) -> &'a RootDatabase {
99 self.sema.db
100 }
101
102 pub(crate) fn offset(&self) -> TextSize {
104 self.frange.range.start()
105 }
106
107 pub(crate) fn vfs_file_id(&self) -> FileId {
108 self.frange.file_id.file_id(self.db())
109 }
110
111 pub(crate) fn file_id(&self) -> EditionedFileId {
112 self.frange.file_id
113 }
114
115 pub(crate) fn edition(&self) -> Edition {
116 self.frange.file_id.edition(self.db())
117 }
118
119 pub(crate) fn has_empty_selection(&self) -> bool {
120 self.trimmed_range.is_empty()
121 }
122
123 pub(crate) fn selection_trimmed(&self) -> TextRange {
126 self.trimmed_range
127 }
128
129 pub(crate) fn source_file(&self) -> &SourceFile {
130 &self.source_file
131 }
132
133 pub(crate) fn token_at_offset(&self) -> TokenAtOffset<SyntaxToken> {
134 self.token_at_offset.clone()
135 }
136 pub(crate) fn find_token_syntax_at_offset(&self, kind: SyntaxKind) -> Option<SyntaxToken> {
137 self.token_at_offset().find(|it| it.kind() == kind)
138 }
139 pub(crate) fn find_token_at_offset<T: AstToken>(&self) -> Option<T> {
140 self.token_at_offset().find_map(T::cast)
141 }
142 pub(crate) fn find_node_at_offset<N: AstNode>(&self) -> Option<N> {
143 find_node_at_offset(self.source_file.syntax(), self.offset())
144 }
145 pub(crate) fn find_node_at_trimmed_offset<N: AstNode>(&self) -> Option<N> {
146 find_node_at_offset(self.source_file.syntax(), self.trimmed_range.start())
147 }
148 pub(crate) fn find_node_at_range<N: AstNode>(&self) -> Option<N> {
149 find_node_at_range(self.source_file.syntax(), self.trimmed_range)
150 }
151 pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> {
152 self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset())
153 }
154 pub(crate) fn covering_element(&self) -> SyntaxElement {
156 self.covering_element.clone()
157 }
158}
159
160pub(crate) struct Assists {
161 file: FileId,
162 resolve: AssistResolveStrategy,
163 buf: Vec<Assist>,
164 allowed: Option<Vec<AssistKind>>,
165}
166
167impl Assists {
168 pub(crate) fn new(ctx: &AssistContext<'_>, resolve: AssistResolveStrategy) -> Assists {
169 Assists {
170 resolve,
171 file: ctx.frange.file_id.file_id(ctx.db()),
172 buf: Vec::new(),
173 allowed: ctx.config.allowed.clone(),
174 }
175 }
176
177 pub(crate) fn finish(mut self) -> Vec<Assist> {
178 self.buf.sort_by_key(|assist| assist.target.len());
179 self.buf
180 }
181
182 pub(crate) fn add(
183 &mut self,
184 id: AssistId,
185 label: impl Into<String>,
186 target: TextRange,
187 f: impl FnOnce(&mut SourceChangeBuilder),
188 ) -> Option<()> {
189 let mut f = Some(f);
190 self.add_impl(None, id, label.into(), target, &mut |it| f.take().unwrap()(it))
191 }
192
193 pub(crate) fn add_group(
194 &mut self,
195 group: &GroupLabel,
196 id: AssistId,
197 label: impl Into<String>,
198 target: TextRange,
199 f: impl FnOnce(&mut SourceChangeBuilder),
200 ) -> Option<()> {
201 let mut f = Some(f);
202 self.add_impl(Some(group), id, label.into(), target, &mut |it| f.take().unwrap()(it))
203 }
204
205 fn add_impl(
206 &mut self,
207 group: Option<&GroupLabel>,
208 id: AssistId,
209 label: String,
210 target: TextRange,
211 f: &mut dyn FnMut(&mut SourceChangeBuilder),
212 ) -> Option<()> {
213 if !self.is_allowed(&id) {
214 return None;
215 }
216
217 let mut command = None;
218 let source_change = if self.resolve.should_resolve(&id) {
219 let mut builder = SourceChangeBuilder::new(self.file);
220 f(&mut builder);
221 command = builder.command.take();
222 Some(builder.finish())
223 } else {
224 None
225 };
226
227 let label = Label::new(label);
228 let group = group.cloned();
229 self.buf.push(Assist { id, label, group, target, source_change, command });
230 Some(())
231 }
232
233 fn is_allowed(&self, id: &AssistId) -> bool {
234 match &self.allowed {
235 Some(allowed) => allowed.iter().any(|kind| kind.contains(id.1)),
236 None => true,
237 }
238 }
239}