diff options
Diffstat (limited to 'crates/ra_ide/src/completion/completion_context.rs')
-rw-r--r-- | crates/ra_ide/src/completion/completion_context.rs | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/crates/ra_ide/src/completion/completion_context.rs b/crates/ra_ide/src/completion/completion_context.rs new file mode 100644 index 000000000..b8345c91d --- /dev/null +++ b/crates/ra_ide/src/completion/completion_context.rs | |||
@@ -0,0 +1,274 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | algo::{find_covering_element, find_node_at_offset}, | ||
5 | ast, AstNode, Parse, SourceFile, | ||
6 | SyntaxKind::*, | ||
7 | SyntaxNode, SyntaxToken, TextRange, TextUnit, | ||
8 | }; | ||
9 | use ra_text_edit::AtomTextEdit; | ||
10 | |||
11 | use crate::{db, FilePosition}; | ||
12 | |||
13 | /// `CompletionContext` is created early during completion to figure out, where | ||
14 | /// exactly is the cursor, syntax-wise. | ||
15 | #[derive(Debug)] | ||
16 | pub(crate) struct CompletionContext<'a> { | ||
17 | pub(super) db: &'a db::RootDatabase, | ||
18 | pub(super) analyzer: hir::SourceAnalyzer, | ||
19 | pub(super) offset: TextUnit, | ||
20 | pub(super) token: SyntaxToken, | ||
21 | pub(super) module: Option<hir::Module>, | ||
22 | pub(super) function_syntax: Option<ast::FnDef>, | ||
23 | pub(super) use_item_syntax: Option<ast::UseItem>, | ||
24 | pub(super) record_lit_syntax: Option<ast::RecordLit>, | ||
25 | pub(super) record_lit_pat: Option<ast::RecordPat>, | ||
26 | pub(super) is_param: bool, | ||
27 | /// If a name-binding or reference to a const in a pattern. | ||
28 | /// Irrefutable patterns (like let) are excluded. | ||
29 | pub(super) is_pat_binding: bool, | ||
30 | /// A single-indent path, like `foo`. `::foo` should not be considered a trivial path. | ||
31 | pub(super) is_trivial_path: bool, | ||
32 | /// If not a trivial path, the prefix (qualifier). | ||
33 | pub(super) path_prefix: Option<hir::Path>, | ||
34 | pub(super) after_if: bool, | ||
35 | /// `true` if we are a statement or a last expr in the block. | ||
36 | pub(super) can_be_stmt: bool, | ||
37 | /// Something is typed at the "top" level, in module or impl/trait. | ||
38 | pub(super) is_new_item: bool, | ||
39 | /// The receiver if this is a field or method access, i.e. writing something.<|> | ||
40 | pub(super) dot_receiver: Option<ast::Expr>, | ||
41 | pub(super) dot_receiver_is_ambiguous_float_literal: bool, | ||
42 | /// If this is a call (method or function) in particular, i.e. the () are already there. | ||
43 | pub(super) is_call: bool, | ||
44 | pub(super) is_path_type: bool, | ||
45 | pub(super) has_type_args: bool, | ||
46 | } | ||
47 | |||
48 | impl<'a> CompletionContext<'a> { | ||
49 | pub(super) fn new( | ||
50 | db: &'a db::RootDatabase, | ||
51 | original_parse: &'a Parse<ast::SourceFile>, | ||
52 | position: FilePosition, | ||
53 | ) -> Option<CompletionContext<'a>> { | ||
54 | let src = hir::ModuleSource::from_position(db, position); | ||
55 | let module = hir::Module::from_definition( | ||
56 | db, | ||
57 | hir::Source { file_id: position.file_id.into(), value: src }, | ||
58 | ); | ||
59 | let token = | ||
60 | original_parse.tree().syntax().token_at_offset(position.offset).left_biased()?; | ||
61 | let analyzer = hir::SourceAnalyzer::new( | ||
62 | db, | ||
63 | hir::Source::new(position.file_id.into(), &token.parent()), | ||
64 | Some(position.offset), | ||
65 | ); | ||
66 | let mut ctx = CompletionContext { | ||
67 | db, | ||
68 | analyzer, | ||
69 | token, | ||
70 | offset: position.offset, | ||
71 | module, | ||
72 | function_syntax: None, | ||
73 | use_item_syntax: None, | ||
74 | record_lit_syntax: None, | ||
75 | record_lit_pat: None, | ||
76 | is_param: false, | ||
77 | is_pat_binding: false, | ||
78 | is_trivial_path: false, | ||
79 | path_prefix: None, | ||
80 | after_if: false, | ||
81 | can_be_stmt: false, | ||
82 | is_new_item: false, | ||
83 | dot_receiver: None, | ||
84 | is_call: false, | ||
85 | is_path_type: false, | ||
86 | has_type_args: false, | ||
87 | dot_receiver_is_ambiguous_float_literal: false, | ||
88 | }; | ||
89 | ctx.fill(&original_parse, position.offset); | ||
90 | Some(ctx) | ||
91 | } | ||
92 | |||
93 | // The range of the identifier that is being completed. | ||
94 | pub(crate) fn source_range(&self) -> TextRange { | ||
95 | match self.token.kind() { | ||
96 | // workaroud when completion is triggered by trigger characters. | ||
97 | IDENT => self.token.text_range(), | ||
98 | _ => TextRange::offset_len(self.offset, 0.into()), | ||
99 | } | ||
100 | } | ||
101 | |||
102 | fn fill(&mut self, original_parse: &'a Parse<ast::SourceFile>, offset: TextUnit) { | ||
103 | // Insert a fake ident to get a valid parse tree. We will use this file | ||
104 | // to determine context, though the original_file will be used for | ||
105 | // actual completion. | ||
106 | let file = { | ||
107 | let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); | ||
108 | original_parse.reparse(&edit).tree() | ||
109 | }; | ||
110 | |||
111 | // First, let's try to complete a reference to some declaration. | ||
112 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), offset) { | ||
113 | // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. | ||
114 | // See RFC#1685. | ||
115 | if is_node::<ast::Param>(name_ref.syntax()) { | ||
116 | self.is_param = true; | ||
117 | return; | ||
118 | } | ||
119 | self.classify_name_ref(original_parse.tree(), name_ref); | ||
120 | } | ||
121 | |||
122 | // Otherwise, see if this is a declaration. We can use heuristics to | ||
123 | // suggest declaration names, see `CompletionKind::Magic`. | ||
124 | if let Some(name) = find_node_at_offset::<ast::Name>(file.syntax(), offset) { | ||
125 | if let Some(bind_pat) = name.syntax().ancestors().find_map(ast::BindPat::cast) { | ||
126 | let parent = bind_pat.syntax().parent(); | ||
127 | if parent.clone().and_then(ast::MatchArm::cast).is_some() | ||
128 | || parent.and_then(ast::Condition::cast).is_some() | ||
129 | { | ||
130 | self.is_pat_binding = true; | ||
131 | } | ||
132 | } | ||
133 | if is_node::<ast::Param>(name.syntax()) { | ||
134 | self.is_param = true; | ||
135 | return; | ||
136 | } | ||
137 | if name.syntax().ancestors().find_map(ast::RecordFieldPatList::cast).is_some() { | ||
138 | self.record_lit_pat = | ||
139 | find_node_at_offset(original_parse.tree().syntax(), self.offset); | ||
140 | } | ||
141 | } | ||
142 | } | ||
143 | |||
144 | fn classify_name_ref(&mut self, original_file: SourceFile, name_ref: ast::NameRef) { | ||
145 | let name_range = name_ref.syntax().text_range(); | ||
146 | if name_ref.syntax().parent().and_then(ast::RecordField::cast).is_some() { | ||
147 | self.record_lit_syntax = find_node_at_offset(original_file.syntax(), self.offset); | ||
148 | } | ||
149 | |||
150 | let top_node = name_ref | ||
151 | .syntax() | ||
152 | .ancestors() | ||
153 | .take_while(|it| it.text_range() == name_range) | ||
154 | .last() | ||
155 | .unwrap(); | ||
156 | |||
157 | match top_node.parent().map(|it| it.kind()) { | ||
158 | Some(SOURCE_FILE) | Some(ITEM_LIST) => { | ||
159 | self.is_new_item = true; | ||
160 | return; | ||
161 | } | ||
162 | _ => (), | ||
163 | } | ||
164 | |||
165 | self.use_item_syntax = self.token.parent().ancestors().find_map(ast::UseItem::cast); | ||
166 | |||
167 | self.function_syntax = self | ||
168 | .token | ||
169 | .parent() | ||
170 | .ancestors() | ||
171 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
172 | .find_map(ast::FnDef::cast); | ||
173 | |||
174 | let parent = match name_ref.syntax().parent() { | ||
175 | Some(it) => it, | ||
176 | None => return, | ||
177 | }; | ||
178 | |||
179 | if let Some(segment) = ast::PathSegment::cast(parent.clone()) { | ||
180 | let path = segment.parent_path(); | ||
181 | self.is_call = path | ||
182 | .syntax() | ||
183 | .parent() | ||
184 | .and_then(ast::PathExpr::cast) | ||
185 | .and_then(|it| it.syntax().parent().and_then(ast::CallExpr::cast)) | ||
186 | .is_some(); | ||
187 | |||
188 | self.is_path_type = path.syntax().parent().and_then(ast::PathType::cast).is_some(); | ||
189 | self.has_type_args = segment.type_arg_list().is_some(); | ||
190 | |||
191 | if let Some(mut path) = hir::Path::from_ast(path.clone()) { | ||
192 | if !path.is_ident() { | ||
193 | path.segments.pop().unwrap(); | ||
194 | self.path_prefix = Some(path); | ||
195 | return; | ||
196 | } | ||
197 | } | ||
198 | |||
199 | if path.qualifier().is_none() { | ||
200 | self.is_trivial_path = true; | ||
201 | |||
202 | // Find either enclosing expr statement (thing with `;`) or a | ||
203 | // block. If block, check that we are the last expr. | ||
204 | self.can_be_stmt = name_ref | ||
205 | .syntax() | ||
206 | .ancestors() | ||
207 | .find_map(|node| { | ||
208 | if let Some(stmt) = ast::ExprStmt::cast(node.clone()) { | ||
209 | return Some( | ||
210 | stmt.syntax().text_range() == name_ref.syntax().text_range(), | ||
211 | ); | ||
212 | } | ||
213 | if let Some(block) = ast::Block::cast(node) { | ||
214 | return Some( | ||
215 | block.expr().map(|e| e.syntax().text_range()) | ||
216 | == Some(name_ref.syntax().text_range()), | ||
217 | ); | ||
218 | } | ||
219 | None | ||
220 | }) | ||
221 | .unwrap_or(false); | ||
222 | |||
223 | if let Some(off) = name_ref.syntax().text_range().start().checked_sub(2.into()) { | ||
224 | if let Some(if_expr) = | ||
225 | find_node_at_offset::<ast::IfExpr>(original_file.syntax(), off) | ||
226 | { | ||
227 | if if_expr.syntax().text_range().end() | ||
228 | < name_ref.syntax().text_range().start() | ||
229 | { | ||
230 | self.after_if = true; | ||
231 | } | ||
232 | } | ||
233 | } | ||
234 | } | ||
235 | } | ||
236 | if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) { | ||
237 | // The receiver comes before the point of insertion of the fake | ||
238 | // ident, so it should have the same range in the non-modified file | ||
239 | self.dot_receiver = field_expr | ||
240 | .expr() | ||
241 | .map(|e| e.syntax().text_range()) | ||
242 | .and_then(|r| find_node_with_range(original_file.syntax(), r)); | ||
243 | self.dot_receiver_is_ambiguous_float_literal = if let Some(ast::Expr::Literal(l)) = | ||
244 | &self.dot_receiver | ||
245 | { | ||
246 | match l.kind() { | ||
247 | ast::LiteralKind::FloatNumber { suffix: _ } => l.token().text().ends_with('.'), | ||
248 | _ => false, | ||
249 | } | ||
250 | } else { | ||
251 | false | ||
252 | } | ||
253 | } | ||
254 | if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { | ||
255 | // As above | ||
256 | self.dot_receiver = method_call_expr | ||
257 | .expr() | ||
258 | .map(|e| e.syntax().text_range()) | ||
259 | .and_then(|r| find_node_with_range(original_file.syntax(), r)); | ||
260 | self.is_call = true; | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | |||
265 | fn find_node_with_range<N: AstNode>(syntax: &SyntaxNode, range: TextRange) -> Option<N> { | ||
266 | find_covering_element(syntax, range).ancestors().find_map(N::cast) | ||
267 | } | ||
268 | |||
269 | fn is_node<N: AstNode>(node: &SyntaxNode) -> bool { | ||
270 | match node.ancestors().find_map(N::cast) { | ||
271 | None => false, | ||
272 | Some(n) => n.syntax().text_range() == node.text_range(), | ||
273 | } | ||
274 | } | ||