OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library services.src.completion.statement; | |
6 | |
7 import 'dart:async'; | |
8 | |
9 import 'package:analysis_server/plugin/protocol/protocol.dart'; | |
10 import 'package:analysis_server/src/protocol_server.dart' hide Element; | |
11 import 'package:analysis_server/src/services/correction/source_buffer.dart'; | |
12 import 'package:analysis_server/src/services/correction/source_range.dart'; | |
13 import 'package:analysis_server/src/services/correction/util.dart'; | |
14 import 'package:analyzer/dart/ast/ast.dart'; | |
15 import 'package:analyzer/dart/ast/token.dart'; | |
16 import 'package:analyzer/dart/element/element.dart'; | |
17 import 'package:analyzer/error/error.dart'; | |
18 import 'package:analyzer/error/error.dart' as engine; | |
19 import 'package:analyzer/src/dart/ast/utilities.dart'; | |
20 import 'package:analyzer/src/dart/error/hint_codes.dart'; | |
21 import 'package:analyzer/src/dart/error/syntactic_errors.dart'; | |
22 import 'package:analyzer/src/generated/engine.dart'; | |
23 import 'package:analyzer/src/generated/java_core.dart'; | |
24 import 'package:analyzer/src/generated/source.dart'; | |
25 | |
26 /** | |
27 * An enumeration of possible statement completion kinds. | |
28 */ | |
29 class DartStatementCompletion { | |
30 static const NO_COMPLETION = | |
31 const StatementCompletionKind('No_COMPLETION', 'No completion available'); | |
32 static const PLAIN_OLE_ENTER = const StatementCompletionKind( | |
scheglov
2017/04/07 19:36:07
What means OLE?
And how about more general COM?
messick
2017/04/07 20:10:09
I'll change it something more readable.
| |
33 'PLAIN_OLE_ENTER', "Insert a newline at the end of the current line"); | |
34 static const SIMPLE_SEMICOLON = const StatementCompletionKind( | |
35 'SIMPLE_SEMICOLON', "Add a semicolon and newline"); | |
36 static const COMPLETE_IF_STMT = const StatementCompletionKind( | |
37 'COMPLETE_IF_STMT', "Complete if-statement"); | |
38 static const COMPLETE_WHILE_STMT = const StatementCompletionKind( | |
39 'COMPLETE_WHILE_STMT', "Complete while-statement"); | |
40 } | |
41 | |
42 /** | |
43 * A description of a statement completion. | |
44 * | |
45 * Clients may not extend, implement or mix-in this class. | |
46 */ | |
47 class StatementCompletion { | |
48 /** | |
49 * A description of the assist being proposed. | |
50 */ | |
51 final StatementCompletionKind kind; | |
52 | |
53 /** | |
54 * The change to be made in order to apply the assist. | |
55 */ | |
56 final SourceChange change; | |
57 | |
58 /** | |
59 * Initialize a newly created completion to have the given [kind] and [change] . | |
60 */ | |
61 StatementCompletion(this.kind, this.change); | |
62 } | |
63 | |
64 /** | |
65 * The context for computing a statement completion. | |
66 */ | |
67 class StatementCompletionContext { | |
68 final CompilationUnitElement unitElement; | |
69 final int selectionOffset; | |
70 final LineInfo lineInfo; | |
71 final List<engine.AnalysisError> errors; | |
72 final CompilationUnit unit; | |
73 final String file; | |
scheglov
2017/04/07 19:36:07
The order of fields seems random.
Might be worth t
messick
2017/04/07 20:10:09
It wasn't random but I can change it to the order
| |
74 | |
75 StatementCompletionContext(this.unitElement, this.selectionOffset, | |
76 this.lineInfo, this.errors, this.file, this.unit) { | |
77 if (unitElement.context == null) { | |
78 throw new Error(); // not reached; see getStatementCompletion() | |
79 } | |
80 } | |
81 } | |
82 | |
83 /** | |
84 * A description of a class of statement completions. Instances are intended to | |
85 * hold the information that is common across a number of completions and to be | |
86 * shared by those completions. | |
87 * | |
88 * Clients may not extend, implement or mix-in this class. | |
89 */ | |
90 class StatementCompletionKind { | |
91 /** | |
92 * The name of this kind of statement completion, used for debugging. | |
93 */ | |
94 final String name; | |
95 | |
96 /** | |
97 * A human-readable description of the changes that will be applied by this | |
98 * kind of statement completion. | |
99 */ | |
100 final String message; | |
101 | |
102 /** | |
103 * Initialize a newly created kind of statement completion to have the given | |
104 * [name] and [message]. | |
105 */ | |
106 const StatementCompletionKind(this.name, this.message); | |
107 | |
108 @override | |
109 String toString() => name; | |
110 } | |
111 | |
112 /** | |
113 * The computer for Dart statement completions. | |
114 */ | |
115 class StatementCompletionProcessor { | |
116 static final NO_COMPLETION = new StatementCompletion( | |
117 DartStatementCompletion.NO_COMPLETION, new SourceChange("", edits: [])); | |
118 | |
119 final StatementCompletionContext statementContext; | |
120 final AnalysisContext analysisContext; | |
121 final CorrectionUtils utils; | |
122 int fileStamp; | |
123 AstNode node; | |
124 StatementCompletion completion; | |
125 SourceChange change = new SourceChange('statement-completion'); | |
126 List errors = <engine.AnalysisError>[]; | |
127 final Map<String, LinkedEditGroup> linkedPositionGroups = | |
128 <String, LinkedEditGroup>{}; | |
129 Position exitPosition = null; | |
130 | |
131 StatementCompletionProcessor(this.statementContext) | |
132 : analysisContext = statementContext.unitElement.context, | |
133 utils = new CorrectionUtils(statementContext.unit) { | |
134 fileStamp = analysisContext.getModificationStamp(source); | |
135 } | |
136 | |
137 String get eol => utils.endOfLine; | |
138 | |
139 String get file => statementContext.file; | |
140 | |
141 LineInfo get lineInfo => statementContext.lineInfo; | |
142 | |
143 int get requestLine => lineInfo.getLocation(selectionOffset).lineNumber; | |
144 | |
145 int get selectionOffset => statementContext.selectionOffset; | |
146 | |
147 Source get source => statementContext.unitElement.source; | |
148 | |
149 CompilationUnit get unit => statementContext.unit; | |
150 | |
151 CompilationUnitElement get unitElement => statementContext.unitElement; | |
152 | |
153 Future<StatementCompletion> compute() async { | |
154 // If the source was changed between the constructor and running | |
155 // this asynchronous method, it is not safe to use the unit. | |
156 if (analysisContext.getModificationStamp(source) != fileStamp) { | |
157 return NO_COMPLETION; | |
158 } | |
159 node = new NodeLocator(selectionOffset).searchWithin(unit); | |
160 if (node == null) { | |
161 return NO_COMPLETION; | |
162 } | |
163 // TODO(messick): This needs to work for declarations. | |
164 node = node.getAncestor((n) => n is Statement); | |
165 for (engine.AnalysisError error in statementContext.errors) { | |
166 if (error.offset >= node.offset && | |
167 error.offset <= node.offset + node.length) { | |
168 if (error.errorCode is! HintCode) { | |
169 errors.add(error); | |
170 } | |
171 } | |
172 } | |
173 | |
174 if (_complete_ifStatement() || | |
175 _complete_whileStatement() || | |
176 _complete_simpleSemicolon() || | |
177 _complete_plainOleEnter()) { | |
178 return completion; | |
179 } | |
180 return NO_COMPLETION; | |
181 } | |
182 | |
183 void _addIndentEdit(SourceRange range, String oldIndent, String newIndent) { | |
184 SourceEdit edit = utils.createIndentEdit(range, oldIndent, newIndent); | |
185 doSourceChange_addElementEdit(change, unitElement, edit); | |
186 } | |
187 | |
188 void _addInsertEdit(int offset, String text) { | |
189 SourceEdit edit = new SourceEdit(offset, 0, text); | |
190 doSourceChange_addElementEdit(change, unitElement, edit); | |
191 } | |
192 | |
193 void _addReplaceEdit(SourceRange range, String text) { | |
194 SourceEdit edit = new SourceEdit(range.offset, range.length, text); | |
195 doSourceChange_addElementEdit(change, unitElement, edit); | |
196 } | |
197 | |
198 void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) { | |
199 sb.append(' {'); | |
200 sb.append(eol); | |
201 String indent = utils.getLinePrefix(selectionOffset); | |
202 sb.append(indent); | |
203 sb.append(utils.getIndent(1)); | |
204 if (needsExitMark) { | |
205 sb.setExitOffset(); | |
206 } | |
207 sb.append(eol); | |
208 sb.append(indent); | |
209 sb.append('}'); | |
210 } | |
211 | |
212 int _appendNewlinePlusIndent() { | |
213 // Append a newline plus proper indent and another newline. | |
214 // Return the position before the second newline. | |
215 String indent = utils.getLinePrefix(selectionOffset); | |
216 int loc = utils.getLineNext(selectionOffset); | |
217 _addInsertEdit(loc, indent + eol); | |
218 return loc + indent.length; | |
219 } | |
220 | |
221 bool _complete_ifOrWhileStatement( | |
222 _IfWhileStructure ifNode, StatementCompletionKind kind) { | |
223 String text = utils.getNodeText(node); | |
224 if (text.endsWith(eol)) { | |
225 text = text.substring(0, text.length - eol.length); | |
226 } | |
227 SourceBuilder sb; | |
228 bool needsExit = false; | |
229 if (ifNode.leftParenthesis.lexeme.isEmpty) { | |
230 if (!ifNode.rightParenthesis.lexeme.isEmpty) { | |
231 // Quite unlikely to see this so don't try to fix it. | |
232 return false; | |
233 } | |
234 int len = ifNode.keyword.length; | |
235 if (text.length == len || | |
236 !text.substring(len, len + 1).contains(new RegExp(r'\s'))) { | |
237 sb = new SourceBuilder(file, ifNode.offset + len); | |
238 sb.append(' '); | |
239 } else { | |
240 sb = new SourceBuilder(file, ifNode.offset + len + 1); | |
241 } | |
242 sb.append('('); | |
243 sb.setExitOffset(); | |
244 sb.append(')'); | |
245 } else { | |
246 if (_isEmptyExpression(ifNode.condition)) { | |
247 exitPosition = _newPosition(ifNode.leftParenthesis.offset + 1); | |
248 sb = new SourceBuilder(file, ifNode.rightParenthesis.offset + 1); | |
249 } else { | |
250 sb = new SourceBuilder(file, ifNode.rightParenthesis.offset + 1); | |
251 needsExit = true; | |
252 } | |
253 } | |
254 if (ifNode.block is EmptyStatement) { | |
255 _appendEmptyBraces(sb, needsExit); | |
256 } | |
257 _insertBuilder(sb); | |
258 _setCompletion(kind); | |
259 return true; | |
260 } | |
261 | |
262 bool _complete_ifStatement() { | |
263 if (errors.isEmpty) { | |
264 return false; | |
265 } | |
266 IfStatement ifNode = node.getAncestor((n) => n is IfStatement); | |
Brian Wilkerson
2017/04/07 19:17:14
Given that this can travel an indeterminate distan
messick
2017/04/07 20:10:09
Yes, that's a good point. Since the conditional ex
Brian Wilkerson
2017/04/07 21:23:37
If you know it's contained in a statement (as oppo
| |
267 if (ifNode != null) { | |
268 if (ifNode.elseKeyword != null) { | |
269 return false; | |
270 } | |
271 var stmt = new _IfWhileStructure(ifNode.ifKeyword, ifNode.leftParenthesis, | |
272 ifNode.condition, ifNode.rightParenthesis, ifNode.thenStatement); | |
273 return _complete_ifOrWhileStatement( | |
274 stmt, DartStatementCompletion.COMPLETE_IF_STMT); | |
275 } | |
276 return false; | |
277 } | |
278 | |
279 bool _complete_plainOleEnter() { | |
280 int offset; | |
281 if (!errors.isEmpty) { | |
282 offset = selectionOffset; | |
283 } else { | |
284 String indent = utils.getLinePrefix(selectionOffset); | |
285 int loc = utils.getLineNext(selectionOffset); | |
286 _addInsertEdit(loc, indent + eol); | |
287 offset = loc + indent.length + eol.length; | |
288 } | |
289 _setCompletionAt(DartStatementCompletion.PLAIN_OLE_ENTER, offset); | |
290 return true; | |
291 } | |
292 | |
293 bool _complete_simpleSemicolon() { | |
294 if (errors.length != 1) { | |
295 return false; | |
296 } | |
297 var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'"); | |
298 if (error != null) { | |
299 int insertOffset = error.offset + error.length; | |
300 _addInsertEdit(insertOffset, ';'); | |
301 int offset = _appendNewlinePlusIndent() + 1 /* ';' */; | |
302 _setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset); | |
303 return true; | |
304 } | |
305 return false; | |
306 } | |
307 | |
308 bool _complete_whileStatement() { | |
309 if (errors.isEmpty) { | |
310 return false; | |
311 } | |
312 WhileStatement whileNode = node.getAncestor((n) => n is WhileStatement); | |
313 if (whileNode != null) { | |
314 var stmt = new _IfWhileStructure( | |
315 whileNode.whileKeyword, | |
316 whileNode.leftParenthesis, | |
317 whileNode.condition, | |
318 whileNode.rightParenthesis, | |
319 whileNode.body); | |
320 return _complete_ifOrWhileStatement( | |
321 stmt, DartStatementCompletion.COMPLETE_WHILE_STMT); | |
322 } | |
323 return false; | |
324 } | |
325 | |
326 engine.AnalysisError _findError(ErrorCode code, {partialMatch: null}) { | |
327 var error = | |
328 errors.firstWhere((err) => err.errorCode == code, orElse: () => null); | |
329 if (error != null) { | |
330 if (partialMatch != null) { | |
331 return error.message.contains(partialMatch) ? error : null; | |
332 } | |
333 return error; | |
334 } | |
335 return null; | |
336 } | |
337 | |
338 LinkedEditGroup _getLinkedPosition(String groupId) { | |
339 LinkedEditGroup group = linkedPositionGroups[groupId]; | |
340 if (group == null) { | |
341 group = new LinkedEditGroup.empty(); | |
342 linkedPositionGroups[groupId] = group; | |
343 } | |
344 return group; | |
345 } | |
346 | |
347 void _insertBuilder(SourceBuilder builder, [int length = 0]) { | |
348 { | |
349 SourceRange range = rangeStartLength(builder.offset, length); | |
350 String text = builder.toString(); | |
351 _addReplaceEdit(range, text); | |
352 } | |
353 // add linked positions | |
354 builder.linkedPositionGroups.forEach((String id, LinkedEditGroup group) { | |
355 LinkedEditGroup fixGroup = _getLinkedPosition(id); | |
356 group.positions.forEach((Position position) { | |
357 fixGroup.addPosition(position, group.length); | |
358 }); | |
359 group.suggestions.forEach((LinkedEditSuggestion suggestion) { | |
360 fixGroup.addSuggestion(suggestion); | |
361 }); | |
362 }); | |
363 // add exit position | |
364 { | |
365 int exitOffset = builder.exitOffset; | |
366 if (exitOffset != null) { | |
367 exitPosition = _newPosition(exitOffset); | |
368 } | |
369 } | |
370 } | |
371 | |
372 bool _isEmptyExpression(Expression expr) { | |
373 if (expr is! SimpleIdentifier) { | |
374 return false; | |
375 } | |
376 SimpleIdentifier id = expr as SimpleIdentifier; | |
377 return id.length == 0; | |
378 } | |
379 | |
380 Position _newPosition(int offset) { | |
381 return new Position(file, offset); | |
382 } | |
383 | |
384 void _setCompletion(StatementCompletionKind kind, [List args]) { | |
385 assert(exitPosition != null); | |
386 change.selection = exitPosition; | |
387 change.message = formatList(kind.message, args); | |
388 linkedPositionGroups.values | |
389 .forEach((group) => change.addLinkedEditGroup(group)); | |
390 completion = new StatementCompletion(kind, change); | |
391 } | |
392 | |
393 void _setCompletionAt(StatementCompletionKind kind, int offset, [List args]) { | |
394 exitPosition = _newPosition(offset); | |
395 _setCompletion(kind, args); | |
396 } | |
397 } | |
398 | |
399 // Encapsulate common structure of if-statement and while-statement. | |
400 class _IfWhileStructure { | |
401 final Token keyword; | |
402 final Token leftParenthesis, rightParenthesis; | |
403 final Expression condition; | |
404 final Statement block; | |
405 | |
406 _IfWhileStructure(this.keyword, this.leftParenthesis, this.condition, | |
407 this.rightParenthesis, this.block); | |
408 | |
409 int get offset => keyword.offset; | |
410 } | |
OLD | NEW |