OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 | |
3 // for details. All rights reserved. Use of this source code is governed by a | |
4 // BSD-style license that can be found in the LICENSE file. | |
5 | |
6 library services.completion.contributor.dart.keyword; | |
7 | |
8 import 'dart:async'; | |
9 | |
10 import 'package:analysis_server/plugin/protocol/protocol.dart'; | |
11 import 'package:analysis_server/src/services/completion/dart_completion_manager.
dart'; | |
12 import 'package:analyzer/src/generated/ast.dart'; | |
13 import 'package:analyzer/src/generated/scanner.dart'; | |
14 | |
15 const ASYNC = 'async'; | |
16 const AWAIT = 'await'; | |
17 | |
18 /** | |
19 * A contributor for calculating `completion.getSuggestions` request results | |
20 * for the local library in which the completion is requested. | |
21 */ | |
22 class KeywordContributor extends DartCompletionContributor { | |
23 @override | |
24 bool computeFast(DartCompletionRequest request) { | |
25 if (!request.target.isCommentText) { | |
26 request.target.containingNode.accept(new _KeywordVisitor(request)); | |
27 } | |
28 return true; | |
29 } | |
30 | |
31 @override | |
32 Future<bool> computeFull(DartCompletionRequest request) { | |
33 return new Future.value(false); | |
34 } | |
35 } | |
36 | |
37 /** | |
38 * A visitor for generating keyword suggestions. | |
39 */ | |
40 class _KeywordVisitor extends GeneralizingAstVisitor { | |
41 final DartCompletionRequest request; | |
42 final Object entity; | |
43 | |
44 _KeywordVisitor(DartCompletionRequest request) | |
45 : this.request = request, | |
46 this.entity = request.target.entity; | |
47 | |
48 @override | |
49 visitArgumentList(ArgumentList node) { | |
50 if (entity == node.rightParenthesis || | |
51 (entity is SimpleIdentifier && node.arguments.contains(entity))) { | |
52 _addExpressionKeywords(node); | |
53 } | |
54 } | |
55 | |
56 @override | |
57 visitBlock(Block node) { | |
58 if (entity is ExpressionStatement) { | |
59 Expression expression = (entity as ExpressionStatement).expression; | |
60 if (expression is SimpleIdentifier) { | |
61 Token token = expression.token; | |
62 Token previous = token.previous; | |
63 if (previous.isSynthetic) { | |
64 previous = previous.previous; | |
65 } | |
66 Token next = token.next; | |
67 if (next.isSynthetic) { | |
68 next = next.next; | |
69 } | |
70 if (previous.lexeme == ')' && next.lexeme == '{') { | |
71 _addSuggestion2(ASYNC); | |
72 } | |
73 } | |
74 } | |
75 _addStatementKeywords(node); | |
76 if (_inCatchClause(node)) { | |
77 _addSuggestion(Keyword.RETHROW, DART_RELEVANCE_KEYWORD - 1); | |
78 } | |
79 } | |
80 | |
81 @override | |
82 visitClassDeclaration(ClassDeclaration node) { | |
83 // Don't suggest class name | |
84 if (entity == node.name) { | |
85 return; | |
86 } | |
87 if (entity == node.rightBracket) { | |
88 _addClassBodyKeywords(); | |
89 } else if (entity is ClassMember) { | |
90 _addClassBodyKeywords(); | |
91 int index = node.members.indexOf(entity); | |
92 ClassMember previous = index > 0 ? node.members[index - 1] : null; | |
93 if (previous is MethodDeclaration && previous.body is EmptyFunctionBody) { | |
94 _addSuggestion2(ASYNC); | |
95 } | |
96 } else { | |
97 _addClassDeclarationKeywords(node); | |
98 } | |
99 } | |
100 | |
101 @override | |
102 visitCompilationUnit(CompilationUnit node) { | |
103 var previousMember = null; | |
104 for (var member in node.childEntities) { | |
105 if (entity == member) { | |
106 break; | |
107 } | |
108 previousMember = member; | |
109 } | |
110 if (previousMember is ClassDeclaration) { | |
111 if (previousMember.leftBracket == null || | |
112 previousMember.leftBracket.isSynthetic) { | |
113 // If the prior member is an unfinished class declaration | |
114 // then the user is probably finishing that | |
115 _addClassDeclarationKeywords(previousMember); | |
116 return; | |
117 } | |
118 } | |
119 if (previousMember is ImportDirective) { | |
120 if (previousMember.semicolon == null || | |
121 previousMember.semicolon.isSynthetic) { | |
122 // If the prior member is an unfinished import directive | |
123 // then the user is probably finishing that | |
124 _addImportDirectiveKeywords(previousMember); | |
125 return; | |
126 } | |
127 } | |
128 if (previousMember == null || previousMember is Directive) { | |
129 if (previousMember == null && | |
130 !node.directives.any((d) => d is LibraryDirective)) { | |
131 _addSuggestions([Keyword.LIBRARY], DART_RELEVANCE_HIGH); | |
132 } | |
133 _addSuggestions( | |
134 [Keyword.IMPORT, Keyword.EXPORT, Keyword.PART], DART_RELEVANCE_HIGH); | |
135 } | |
136 if (entity == null || entity is Declaration) { | |
137 if (previousMember is FunctionDeclaration && | |
138 previousMember.functionExpression is FunctionExpression && | |
139 previousMember.functionExpression.body is EmptyFunctionBody) { | |
140 _addSuggestion2(ASYNC, relevance: DART_RELEVANCE_HIGH); | |
141 } | |
142 _addCompilationUnitKeywords(); | |
143 } | |
144 } | |
145 | |
146 @override | |
147 visitExpression(Expression node) { | |
148 _addExpressionKeywords(node); | |
149 } | |
150 | |
151 @override | |
152 visitExpressionFunctionBody(ExpressionFunctionBody node) { | |
153 if (entity == node.expression) { | |
154 _addExpressionKeywords(node); | |
155 } | |
156 } | |
157 | |
158 @override | |
159 visitForEachStatement(ForEachStatement node) { | |
160 if (entity == node.inKeyword) { | |
161 Token previous = node.inKeyword.previous; | |
162 if (previous is SyntheticStringToken && previous.lexeme == 'in') { | |
163 previous = previous.previous; | |
164 } | |
165 if (previous != null && previous.type == TokenType.EQ) { | |
166 _addSuggestions([ | |
167 Keyword.CONST, | |
168 Keyword.FALSE, | |
169 Keyword.NEW, | |
170 Keyword.NULL, | |
171 Keyword.TRUE | |
172 ]); | |
173 } else { | |
174 _addSuggestion(Keyword.IN, DART_RELEVANCE_HIGH); | |
175 } | |
176 } | |
177 } | |
178 | |
179 @override | |
180 visitFormalParameterList(FormalParameterList node) { | |
181 AstNode constructorDeclaration = | |
182 node.getAncestor((p) => p is ConstructorDeclaration); | |
183 if (constructorDeclaration != null) { | |
184 _addSuggestions([Keyword.THIS]); | |
185 } | |
186 } | |
187 | |
188 @override | |
189 visitForStatement(ForStatement node) { | |
190 // Handle the degenerate case while typing - for (int x i^) | |
191 if (node.condition == entity && entity is SimpleIdentifier) { | |
192 Token entityToken = (entity as SimpleIdentifier).beginToken; | |
193 if (entityToken.previous.isSynthetic && | |
194 entityToken.previous.type == TokenType.SEMICOLON) { | |
195 _addSuggestion(Keyword.IN, DART_RELEVANCE_HIGH); | |
196 } | |
197 } | |
198 } | |
199 | |
200 @override | |
201 visitFunctionExpression(FunctionExpression node) { | |
202 if (entity == node.body) { | |
203 if (!node.body.isAsynchronous) { | |
204 _addSuggestion2(ASYNC, relevance: DART_RELEVANCE_HIGH); | |
205 } | |
206 if (node.body is EmptyFunctionBody && | |
207 node.parent is FunctionDeclaration && | |
208 node.parent.parent is CompilationUnit) { | |
209 _addCompilationUnitKeywords(); | |
210 } | |
211 } | |
212 } | |
213 | |
214 @override | |
215 visitIfStatement(IfStatement node) { | |
216 if (entity == node.thenStatement) { | |
217 _addStatementKeywords(node); | |
218 } else if (entity == node.condition) { | |
219 _addExpressionKeywords(node); | |
220 } | |
221 } | |
222 | |
223 @override | |
224 visitImportDirective(ImportDirective node) { | |
225 if (entity == node.asKeyword) { | |
226 if (node.deferredKeyword == null) { | |
227 _addSuggestion(Keyword.DEFERRED, DART_RELEVANCE_HIGH); | |
228 } | |
229 } | |
230 // Handle degenerate case where import statement does not have a semicolon | |
231 // and the cursor is in the uri string | |
232 if ((entity == node.semicolon && | |
233 node.uri != null && | |
234 node.uri.offset + 1 != request.offset) || | |
235 node.combinators.contains(entity)) { | |
236 _addImportDirectiveKeywords(node); | |
237 } | |
238 } | |
239 | |
240 @override | |
241 visitInstanceCreationExpression(InstanceCreationExpression node) { | |
242 if (entity == node.constructorName) { | |
243 // no keywords in 'new ^' expression | |
244 } else { | |
245 super.visitInstanceCreationExpression(node); | |
246 } | |
247 } | |
248 | |
249 @override | |
250 visitIsExpression(IsExpression node) { | |
251 if (entity == node.isOperator) { | |
252 _addSuggestion(Keyword.IS, DART_RELEVANCE_HIGH); | |
253 } else { | |
254 _addExpressionKeywords(node); | |
255 } | |
256 } | |
257 | |
258 @override | |
259 visitLibraryIdentifier(LibraryIdentifier node) { | |
260 // no suggestions | |
261 } | |
262 | |
263 @override | |
264 visitMethodDeclaration(MethodDeclaration node) { | |
265 if (entity == node.body) { | |
266 if (node.body is EmptyFunctionBody) { | |
267 _addClassBodyKeywords(); | |
268 _addSuggestion2(ASYNC); | |
269 } else { | |
270 _addSuggestion2(ASYNC, relevance: DART_RELEVANCE_HIGH); | |
271 } | |
272 } | |
273 } | |
274 | |
275 @override | |
276 visitMethodInvocation(MethodInvocation node) { | |
277 if (entity == node.methodName) { | |
278 // no keywords in '.' expression | |
279 } else { | |
280 super.visitMethodInvocation(node); | |
281 } | |
282 } | |
283 | |
284 @override | |
285 visitNamedExpression(NamedExpression node) { | |
286 if (entity is SimpleIdentifier && entity == node.expression) { | |
287 _addExpressionKeywords(node); | |
288 } | |
289 } | |
290 | |
291 @override | |
292 visitNode(AstNode node) { | |
293 // ignored | |
294 } | |
295 | |
296 @override | |
297 visitPrefixedIdentifier(PrefixedIdentifier node) { | |
298 if (entity != node.identifier) { | |
299 _addExpressionKeywords(node); | |
300 } | |
301 } | |
302 | |
303 @override | |
304 visitPropertyAccess(PropertyAccess node) { | |
305 // suggestions before '.' but not after | |
306 if (entity != node.propertyName) { | |
307 super.visitPropertyAccess(node); | |
308 } | |
309 } | |
310 | |
311 @override | |
312 visitReturnStatement(ReturnStatement node) { | |
313 if (entity == node.expression) { | |
314 _addExpressionKeywords(node); | |
315 } | |
316 } | |
317 | |
318 @override | |
319 visitStringLiteral(StringLiteral node) { | |
320 // ignored | |
321 } | |
322 | |
323 @override | |
324 visitSwitchStatement(SwitchStatement node) { | |
325 if (entity == node.expression) { | |
326 _addExpressionKeywords(node); | |
327 } else if (entity == node.rightBracket) { | |
328 if (node.members.isEmpty) { | |
329 _addSuggestions([Keyword.CASE, Keyword.DEFAULT], DART_RELEVANCE_HIGH); | |
330 } else { | |
331 _addSuggestions([Keyword.CASE, Keyword.DEFAULT]); | |
332 _addStatementKeywords(node); | |
333 } | |
334 } | |
335 if (node.members.contains(entity)) { | |
336 if (entity == node.members.first) { | |
337 _addSuggestions([Keyword.CASE, Keyword.DEFAULT], DART_RELEVANCE_HIGH); | |
338 } else { | |
339 _addSuggestions([Keyword.CASE, Keyword.DEFAULT]); | |
340 _addStatementKeywords(node); | |
341 } | |
342 } | |
343 } | |
344 | |
345 @override | |
346 visitVariableDeclaration(VariableDeclaration node) { | |
347 if (entity == node.initializer) { | |
348 _addExpressionKeywords(node); | |
349 } | |
350 } | |
351 | |
352 void _addClassBodyKeywords() { | |
353 _addSuggestions([ | |
354 Keyword.CONST, | |
355 Keyword.DYNAMIC, | |
356 Keyword.FACTORY, | |
357 Keyword.FINAL, | |
358 Keyword.GET, | |
359 Keyword.OPERATOR, | |
360 Keyword.SET, | |
361 Keyword.STATIC, | |
362 Keyword.VAR, | |
363 Keyword.VOID | |
364 ]); | |
365 } | |
366 | |
367 void _addClassDeclarationKeywords(ClassDeclaration node) { | |
368 // Very simplistic suggestion because analyzer will warn if | |
369 // the extends / with / implements keywords are out of order | |
370 if (node.extendsClause == null) { | |
371 _addSuggestion(Keyword.EXTENDS, DART_RELEVANCE_HIGH); | |
372 } else if (node.withClause == null) { | |
373 _addSuggestion(Keyword.WITH, DART_RELEVANCE_HIGH); | |
374 } | |
375 if (node.implementsClause == null) { | |
376 _addSuggestion(Keyword.IMPLEMENTS, DART_RELEVANCE_HIGH); | |
377 } | |
378 } | |
379 | |
380 void _addCompilationUnitKeywords() { | |
381 _addSuggestions([ | |
382 Keyword.ABSTRACT, | |
383 Keyword.CLASS, | |
384 Keyword.CONST, | |
385 Keyword.DYNAMIC, | |
386 Keyword.FINAL, | |
387 Keyword.TYPEDEF, | |
388 Keyword.VAR, | |
389 Keyword.VOID | |
390 ], DART_RELEVANCE_HIGH); | |
391 } | |
392 | |
393 void _addExpressionKeywords(AstNode node) { | |
394 _addSuggestions([ | |
395 Keyword.CONST, | |
396 Keyword.FALSE, | |
397 Keyword.NEW, | |
398 Keyword.NULL, | |
399 Keyword.TRUE, | |
400 ]); | |
401 if (_inClassMemberBody(node)) { | |
402 _addSuggestions([Keyword.SUPER, Keyword.THIS,]); | |
403 } | |
404 if (_inAsyncMethodOrFunction(node)) { | |
405 _addSuggestion2(AWAIT); | |
406 } | |
407 } | |
408 | |
409 void _addImportDirectiveKeywords(ImportDirective node) { | |
410 bool hasDeferredKeyword = node.deferredKeyword != null; | |
411 bool hasAsKeyword = node.asKeyword != null; | |
412 if (!hasAsKeyword) { | |
413 _addSuggestion(Keyword.AS, DART_RELEVANCE_HIGH); | |
414 } | |
415 if (!hasDeferredKeyword) { | |
416 if (!hasAsKeyword) { | |
417 _addSuggestion2('deferred as', relevance: DART_RELEVANCE_HIGH); | |
418 } else if (entity == node.asKeyword) { | |
419 _addSuggestion(Keyword.DEFERRED, DART_RELEVANCE_HIGH); | |
420 } | |
421 } | |
422 if (!hasDeferredKeyword || hasAsKeyword) { | |
423 if (node.combinators.isEmpty) { | |
424 _addSuggestion2('show', relevance: DART_RELEVANCE_HIGH); | |
425 _addSuggestion2('hide', relevance: DART_RELEVANCE_HIGH); | |
426 } | |
427 } | |
428 } | |
429 | |
430 void _addStatementKeywords(AstNode node) { | |
431 if (_inClassMemberBody(node)) { | |
432 _addSuggestions([Keyword.SUPER, Keyword.THIS,]); | |
433 } | |
434 if (_inAsyncMethodOrFunction(node)) { | |
435 _addSuggestion2(AWAIT); | |
436 } | |
437 if (_inLoop(node)) { | |
438 _addSuggestions([Keyword.BREAK, Keyword.CONTINUE]); | |
439 } | |
440 if (_inSwitch(node)) { | |
441 _addSuggestions([Keyword.BREAK]); | |
442 } | |
443 _addSuggestions([ | |
444 Keyword.ASSERT, | |
445 Keyword.CONST, | |
446 Keyword.DO, | |
447 Keyword.FINAL, | |
448 Keyword.FOR, | |
449 Keyword.IF, | |
450 Keyword.NEW, | |
451 Keyword.RETURN, | |
452 Keyword.SWITCH, | |
453 Keyword.THROW, | |
454 Keyword.TRY, | |
455 Keyword.VAR, | |
456 Keyword.VOID, | |
457 Keyword.WHILE | |
458 ]); | |
459 } | |
460 | |
461 void _addSuggestion(Keyword keyword, | |
462 [int relevance = DART_RELEVANCE_KEYWORD]) { | |
463 _addSuggestion2(keyword.syntax, relevance: relevance); | |
464 } | |
465 | |
466 void _addSuggestion2(String completion, | |
467 {int offset, int relevance: DART_RELEVANCE_KEYWORD}) { | |
468 if (offset == null) { | |
469 offset = completion.length; | |
470 } | |
471 request.addSuggestion(new CompletionSuggestion( | |
472 CompletionSuggestionKind.KEYWORD, | |
473 relevance, | |
474 completion, | |
475 offset, | |
476 0, | |
477 false, | |
478 false)); | |
479 } | |
480 | |
481 void _addSuggestions(List<Keyword> keywords, | |
482 [int relevance = DART_RELEVANCE_KEYWORD]) { | |
483 keywords.forEach((Keyword keyword) { | |
484 _addSuggestion(keyword, relevance); | |
485 }); | |
486 } | |
487 | |
488 bool _inAsyncMethodOrFunction(AstNode node) { | |
489 FunctionBody body = node.getAncestor((n) => n is FunctionBody); | |
490 return body != null && body.isAsynchronous; | |
491 } | |
492 | |
493 bool _inCatchClause(Block node) => | |
494 node.getAncestor((p) => p is CatchClause) != null; | |
495 | |
496 bool _inClassMemberBody(AstNode node) { | |
497 while (true) { | |
498 AstNode body = node.getAncestor((n) => n is FunctionBody); | |
499 if (body == null) { | |
500 return false; | |
501 } | |
502 AstNode parent = body.parent; | |
503 if (parent is ConstructorDeclaration || parent is MethodDeclaration) { | |
504 return true; | |
505 } | |
506 node = parent; | |
507 } | |
508 } | |
509 | |
510 bool _inDoLoop(AstNode node) => | |
511 node.getAncestor((p) => p is DoStatement) != null; | |
512 | |
513 bool _inForLoop(AstNode node) => | |
514 node.getAncestor((p) => p is ForStatement || p is ForEachStatement) != | |
515 null; | |
516 | |
517 bool _inLoop(AstNode node) => | |
518 _inDoLoop(node) || _inForLoop(node) || _inWhileLoop(node); | |
519 | |
520 bool _inSwitch(AstNode node) => | |
521 node.getAncestor((p) => p is SwitchStatement) != null; | |
522 | |
523 bool _inWhileLoop(AstNode node) => | |
524 node.getAncestor((p) => p is WhileStatement) != null; | |
525 } | |
OLD | NEW |