Chromium Code Reviews| 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 |