| 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..73fe15ed155a325e5c6fee421bbb5128c03fce0a
|
| --- /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(
|
| + '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 String file;
|
| + final LineInfo lineInfo;
|
| + final int selectionOffset;
|
| + final CompilationUnit unit;
|
| + final CompilationUnitElement unitElement;
|
| + final List<engine.AnalysisError> errors;
|
| +
|
| + StatementCompletionContext(this.file, this.lineInfo, this.selectionOffset,
|
| + this.unit, this.unitElement, this.errors) {
|
| + 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 statement, 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 (statement.leftParenthesis.lexeme.isEmpty) {
|
| + if (!statement.rightParenthesis.lexeme.isEmpty) {
|
| + // Quite unlikely to see this so don't try to fix it.
|
| + return false;
|
| + }
|
| + int len = statement.keyword.length;
|
| + if (text.length == len ||
|
| + !text.substring(len, len + 1).contains(new RegExp(r'\s'))) {
|
| + sb = new SourceBuilder(file, statement.offset + len);
|
| + sb.append(' ');
|
| + } else {
|
| + sb = new SourceBuilder(file, statement.offset + len + 1);
|
| + }
|
| + sb.append('(');
|
| + sb.setExitOffset();
|
| + sb.append(')');
|
| + } else {
|
| + if (_isEmptyExpression(statement.condition)) {
|
| + exitPosition = _newPosition(statement.leftParenthesis.offset + 1);
|
| + sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
|
| + } else {
|
| + sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
|
| + needsExit = true;
|
| + }
|
| + }
|
| + if (statement.block is EmptyStatement) {
|
| + _appendEmptyBraces(sb, needsExit);
|
| + }
|
| + _insertBuilder(sb);
|
| + _setCompletion(kind);
|
| + return true;
|
| + }
|
| +
|
| + bool _complete_ifStatement() {
|
| + if (errors.isEmpty || node is! IfStatement) {
|
| + return false;
|
| + }
|
| + IfStatement ifNode = node;
|
| + 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 || node is! WhileStatement) {
|
| + return false;
|
| + }
|
| + WhileStatement whileNode = node;
|
| + 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;
|
| +}
|
|
|