Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(9)

Unified Diff: pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart

Issue 2803313002: Statement completion framework with a few examples (Closed)
Patch Set: Address review comments Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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;
+}
« no previous file with comments | « pkg/analysis_server/lib/src/edit/edit_domain.dart ('k') | pkg/analysis_server/test/edit/statement_completion_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698