| 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
|
| index 167efb932403aa14c5b416bcd1713a19cd3c0690..8326733fac1f314befa75f4faaa67a010bb06351 100644
|
| --- a/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart
|
| +++ b/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart
|
| @@ -5,6 +5,7 @@
|
| library services.src.completion.statement;
|
|
|
| import 'dart:async';
|
| +import 'dart:math';
|
|
|
| import 'package:analysis_server/protocol/protocol_generated.dart';
|
| import 'package:analysis_server/src/protocol_server.dart' hide Element;
|
| @@ -18,6 +19,7 @@ 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/error/codes.dart';
|
| import 'package:analyzer/src/generated/engine.dart';
|
| import 'package:analyzer/src/generated/java_core.dart';
|
| import 'package:analyzer/src/generated/source.dart';
|
| @@ -201,6 +203,7 @@ class StatementCompletionProcessor {
|
| }
|
| }
|
|
|
| + _checkExpressions();
|
| if (node is Statement) {
|
| if (errors.isEmpty) {
|
| if (_complete_ifStatement() ||
|
| @@ -285,6 +288,102 @@ class StatementCompletionProcessor {
|
| return text;
|
| }
|
|
|
| + void _checkExpressions() {
|
| + // Note: This may queue edits that have to be accounted for later.
|
| + // See _lengthOfInsertions().
|
| + AstNode errorMatching(errorCode, {partialMatch = null}) {
|
| + var error = _findError(errorCode, partialMatch: partialMatch);
|
| + if (error == null) {
|
| + return null;
|
| + }
|
| + AstNode expr = _selectedNode();
|
| + return (expr.getAncestor((n) => n is StringInterpolation) == null)
|
| + ? expr
|
| + : null;
|
| + }
|
| +
|
| + var expr = errorMatching(ScannerErrorCode.UNTERMINATED_STRING_LITERAL);
|
| + if (expr != null) {
|
| + String source = utils.getNodeText(expr);
|
| + String content = source;
|
| + int char = content.codeUnitAt(0);
|
| + if (char == 'r'.codeUnitAt(0)) {
|
| + content = source.substring(1);
|
| + char = content.codeUnitAt(0);
|
| + }
|
| + String delimiter;
|
| + int loc;
|
| + if (content.length >= 3 &&
|
| + char == content.codeUnitAt(1) &&
|
| + char == content.codeUnitAt(2)) {
|
| + // multi-line string
|
| + delimiter = content.substring(0, 3);
|
| + int newlineLoc = source.indexOf(eol, selectionOffset - expr.offset);
|
| + if (newlineLoc < 0) {
|
| + newlineLoc = source.length;
|
| + }
|
| + loc = newlineLoc + expr.offset;
|
| + } else {
|
| + // add first char of src
|
| + delimiter = content.substring(0, 1);
|
| + loc = expr.offset + source.length;
|
| + }
|
| + _removeError(ScannerErrorCode.UNTERMINATED_STRING_LITERAL);
|
| + _addInsertEdit(loc, delimiter);
|
| + }
|
| + expr = errorMatching(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "']'");
|
| + if (expr != null) {
|
| + expr = expr.getAncestor((n) => n is ListLiteral);
|
| + if (expr != null) {
|
| + ListLiteral lit = expr;
|
| + if (lit.rightBracket.isSynthetic) {
|
| + String src = utils.getNodeText(expr).trim();
|
| + int loc = expr.offset + src.length;
|
| + if (src.contains(eol)) {
|
| + String indent = utils.getNodePrefix(node);
|
| + _addInsertEdit(loc, ',' + eol + indent + ']');
|
| + } else {
|
| + _addInsertEdit(loc, ']');
|
| + }
|
| + _removeError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "']'");
|
| + var ms =
|
| + _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
|
| + if (ms != null) {
|
| + // Ensure the semicolon gets inserted in the correct location.
|
| + ms.offset = loc - 1;
|
| + }
|
| + }
|
| + }
|
| + }
|
| + // The following code is similar to the code for ']' but does not work well.
|
| + // A closing brace is recognized as belong to the map even if it is intended
|
| + // to close a block of code.
|
| + /*
|
| + expr = errorMatching(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "'}'");
|
| + if (expr != null) {
|
| + expr = expr.getAncestor((n) => n is MapLiteral);
|
| + if (expr != null) {
|
| + MapLiteral lit = expr;
|
| + String src = utils.getNodeText(expr).trim();
|
| + int loc = expr.offset + src.length;
|
| + if (lit.entries.last.separator.isSynthetic) {
|
| + _addInsertEdit(loc, ': ');
|
| + }
|
| + if (!src.endsWith('}')/*lit.rightBracket.isSynthetic*/) {
|
| + _addInsertEdit(loc, '}');
|
| + }
|
| + _removeError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "'}'");
|
| + var ms =
|
| + _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
|
| + if (ms != null) {
|
| + // Ensure the semicolon gets inserted in the correct location.
|
| + ms.offset = loc - 1;
|
| + }
|
| + }
|
| + }
|
| + */
|
| + }
|
| +
|
| bool _complete_classDeclaration() {
|
| if (node is! ClassDeclaration) {
|
| return false;
|
| @@ -322,6 +421,7 @@ class StatementCompletionProcessor {
|
| outer is WhileStatement)) {
|
| return false;
|
| }
|
| + int previousInsertions = _lengthOfInsertions();
|
| int delta = 0;
|
| if (errors.isNotEmpty) {
|
| var error =
|
| @@ -349,7 +449,7 @@ class StatementCompletionProcessor {
|
| }
|
| }
|
| int offset = _appendNewlinePlusIndentAt(node.parent.end);
|
| - exitPosition = new Position(file, offset + delta);
|
| + exitPosition = new Position(file, offset + delta + previousInsertions);
|
| _setCompletion(DartStatementCompletion.COMPLETE_CONTROL_FLOW_BLOCK);
|
| return true;
|
| }
|
| @@ -486,7 +586,7 @@ class StatementCompletionProcessor {
|
| }
|
| ForStatement forNode = node;
|
| SourceBuilder sb;
|
| - int delta = 0;
|
| + int replacementLength = 0;
|
| if (forNode.leftParenthesis.isSynthetic) {
|
| if (!forNode.rightParenthesis.isSynthetic) {
|
| return false;
|
| @@ -499,31 +599,40 @@ class StatementCompletionProcessor {
|
| } else {
|
| if (!forNode.rightSeparator.isSynthetic) {
|
| // Fully-defined init, cond, updaters so nothing more needed here.
|
| - // emptyParts
|
| + // emptyParts, noError
|
| sb = new SourceBuilder(file, forNode.rightParenthesis.offset + 1);
|
| } else if (!forNode.leftSeparator.isSynthetic) {
|
| if (_isSyntheticExpression(forNode.condition)) {
|
| - exitPosition = _newPosition(forNode.leftSeparator.offset + 1);
|
| String text = utils
|
| .getNodeText(forNode)
|
| .substring(forNode.leftSeparator.offset - forNode.offset);
|
| - if (text.startsWith(new RegExp(r';\s*\)'))) {
|
| - // emptyCondition
|
| - int end = text.indexOf(')');
|
| + Match match =
|
| + new RegExp(r';\s*(/\*.*\*/\s*)?\)[ \t]*').matchAsPrefix(text);
|
| + if (match != null) {
|
| + // emptyCondition, emptyInitializersEmptyCondition
|
| + replacementLength = match.end - match.start;
|
| sb = new SourceBuilder(file, forNode.leftSeparator.offset);
|
| - _addReplaceEdit(new SourceRange(sb.offset, end), '; ; ');
|
| - delta = end - '; '.length;
|
| + sb.append('; ${match.group(1) == null ? '' : match.group(1)}; )');
|
| + String suffix = text.substring(match.end);
|
| + if (suffix.trim().isNotEmpty) {
|
| + sb.append(' ');
|
| + sb.append(suffix.trim());
|
| + replacementLength += suffix.length;
|
| + if (suffix.endsWith(eol)) {
|
| + // emptyCondition
|
| + replacementLength -= eol.length;
|
| + }
|
| + }
|
| + exitPosition = _newPosition(forNode.leftSeparator.offset + 2);
|
| } else {
|
| - // emptyInitializersEmptyCondition
|
| - exitPosition = _newPosition(forNode.rightParenthesis.offset);
|
| - sb = new SourceBuilder(file, forNode.rightParenthesis.offset);
|
| + return false; // Line comment in condition
|
| }
|
| } else {
|
| // emptyUpdaters
|
| - exitPosition = _newPosition(forNode.rightSeparator.offset);
|
| - sb = new SourceBuilder(file, forNode.rightSeparator.offset);
|
| - _addReplaceEdit(new SourceRange(sb.offset, 0), '; ');
|
| - delta = -'; '.length;
|
| + sb = new SourceBuilder(file, forNode.rightParenthesis.offset);
|
| + replacementLength = 1;
|
| + sb.append('; )');
|
| + exitPosition = _newPosition(forNode.rightSeparator.offset + 2);
|
| }
|
| } else if (_isSyntheticExpression(forNode.initialization)) {
|
| // emptyInitializers
|
| @@ -537,9 +646,8 @@ class StatementCompletionProcessor {
|
| // missingLeftSeparator
|
| int end = text.indexOf(')');
|
| sb = new SourceBuilder(file, start);
|
| - _addReplaceEdit(new SourceRange(start, end), '; ');
|
| - delta = end - '; '.length;
|
| - exitPosition = new Position(file, start);
|
| + _addReplaceEdit(new SourceRange(start, end), '; ; ');
|
| + exitPosition = new Position(file, start - (end - '; '.length));
|
| } else {
|
| // Not possible; any comment following init is attached to init.
|
| exitPosition = _newPosition(forNode.rightParenthesis.offset);
|
| @@ -550,13 +658,16 @@ class StatementCompletionProcessor {
|
| if (!_statementHasValidBody(forNode.forKeyword, forNode.body)) {
|
| // keywordOnly, noError
|
| sb.append(' ');
|
| - _appendEmptyBraces(sb, exitPosition == null);
|
| - }
|
| - if (delta != 0 && exitPosition != null) {
|
| - // missingLeftSeparator
|
| - exitPosition = new Position(file, exitPosition.offset - delta);
|
| + _appendEmptyBraces(sb, true /*exitPosition == null*/);
|
| + } else if (forNode.body is Block) {
|
| + Block body = forNode.body;
|
| + if (body.rightBracket.end <= selectionOffset) {
|
| + // emptyInitializersAfterBody
|
| + errors = []; // Ignore errors; they are for previous statement.
|
| + return false; // If cursor is after closing brace just add newline.
|
| + }
|
| }
|
| - _insertBuilder(sb);
|
| + _insertBuilder(sb, replacementLength);
|
| _setCompletion(DartStatementCompletion.COMPLETE_FOR_STMT);
|
| return true;
|
| }
|
| @@ -565,7 +676,29 @@ class StatementCompletionProcessor {
|
| if (node is! MethodDeclaration && node is! FunctionDeclaration) {
|
| return false;
|
| }
|
| - SourceBuilder sb = new SourceBuilder(file, node.end - 1);
|
| + bool needsParen = false;
|
| + int computeExitPos(FormalParameterList parameters) {
|
| + if (needsParen = parameters.rightParenthesis.isSynthetic) {
|
| + var error = _findError(ParserErrorCode.MISSING_CLOSING_PARENTHESIS);
|
| + if (error != null) {
|
| + return error.offset - 1;
|
| + }
|
| + }
|
| + return node.end - 1;
|
| + }
|
| +
|
| + int paramListEnd;
|
| + if (node is FunctionDeclaration) {
|
| + FunctionDeclaration func = node;
|
| + paramListEnd = computeExitPos(func.functionExpression.parameters);
|
| + } else {
|
| + MethodDeclaration meth = node;
|
| + paramListEnd = computeExitPos(meth.parameters);
|
| + }
|
| + SourceBuilder sb = new SourceBuilder(file, paramListEnd);
|
| + if (needsParen) {
|
| + sb.append(')');
|
| + }
|
| sb.append(' ');
|
| _appendEmptyBraces(sb, true);
|
| _insertBuilder(sb);
|
| @@ -621,9 +754,13 @@ class StatementCompletionProcessor {
|
| if (sb == null) {
|
| return false;
|
| }
|
| + int overshoot = _lengthOfDeletions();
|
| sb.append(' ');
|
| _appendEmptyBraces(sb, exitPosition == null);
|
| _insertBuilder(sb);
|
| + if (overshoot != 0) {
|
| + exitPosition = _newPosition(exitPosition.offset - overshoot);
|
| + }
|
| _setCompletion(kind);
|
| return true;
|
| }
|
| @@ -634,6 +771,20 @@ class StatementCompletionProcessor {
|
| }
|
| IfStatement ifNode = node;
|
| if (ifNode.elseKeyword != null) {
|
| + if (selectionOffset >= ifNode.elseKeyword.end &&
|
| + ifNode.elseStatement is EmptyStatement) {
|
| + SourceBuilder sb = new SourceBuilder(file, selectionOffset);
|
| + String src = utils.getNodeText(ifNode);
|
| + if (!src
|
| + .substring(ifNode.elseKeyword.end - node.offset)
|
| + .startsWith(new RegExp(r'[ \t]'))) {
|
| + sb.append(' ');
|
| + }
|
| + _appendEmptyBraces(sb, true);
|
| + _insertBuilder(sb);
|
| + _setCompletion(DartStatementCompletion.COMPLETE_IF_STMT);
|
| + return true;
|
| + }
|
| return false;
|
| }
|
| var stmt = new _KeywordConditionBlockStructure(
|
| @@ -663,7 +814,16 @@ class StatementCompletionProcessor {
|
| exitPosition = _newPosition(statement.leftParenthesis.offset + 1);
|
| sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
|
| } else {
|
| - sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
|
| + int afterParen = statement.rightParenthesis.offset + 1;
|
| + if (utils
|
| + .getNodeText(node)
|
| + .substring(afterParen - node.offset)
|
| + .startsWith(new RegExp(r'[ \t]'))) {
|
| + _addReplaceEdit(new SourceRange(afterParen, 1), '');
|
| + sb = new SourceBuilder(file, afterParen + 1);
|
| + } else {
|
| + sb = new SourceBuilder(file, afterParen);
|
| + }
|
| }
|
| }
|
| return sb;
|
| @@ -675,12 +835,17 @@ class StatementCompletionProcessor {
|
| if (parenError == null) {
|
| return false;
|
| }
|
| - AstNode argList = _selectedNode(at: parenError.offset - 1)
|
| + AstNode argList = _selectedNode(at: selectionOffset)
|
| .getAncestor((n) => n is ArgumentList);
|
| + if (argList == null) {
|
| + argList = _selectedNode(at: parenError.offset)
|
| + .getAncestor((n) => n is ArgumentList);
|
| + }
|
| if (argList?.getAncestor((n) => n == node) == null) {
|
| return false;
|
| }
|
| - int loc = argList.end - 1;
|
| + int previousInsertions = _lengthOfInsertions();
|
| + int loc = min(selectionOffset, argList.end - 1);
|
| int delta = 1;
|
| var semicolonError =
|
| _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
|
| @@ -695,7 +860,7 @@ class StatementCompletionProcessor {
|
| String indent = utils.getLinePrefix(selectionOffset);
|
| int exit = utils.getLineNext(selectionOffset);
|
| _addInsertEdit(exit, indent + eol);
|
| - exit += indent.length + eol.length;
|
| + exit += indent.length + eol.length + previousInsertions;
|
|
|
| _setCompletionAt(DartStatementCompletion.SIMPLE_ENTER, exit + delta);
|
| return true;
|
| @@ -709,7 +874,7 @@ class StatementCompletionProcessor {
|
| String indent = utils.getLinePrefix(selectionOffset);
|
| int loc = utils.getLineNext(selectionOffset);
|
| _addInsertEdit(loc, indent + eol);
|
| - offset = loc + indent.length + eol.length;
|
| + offset = loc + indent.length;
|
| }
|
| _setCompletionAt(DartStatementCompletion.SIMPLE_ENTER, offset);
|
| return true;
|
| @@ -721,10 +886,11 @@ class StatementCompletionProcessor {
|
| }
|
| var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
|
| if (error != null) {
|
| + int previousInsertions = _lengthOfInsertions();
|
| // TODO(messick) Fix this to find the correct place in all cases.
|
| int insertOffset = error.offset + error.length;
|
| _addInsertEdit(insertOffset, ';');
|
| - int offset = _appendNewlinePlusIndent() + 1 /* ';' */;
|
| + int offset = _appendNewlinePlusIndent() + 1 /*';'*/ + previousInsertions;
|
| _setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset);
|
| return true;
|
| }
|
| @@ -808,7 +974,10 @@ class StatementCompletionProcessor {
|
| } else if ((catchNode = _findInvalidElement(tryNode.catchClauses)) !=
|
| null) {
|
| if (catchNode.onKeyword != null) {
|
| - if (catchNode.exceptionType.length == 0) {
|
| + if (catchNode.exceptionType.length == 0 ||
|
| + null !=
|
| + _findError(StaticWarningCode.NON_TYPE_IN_CATCH_CLAUSE,
|
| + partialMatch: "name 'catch")) {
|
| String src = utils.getNodeText(catchNode);
|
| if (src.startsWith(new RegExp(r'on[ \t]+'))) {
|
| if (src.startsWith(new RegExp(r'on[ \t][ \t]+'))) {
|
| @@ -969,10 +1138,52 @@ class StatementCompletionProcessor {
|
| return expr is SimpleIdentifier && expr.isSynthetic;
|
| }
|
|
|
| + int _lengthOfDeletions() {
|
| + if (change.edits.isEmpty) {
|
| + return 0;
|
| + }
|
| + int length = 0;
|
| + for (SourceFileEdit edit in change.edits) {
|
| + for (SourceEdit srcEdit in edit.edits) {
|
| + if (srcEdit.length > 0) {
|
| + length += srcEdit.length - srcEdit.replacement.length;
|
| + }
|
| + }
|
| + }
|
| + return length;
|
| + }
|
| +
|
| + int _lengthOfInsertions() {
|
| + // Any _complete_*() that may follow changes made by _checkExpressions()
|
| + // must cache the result of this method and add that value to its
|
| + // exit position. That's assuming all edits are done in increasing position.
|
| + // There are currently no editing sequences that produce both insertions and
|
| + // deletions, but if there were this approach would have to be generalized.
|
| + if (change.edits.isEmpty) {
|
| + return 0;
|
| + }
|
| + int length = 0;
|
| + for (SourceFileEdit edit in change.edits) {
|
| + for (SourceEdit srcEdit in edit.edits) {
|
| + if (srcEdit.length == 0) {
|
| + length += srcEdit.replacement.length;
|
| + }
|
| + }
|
| + }
|
| + return length;
|
| + }
|
| +
|
| Position _newPosition(int offset) {
|
| return new Position(file, offset);
|
| }
|
|
|
| + void _removeError(errorCode, {partialMatch = null}) {
|
| + var error = _findError(errorCode, partialMatch: partialMatch);
|
| + if (error != null) {
|
| + errors.remove(error);
|
| + }
|
| + }
|
| +
|
| AstNode _selectedNode({int at: null}) =>
|
| new NodeLocator(at == null ? selectionOffset : at).searchWithin(unit);
|
|
|
| @@ -995,8 +1206,8 @@ class StatementCompletionProcessor {
|
| String text = _baseNodeText(node);
|
| text = text.substring(keyword.offset - node.offset);
|
| int len = keyword.length;
|
| - if (text.length == len ||
|
| - !text.substring(len, len + 1).contains(new RegExp(r'\s'))) {
|
| + if (text.length == len || // onCatchComment
|
| + !text.substring(len, len + 1).contains(new RegExp(r'[ \t]'))) {
|
| sb = new SourceBuilder(file, keyword.offset + len);
|
| sb.append(' ');
|
| } else {
|
|
|