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( |
| 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 String file; |
| 69 final LineInfo lineInfo; |
| 70 final int selectionOffset; |
| 71 final CompilationUnit unit; |
| 72 final CompilationUnitElement unitElement; |
| 73 final List<engine.AnalysisError> errors; |
| 74 |
| 75 StatementCompletionContext(this.file, this.lineInfo, this.selectionOffset, |
| 76 this.unit, this.unitElement, this.errors) { |
| 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 statement, 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 (statement.leftParenthesis.lexeme.isEmpty) { |
| 230 if (!statement.rightParenthesis.lexeme.isEmpty) { |
| 231 // Quite unlikely to see this so don't try to fix it. |
| 232 return false; |
| 233 } |
| 234 int len = statement.keyword.length; |
| 235 if (text.length == len || |
| 236 !text.substring(len, len + 1).contains(new RegExp(r'\s'))) { |
| 237 sb = new SourceBuilder(file, statement.offset + len); |
| 238 sb.append(' '); |
| 239 } else { |
| 240 sb = new SourceBuilder(file, statement.offset + len + 1); |
| 241 } |
| 242 sb.append('('); |
| 243 sb.setExitOffset(); |
| 244 sb.append(')'); |
| 245 } else { |
| 246 if (_isEmptyExpression(statement.condition)) { |
| 247 exitPosition = _newPosition(statement.leftParenthesis.offset + 1); |
| 248 sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1); |
| 249 } else { |
| 250 sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1); |
| 251 needsExit = true; |
| 252 } |
| 253 } |
| 254 if (statement.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 || node is! IfStatement) { |
| 264 return false; |
| 265 } |
| 266 IfStatement ifNode = node; |
| 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 || node is! WhileStatement) { |
| 310 return false; |
| 311 } |
| 312 WhileStatement whileNode = node; |
| 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 |