aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/completion/completion_context.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src/completion/completion_context.rs')
-rw-r--r--crates/ra_ide/src/completion/completion_context.rs274
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
3use ra_syntax::{
4 algo::{find_covering_element, find_node_at_offset},
5 ast, AstNode, Parse, SourceFile,
6 SyntaxKind::*,
7 SyntaxNode, SyntaxToken, TextRange, TextUnit,
8};
9use ra_text_edit::AtomTextEdit;
10
11use 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)]
16pub(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
48impl<'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
265fn 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
269fn 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}