Chromium Code Reviews| Index: pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart |
| diff --git a/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart b/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..2814dfa15856e1c04b03192d91f5fdc323c8985b |
| --- /dev/null |
| +++ b/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart |
| @@ -0,0 +1,410 @@ |
| +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +library services.src.completion.statement; |
| + |
| +import 'dart:async'; |
| + |
| +import 'package:analysis_server/plugin/protocol/protocol.dart'; |
| +import 'package:analysis_server/src/protocol_server.dart' hide Element; |
| +import 'package:analysis_server/src/services/correction/source_buffer.dart'; |
| +import 'package:analysis_server/src/services/correction/source_range.dart'; |
| +import 'package:analysis_server/src/services/correction/util.dart'; |
| +import 'package:analyzer/dart/ast/ast.dart'; |
| +import 'package:analyzer/dart/ast/token.dart'; |
| +import 'package:analyzer/dart/element/element.dart'; |
| +import 'package:analyzer/error/error.dart'; |
| +import 'package:analyzer/error/error.dart' as engine; |
| +import 'package:analyzer/src/dart/ast/utilities.dart'; |
| +import 'package:analyzer/src/dart/error/hint_codes.dart'; |
| +import 'package:analyzer/src/dart/error/syntactic_errors.dart'; |
| +import 'package:analyzer/src/generated/engine.dart'; |
| +import 'package:analyzer/src/generated/java_core.dart'; |
| +import 'package:analyzer/src/generated/source.dart'; |
| + |
| +/** |
| + * An enumeration of possible statement completion kinds. |
| + */ |
| +class DartStatementCompletion { |
| + static const NO_COMPLETION = |
| + const StatementCompletionKind('No_COMPLETION', 'No completion available'); |
| + 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.
|
| + 'PLAIN_OLE_ENTER', "Insert a newline at the end of the current line"); |
| + static const SIMPLE_SEMICOLON = const StatementCompletionKind( |
| + 'SIMPLE_SEMICOLON', "Add a semicolon and newline"); |
| + static const COMPLETE_IF_STMT = const StatementCompletionKind( |
| + 'COMPLETE_IF_STMT', "Complete if-statement"); |
| + static const COMPLETE_WHILE_STMT = const StatementCompletionKind( |
| + 'COMPLETE_WHILE_STMT', "Complete while-statement"); |
| +} |
| + |
| +/** |
| + * A description of a statement completion. |
| + * |
| + * Clients may not extend, implement or mix-in this class. |
| + */ |
| +class StatementCompletion { |
| + /** |
| + * A description of the assist being proposed. |
| + */ |
| + final StatementCompletionKind kind; |
| + |
| + /** |
| + * The change to be made in order to apply the assist. |
| + */ |
| + final SourceChange change; |
| + |
| + /** |
| + * Initialize a newly created completion to have the given [kind] and [change]. |
| + */ |
| + StatementCompletion(this.kind, this.change); |
| +} |
| + |
| +/** |
| + * The context for computing a statement completion. |
| + */ |
| +class StatementCompletionContext { |
| + final CompilationUnitElement unitElement; |
| + final int selectionOffset; |
| + final LineInfo lineInfo; |
| + final List<engine.AnalysisError> errors; |
| + final CompilationUnit unit; |
| + 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
|
| + |
| + StatementCompletionContext(this.unitElement, this.selectionOffset, |
| + this.lineInfo, this.errors, this.file, this.unit) { |
| + if (unitElement.context == null) { |
| + throw new Error(); // not reached; see getStatementCompletion() |
| + } |
| + } |
| +} |
| + |
| +/** |
| + * A description of a class of statement completions. Instances are intended to |
| + * hold the information that is common across a number of completions and to be |
| + * shared by those completions. |
| + * |
| + * Clients may not extend, implement or mix-in this class. |
| + */ |
| +class StatementCompletionKind { |
| + /** |
| + * The name of this kind of statement completion, used for debugging. |
| + */ |
| + final String name; |
| + |
| + /** |
| + * A human-readable description of the changes that will be applied by this |
| + * kind of statement completion. |
| + */ |
| + final String message; |
| + |
| + /** |
| + * Initialize a newly created kind of statement completion to have the given |
| + * [name] and [message]. |
| + */ |
| + const StatementCompletionKind(this.name, this.message); |
| + |
| + @override |
| + String toString() => name; |
| +} |
| + |
| +/** |
| + * The computer for Dart statement completions. |
| + */ |
| +class StatementCompletionProcessor { |
| + static final NO_COMPLETION = new StatementCompletion( |
| + DartStatementCompletion.NO_COMPLETION, new SourceChange("", edits: [])); |
| + |
| + final StatementCompletionContext statementContext; |
| + final AnalysisContext analysisContext; |
| + final CorrectionUtils utils; |
| + int fileStamp; |
| + AstNode node; |
| + StatementCompletion completion; |
| + SourceChange change = new SourceChange('statement-completion'); |
| + List errors = <engine.AnalysisError>[]; |
| + final Map<String, LinkedEditGroup> linkedPositionGroups = |
| + <String, LinkedEditGroup>{}; |
| + Position exitPosition = null; |
| + |
| + StatementCompletionProcessor(this.statementContext) |
| + : analysisContext = statementContext.unitElement.context, |
| + utils = new CorrectionUtils(statementContext.unit) { |
| + fileStamp = analysisContext.getModificationStamp(source); |
| + } |
| + |
| + String get eol => utils.endOfLine; |
| + |
| + String get file => statementContext.file; |
| + |
| + LineInfo get lineInfo => statementContext.lineInfo; |
| + |
| + int get requestLine => lineInfo.getLocation(selectionOffset).lineNumber; |
| + |
| + int get selectionOffset => statementContext.selectionOffset; |
| + |
| + Source get source => statementContext.unitElement.source; |
| + |
| + CompilationUnit get unit => statementContext.unit; |
| + |
| + CompilationUnitElement get unitElement => statementContext.unitElement; |
| + |
| + Future<StatementCompletion> compute() async { |
| + // If the source was changed between the constructor and running |
| + // this asynchronous method, it is not safe to use the unit. |
| + if (analysisContext.getModificationStamp(source) != fileStamp) { |
| + return NO_COMPLETION; |
| + } |
| + node = new NodeLocator(selectionOffset).searchWithin(unit); |
| + if (node == null) { |
| + return NO_COMPLETION; |
| + } |
| + // TODO(messick): This needs to work for declarations. |
| + node = node.getAncestor((n) => n is Statement); |
| + for (engine.AnalysisError error in statementContext.errors) { |
| + if (error.offset >= node.offset && |
| + error.offset <= node.offset + node.length) { |
| + if (error.errorCode is! HintCode) { |
| + errors.add(error); |
| + } |
| + } |
| + } |
| + |
| + if (_complete_ifStatement() || |
| + _complete_whileStatement() || |
| + _complete_simpleSemicolon() || |
| + _complete_plainOleEnter()) { |
| + return completion; |
| + } |
| + return NO_COMPLETION; |
| + } |
| + |
| + void _addIndentEdit(SourceRange range, String oldIndent, String newIndent) { |
| + SourceEdit edit = utils.createIndentEdit(range, oldIndent, newIndent); |
| + doSourceChange_addElementEdit(change, unitElement, edit); |
| + } |
| + |
| + void _addInsertEdit(int offset, String text) { |
| + SourceEdit edit = new SourceEdit(offset, 0, text); |
| + doSourceChange_addElementEdit(change, unitElement, edit); |
| + } |
| + |
| + void _addReplaceEdit(SourceRange range, String text) { |
| + SourceEdit edit = new SourceEdit(range.offset, range.length, text); |
| + doSourceChange_addElementEdit(change, unitElement, edit); |
| + } |
| + |
| + void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) { |
| + sb.append(' {'); |
| + sb.append(eol); |
| + String indent = utils.getLinePrefix(selectionOffset); |
| + sb.append(indent); |
| + sb.append(utils.getIndent(1)); |
| + if (needsExitMark) { |
| + sb.setExitOffset(); |
| + } |
| + sb.append(eol); |
| + sb.append(indent); |
| + sb.append('}'); |
| + } |
| + |
| + int _appendNewlinePlusIndent() { |
| + // Append a newline plus proper indent and another newline. |
| + // Return the position before the second newline. |
| + String indent = utils.getLinePrefix(selectionOffset); |
| + int loc = utils.getLineNext(selectionOffset); |
| + _addInsertEdit(loc, indent + eol); |
| + return loc + indent.length; |
| + } |
| + |
| + bool _complete_ifOrWhileStatement( |
| + _IfWhileStructure ifNode, StatementCompletionKind kind) { |
| + String text = utils.getNodeText(node); |
| + if (text.endsWith(eol)) { |
| + text = text.substring(0, text.length - eol.length); |
| + } |
| + SourceBuilder sb; |
| + bool needsExit = false; |
| + if (ifNode.leftParenthesis.lexeme.isEmpty) { |
| + if (!ifNode.rightParenthesis.lexeme.isEmpty) { |
| + // Quite unlikely to see this so don't try to fix it. |
| + return false; |
| + } |
| + int len = ifNode.keyword.length; |
| + if (text.length == len || |
| + !text.substring(len, len + 1).contains(new RegExp(r'\s'))) { |
| + sb = new SourceBuilder(file, ifNode.offset + len); |
| + sb.append(' '); |
| + } else { |
| + sb = new SourceBuilder(file, ifNode.offset + len + 1); |
| + } |
| + sb.append('('); |
| + sb.setExitOffset(); |
| + sb.append(')'); |
| + } else { |
| + if (_isEmptyExpression(ifNode.condition)) { |
| + exitPosition = _newPosition(ifNode.leftParenthesis.offset + 1); |
| + sb = new SourceBuilder(file, ifNode.rightParenthesis.offset + 1); |
| + } else { |
| + sb = new SourceBuilder(file, ifNode.rightParenthesis.offset + 1); |
| + needsExit = true; |
| + } |
| + } |
| + if (ifNode.block is EmptyStatement) { |
| + _appendEmptyBraces(sb, needsExit); |
| + } |
| + _insertBuilder(sb); |
| + _setCompletion(kind); |
| + return true; |
| + } |
| + |
| + bool _complete_ifStatement() { |
| + if (errors.isEmpty) { |
| + return false; |
| + } |
| + 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
|
| + if (ifNode != null) { |
| + if (ifNode.elseKeyword != null) { |
| + return false; |
| + } |
| + var stmt = new _IfWhileStructure(ifNode.ifKeyword, ifNode.leftParenthesis, |
| + ifNode.condition, ifNode.rightParenthesis, ifNode.thenStatement); |
| + return _complete_ifOrWhileStatement( |
| + stmt, DartStatementCompletion.COMPLETE_IF_STMT); |
| + } |
| + return false; |
| + } |
| + |
| + bool _complete_plainOleEnter() { |
| + int offset; |
| + if (!errors.isEmpty) { |
| + offset = selectionOffset; |
| + } else { |
| + String indent = utils.getLinePrefix(selectionOffset); |
| + int loc = utils.getLineNext(selectionOffset); |
| + _addInsertEdit(loc, indent + eol); |
| + offset = loc + indent.length + eol.length; |
| + } |
| + _setCompletionAt(DartStatementCompletion.PLAIN_OLE_ENTER, offset); |
| + return true; |
| + } |
| + |
| + bool _complete_simpleSemicolon() { |
| + if (errors.length != 1) { |
| + return false; |
| + } |
| + var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'"); |
| + if (error != null) { |
| + int insertOffset = error.offset + error.length; |
| + _addInsertEdit(insertOffset, ';'); |
| + int offset = _appendNewlinePlusIndent() + 1 /* ';' */; |
| + _setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset); |
| + return true; |
| + } |
| + return false; |
| + } |
| + |
| + bool _complete_whileStatement() { |
| + if (errors.isEmpty) { |
| + return false; |
| + } |
| + WhileStatement whileNode = node.getAncestor((n) => n is WhileStatement); |
| + if (whileNode != null) { |
| + var stmt = new _IfWhileStructure( |
| + whileNode.whileKeyword, |
| + whileNode.leftParenthesis, |
| + whileNode.condition, |
| + whileNode.rightParenthesis, |
| + whileNode.body); |
| + return _complete_ifOrWhileStatement( |
| + stmt, DartStatementCompletion.COMPLETE_WHILE_STMT); |
| + } |
| + return false; |
| + } |
| + |
| + engine.AnalysisError _findError(ErrorCode code, {partialMatch: null}) { |
| + var error = |
| + errors.firstWhere((err) => err.errorCode == code, orElse: () => null); |
| + if (error != null) { |
| + if (partialMatch != null) { |
| + return error.message.contains(partialMatch) ? error : null; |
| + } |
| + return error; |
| + } |
| + return null; |
| + } |
| + |
| + LinkedEditGroup _getLinkedPosition(String groupId) { |
| + LinkedEditGroup group = linkedPositionGroups[groupId]; |
| + if (group == null) { |
| + group = new LinkedEditGroup.empty(); |
| + linkedPositionGroups[groupId] = group; |
| + } |
| + return group; |
| + } |
| + |
| + void _insertBuilder(SourceBuilder builder, [int length = 0]) { |
| + { |
| + SourceRange range = rangeStartLength(builder.offset, length); |
| + String text = builder.toString(); |
| + _addReplaceEdit(range, text); |
| + } |
| + // add linked positions |
| + builder.linkedPositionGroups.forEach((String id, LinkedEditGroup group) { |
| + LinkedEditGroup fixGroup = _getLinkedPosition(id); |
| + group.positions.forEach((Position position) { |
| + fixGroup.addPosition(position, group.length); |
| + }); |
| + group.suggestions.forEach((LinkedEditSuggestion suggestion) { |
| + fixGroup.addSuggestion(suggestion); |
| + }); |
| + }); |
| + // add exit position |
| + { |
| + int exitOffset = builder.exitOffset; |
| + if (exitOffset != null) { |
| + exitPosition = _newPosition(exitOffset); |
| + } |
| + } |
| + } |
| + |
| + bool _isEmptyExpression(Expression expr) { |
| + if (expr is! SimpleIdentifier) { |
| + return false; |
| + } |
| + SimpleIdentifier id = expr as SimpleIdentifier; |
| + return id.length == 0; |
| + } |
| + |
| + Position _newPosition(int offset) { |
| + return new Position(file, offset); |
| + } |
| + |
| + void _setCompletion(StatementCompletionKind kind, [List args]) { |
| + assert(exitPosition != null); |
| + change.selection = exitPosition; |
| + change.message = formatList(kind.message, args); |
| + linkedPositionGroups.values |
| + .forEach((group) => change.addLinkedEditGroup(group)); |
| + completion = new StatementCompletion(kind, change); |
| + } |
| + |
| + void _setCompletionAt(StatementCompletionKind kind, int offset, [List args]) { |
| + exitPosition = _newPosition(offset); |
| + _setCompletion(kind, args); |
| + } |
| +} |
| + |
| +// Encapsulate common structure of if-statement and while-statement. |
| +class _IfWhileStructure { |
| + final Token keyword; |
| + final Token leftParenthesis, rightParenthesis; |
| + final Expression condition; |
| + final Statement block; |
| + |
| + _IfWhileStructure(this.keyword, this.leftParenthesis, this.condition, |
| + this.rightParenthesis, this.block); |
| + |
| + int get offset => keyword.offset; |
| +} |