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

Unified Diff: pkg/analysis_server/lib/src/services/completion/postfix/postfix_completion.dart

Issue 2917943002: Postfix completion (Closed)
Patch Set: typo Created 3 years, 6 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/postfix/postfix_completion.dart
diff --git a/pkg/analysis_server/lib/src/services/completion/postfix/postfix_completion.dart b/pkg/analysis_server/lib/src/services/completion/postfix/postfix_completion.dart
new file mode 100644
index 0000000000000000000000000000000000000000..bb9b7b5f4b22fb2f2ea7d7085875464f912cfd1f
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/postfix/postfix_completion.dart
@@ -0,0 +1,583 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:analysis_server/src/protocol_server.dart' hide Element;
+import 'package:analysis_server/src/services/correction/util.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/error/error.dart' as engine;
+import 'package:analyzer/src/dart/analysis/driver.dart';
+import 'package:analyzer/src/dart/ast/utilities.dart';
+import 'package:analyzer/src/generated/engine.dart';
+import 'package:analyzer/src/generated/java_core.dart';
+import 'package:analyzer/src/generated/resolver.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+
+/**
+ * An enumeration of possible postfix completion kinds.
+ */
+class DartPostfixCompletion {
+ static const NO_TEMPLATE =
+ const PostfixCompletionKind('', 'no change', null, null);
+ static const ALL_TEMPLATES = const [
+ const PostfixCompletionKind("assert", "expr.assert -> assert(expr);",
+ isAssertContext, expandAssert),
+ const PostfixCompletionKind(
+ "fori",
+ "limit.fori -> for(var i = 0; i < limit; i++) {}",
+ isIntContext,
+ expandFori),
+ const PostfixCompletionKind(
+ "for",
+ "values.for -> for(var value in values) {}",
+ isIterableContext,
+ expandFor),
+ const PostfixCompletionKind(
+ "iter",
+ "values.iter -> for(var value in values) {}",
+ isIterableContext,
+ expandFor),
+ const PostfixCompletionKind(
+ "not", "bool.not -> !bool", isBoolContext, expandNegate),
+ const PostfixCompletionKind(
+ "!", "bool! -> !bool", isBoolContext, expandNegate),
+ const PostfixCompletionKind(
+ "else", "bool.else -> if (!bool) {}", isBoolContext, expandElse),
+ const PostfixCompletionKind(
+ "if", "bool.if -> if (bool) {}", isBoolContext, expandIf),
+ const PostfixCompletionKind("nn", "expr.nn -> if (expr != null) {}",
+ isObjectContext, expandNotNull),
+ const PostfixCompletionKind("notnull",
+ "expr.notnull -> if (expr != null) {}", isObjectContext, expandNotNull),
+ const PostfixCompletionKind("null", "expr.null -> if (expr == null) {}",
+ isObjectContext, expandNull),
+ const PostfixCompletionKind(
+ "par", "expr.par -> (expr)", isObjectContext, expandParen),
+ const PostfixCompletionKind(
+ "return", "expr.return -> return expr", isObjectContext, expandReturn),
+ const PostfixCompletionKind("switch", "expr.switch -> switch (expr) {}",
+ isSwitchContext, expandSwitch),
+ const PostfixCompletionKind("try", "stmt.try -> try {stmt} catch (e,s) {}",
+ isStatementContext, expandTry),
+ const PostfixCompletionKind(
+ "tryon",
+ "stmt.try -> try {stmt} on Exception catch (e,s) {}",
+ isStatementContext,
+ expandTryon),
+ const PostfixCompletionKind(
+ "while", "expr.while -> while (expr) {}", isBoolContext, expandWhile),
+ ];
+
+ static Future<PostfixCompletion> expandAssert(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findAssertExpression, (expr) {
+ return "assert(${processor.utils.getNodeText(expr)});";
+ }, withBraces: false);
+ }
+
+ static Future<PostfixCompletion> expandElse(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findBoolExpression,
+ (expr) => "if (${processor.makeNegatedBoolExpr(expr)})");
+ }
+
+ static Future<PostfixCompletion> expandFor(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findIterableExpression, (expr) {
+ String value = processor.newVariable("value");
+ return "for (var $value in ${processor.utils.getNodeText(expr)})";
+ });
+ }
+
+ static Future<PostfixCompletion> expandFori(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findIntExpression, (expr) {
+ String index = processor.newVariable("i");
+ return "for (int $index = 0; $index < ${processor.utils.getNodeText(
+ expr)}; $index++)";
+ });
+ }
+
+ static Future<PostfixCompletion> expandIf(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findBoolExpression,
+ (expr) => "if (${processor.utils.getNodeText(expr)})");
+ }
+
+ static Future<PostfixCompletion> expandNegate(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findBoolExpression,
+ (expr) => processor.makeNegatedBoolExpr(expr),
+ withBraces: false);
+ }
+
+ static Future<PostfixCompletion> expandNotNull(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findObjectExpression, (expr) {
+ return expr is NullLiteral
+ ? "if (false)"
+ : "if (${processor.utils.getNodeText(expr)} != null)";
+ });
+ }
+
+ static Future<PostfixCompletion> expandNull(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findObjectExpression, (expr) {
+ return expr is NullLiteral
+ ? "if (true)"
+ : "if (${processor.utils.getNodeText(expr)} == null)";
+ });
+ }
+
+ static Future<PostfixCompletion> expandParen(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findObjectExpression,
+ (expr) => "(${processor.utils.getNodeText(expr)})",
+ withBraces: false);
+ }
+
+ static Future<PostfixCompletion> expandReturn(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findObjectExpression,
+ (expr) => "return ${processor.utils.getNodeText(expr)};",
+ withBraces: false);
+ }
+
+ static Future<PostfixCompletion> expandSwitch(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findObjectExpression,
+ (expr) => "switch (${processor.utils.getNodeText(expr)})");
+ }
+
+ static Future<PostfixCompletion> expandTry(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expandTry(kind, processor.findStatement, withOn: false);
+ }
+
+ static Future<PostfixCompletion> expandTryon(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expandTry(kind, processor.findStatement, withOn: true);
+ }
+
+ static Future<PostfixCompletion> expandWhile(
+ PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
+ return processor.expand(kind, processor.findBoolExpression,
+ (expr) => "while (${processor.utils.getNodeText(expr)})");
+ }
+
+ static PostfixCompletionKind forKey(String key) =>
+ ALL_TEMPLATES.firstWhere((kind) => kind.key == key, orElse: () => null);
+
+ static bool isAssertContext(PostfixCompletionProcessor processor) {
+ return processor.findAssertExpression() != null;
+ }
+
+ static bool isBoolContext(PostfixCompletionProcessor processor) {
+ return processor.findBoolExpression() != null;
+ }
+
+ static bool isIntContext(PostfixCompletionProcessor processor) {
+ return processor.findIntExpression() != null;
+ }
+
+ static bool isIterableContext(PostfixCompletionProcessor processor) {
+ return processor.findIterableExpression() != null;
+ }
+
+ static bool isObjectContext(PostfixCompletionProcessor processor) {
+ return processor.findObjectExpression() != null;
+ }
+
+ static bool isStatementContext(PostfixCompletionProcessor processor) {
+ return processor.findStatement() != null;
+ }
+
+ static bool isSwitchContext(PostfixCompletionProcessor processor) {
+ return processor.findObjectExpression() != null;
+ }
+}
+
+/**
+ * A description of a postfix completion.
+ *
+ * Clients may not extend, implement or mix-in this class.
+ */
+class PostfixCompletion {
+ /**
+ * A description of the assist being proposed.
+ */
+ final PostfixCompletionKind 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].
+ */
+ PostfixCompletion(this.kind, this.change);
+}
+
+/**
+ * The context for computing a postfix completion.
+ */
+class PostfixCompletionContext {
+ final String file;
+ final LineInfo lineInfo;
+ final int selectionOffset;
+ final String key;
+ final AnalysisDriver driver;
+ final CompilationUnit unit;
+ final CompilationUnitElement unitElement;
+ final List<engine.AnalysisError> errors;
+
+ PostfixCompletionContext(this.file, this.lineInfo, this.selectionOffset,
+ this.key, this.driver, this.unit, this.unitElement, this.errors) {
+ if (unitElement.context == null) {
+ throw new Error(); // not reached
+ }
+ }
+}
+
+/**
+ * A description of a template for postfix completion. Instances are intended to
+ * hold the functions required to determine applicability and expand the
+ * template, in addition to its name and simple example. The example is shown
+ * (in IntelliJ) in a code-completion menu, so must be quite short.
+ *
+ * Clients may not extend, implement or mix-in this class.
+ */
+class PostfixCompletionKind {
+ final String name, example;
+ final Function selector;
+ final Function computer;
+
+ const PostfixCompletionKind(
+ this.name, this.example, this.selector, this.computer);
+
+ String get key => name == '!' ? name : '.$name';
+
+ String get message => 'Expand $key';
+
+ @override
+ String toString() => name;
+}
+
+/**
+ * The computer for Dart postfix completions.
+ */
+class PostfixCompletionProcessor {
+ static final NO_COMPLETION = new PostfixCompletion(
+ DartPostfixCompletion.NO_TEMPLATE, new SourceChange("", edits: []));
+
+ final PostfixCompletionContext completionContext;
+ final AnalysisContext analysisContext;
+ final CorrectionUtils utils;
+ int fileStamp;
+ AstNode node;
+ PostfixCompletion completion;
+ SourceChange change = new SourceChange('postfix-completion');
+ final Map<String, LinkedEditGroup> linkedPositionGroups =
+ <String, LinkedEditGroup>{};
+ Position exitPosition = null;
+ TypeProvider _typeProvider;
+
+ PostfixCompletionProcessor(this.completionContext)
+ : analysisContext = completionContext.unitElement.context,
+ utils = new CorrectionUtils(completionContext.unit) {
+ fileStamp = _modificationStamp(file);
+ }
+
+ AnalysisDriver get driver => completionContext.driver;
+
+ String get eol => utils.endOfLine;
+
+ String get file => completionContext.file;
+
+ String get key => completionContext.key;
+
+ LineInfo get lineInfo => completionContext.lineInfo;
+
+ int get requestLine => lineInfo.getLocation(selectionOffset).lineNumber;
+
+ int get selectionOffset => completionContext.selectionOffset;
+
+ Source get source => completionContext.unitElement.source;
+
+ TypeProvider get typeProvider {
+ return _typeProvider ??= unitElement.context.typeProvider;
+ }
+
+ CompilationUnit get unit => completionContext.unit;
+
+ CompilationUnitElement get unitElement => completionContext.unitElement;
+
+ Future<PostfixCompletion> compute() async {
+ // If the source was changed between the constructor and running
+ // this asynchronous method, it is not safe to use the unit.
+ if (_modificationStamp(file) != fileStamp) {
+ return NO_COMPLETION;
+ }
+ node = _selectedNode();
+ if (node == null) {
+ return NO_COMPLETION;
+ }
+ PostfixCompletionKind completer = DartPostfixCompletion.forKey(key);
+ return completer?.computer(this, completer) ?? NO_COMPLETION;
+ }
+
+ Future<PostfixCompletion> expand(
+ PostfixCompletionKind kind, Function contexter, Function sourcer,
+ {bool withBraces: true}) async {
+ AstNode expr = contexter();
+ if (expr == null) {
+ return null;
+ }
+
+ DartChangeBuilder changeBuilder = new DartChangeBuilder(driver);
+ await changeBuilder.addFileEdit(file, fileStamp,
+ (DartFileEditBuilder builder) {
+ builder.addReplacement(range.node(expr), (DartEditBuilder builder) {
+ String newSrc = sourcer(expr);
+ if (newSrc == null) {
+ return null;
+ }
+ builder.write(newSrc);
+ if (withBraces) {
+ builder.write(" {");
+ builder.write(eol);
+ String indent = utils.getNodePrefix(expr);
+ builder.write(indent);
+ builder.write(utils.getIndent(1));
+ builder.selectHere();
+ builder.write(eol);
+ builder.write(indent);
+ builder.write("}");
+ } else {
+ builder.selectHere();
+ }
+ });
+ });
+ _setCompletionFromBuilder(changeBuilder, kind);
+ return completion;
+ }
+
+ Future<PostfixCompletion> expandTry(
+ PostfixCompletionKind kind, Function contexter,
+ {bool withOn: false}) async {
+ AstNode stmt = contexter();
+ if (stmt == null) {
+ return null;
+ }
+ DartChangeBuilder changeBuilder = new DartChangeBuilder(driver);
+ await changeBuilder.addFileEdit(file, fileStamp,
+ (DartFileEditBuilder builder) {
+ // Embed the full line(s) of the statement in the try block.
+ var startLine = lineInfo.getLocation(stmt.offset).lineNumber - 1;
+ var endLine = lineInfo.getLocation(stmt.end).lineNumber - 1;
+ if (stmt is ExpressionStatement && !stmt.semicolon.isSynthetic) {
+ endLine += 1;
+ }
+ var startOffset = lineInfo.getOffsetOfLine(startLine);
+ var endOffset = lineInfo.getOffsetOfLine(endLine);
+ var src = utils.getText(startOffset, endOffset - startOffset);
+ String indent = utils.getLinePrefix(stmt.offset);
+ builder.addReplacement(range.startOffsetEndOffset(startOffset, endOffset),
+ (DartEditBuilder builder) {
+ builder.write(indent);
+ builder.write('try {');
+ builder.write(eol);
+ builder.write(src.replaceAll(new RegExp("^$indent", multiLine: true),
+ "$indent${utils.getIndent(1)}"));
+ builder.selectHere();
+ builder.write(indent);
+ builder.write('}');
+ if (withOn) {
+ builder.write(' on ');
+ builder.addSimpleLinkedEdit('NAME', nameOfExceptionThrownBy(stmt));
+ }
+ builder.write(' catch (e, s) {');
+ builder.write(eol);
+ builder.write(indent);
+ builder.write(utils.getIndent(1));
+ builder.write('print(s);');
+ builder.write(eol);
+ builder.write(indent);
+ builder.write("}");
+ builder.write(eol);
+ });
+ });
+ _setCompletionFromBuilder(changeBuilder, kind);
+ return completion;
+ }
+
+ Expression findAssertExpression() {
+ if (node is Expression) {
+ Expression boolExpr = _findOuterExpression(node, typeProvider.boolType);
+ if (boolExpr == null) {
+ return null;
+ }
+ if (boolExpr.parent is ExpressionFunctionBody &&
+ boolExpr.parent.parent is FunctionExpression) {
+ FunctionExpression fnExpr = boolExpr.parent.parent;
+ var type = fnExpr.bestType;
+ if (type is! FunctionType) {
+ return boolExpr;
+ }
+ FunctionType fnType = type;
+ if (fnType.returnType == typeProvider.boolType) {
+ return fnExpr;
+ }
+ }
+ if (boolExpr.bestType == typeProvider.boolType) {
+ return boolExpr;
+ }
+ }
+ return null;
+ }
+
+ Expression findBoolExpression() =>
+ _findOuterExpression(node, typeProvider.boolType);
+
+ Expression findIntExpression() =>
+ _findOuterExpression(node, typeProvider.intType);
+
+ Expression findIterableExpression() =>
+ _findOuterExpression(node, typeProvider.iterableType);
+
+ Expression findObjectExpression() =>
+ _findOuterExpression(node, typeProvider.objectType);
+
+ AstNode findStatement() {
+ var astNode = node;
+ while (astNode != null) {
+ if (astNode is Statement && astNode is! Block) {
+ // Disallow control-flow statements.
+ if (astNode is DoStatement ||
+ astNode is IfStatement ||
+ astNode is ForEachStatement ||
+ astNode is ForStatement ||
+ astNode is SwitchStatement ||
+ astNode is TryStatement ||
+ astNode is WhileStatement) {
+ return null;
+ }
+ return astNode;
+ }
+ astNode = astNode.parent;
+ }
+ return null;
+ }
+
+ Future<bool> isApplicable() async {
+ if (_modificationStamp(file) != fileStamp) {
+ return false;
+ }
+ node = _selectedNode();
+ if (node == null) {
+ return false;
+ }
+ PostfixCompletionKind completer = DartPostfixCompletion.forKey(key);
+ return completer?.selector(this);
+ }
+
+ String makeNegatedBoolExpr(Expression expr) {
+ String originalSrc = utils.getNodeText(expr);
+ String newSrc = utils.invertCondition(expr);
+ if (newSrc != originalSrc) {
+ return newSrc;
+ } else {
+ return "!${utils.getNodeText(expr)}";
+ }
+ }
+
+ String nameOfExceptionThrownBy(AstNode astNode) {
+ if (astNode is ExpressionStatement) {
+ astNode = (astNode as ExpressionStatement).expression;
+ }
+ if (astNode is ThrowExpression) {
+ ThrowExpression expr = astNode;
+ var type = expr.expression.bestType;
+ return type.displayName;
+ }
+ return 'Exception';
+ }
+
+ String newVariable(String base) {
+ String name = base;
+ int i = 1;
+ Set<String> vars =
+ utils.findPossibleLocalVariableConflicts(selectionOffset);
+ while (vars.contains(name)) {
+ name = "$base${i++}";
+ }
+ return name;
+ }
+
+ Expression _findOuterExpression(AstNode start, InterfaceType builtInType) {
+ AstNode parent;
+ if (start is Expression) {
+ parent = start;
+ } else if (start is ArgumentList) {
+ parent = start.parent;
+ }
+ if (parent == null) {
+ return null;
+ }
+ var list = <Expression>[];
+ while (parent is Expression) {
+ list.add(parent);
+ parent = parent.parent;
+ }
+ Expression expr = list.firstWhere((expr) {
+ DartType type = expr.bestType;
+ if (type.isSubtypeOf(builtInType)) return true;
+ Element element = type.element;
+ if (element is TypeDefiningElement) {
+ TypeDefiningElement typeDefElem = element;
+ type = typeDefElem.type;
+ if (type is ParameterizedType) {
+ ParameterizedType pType = type;
+ type = pType.instantiate(new List.filled(
+ pType.typeParameters.length, typeProvider.dynamicType));
+ }
+ }
+ return type.isSubtypeOf(builtInType);
+ }, orElse: () => null);
+ if (expr is SimpleIdentifier && expr.parent is PropertyAccess) {
+ expr = expr.parent;
+ }
+ if (expr?.parent is CascadeExpression) {
+ expr = expr.parent;
+ }
+ return expr;
+ }
+
+ int _modificationStamp(String filePath) {
+ // TODO(brianwilkerson) We have lost the ability for clients to know whether
+ // it is safe to apply an edit.
+ return driver.fsState.getFileForPath(filePath).exists ? 0 : -1;
+ }
+
+ AstNode _selectedNode({int at: null}) =>
+ new NodeLocator(at == null ? selectionOffset : at).searchWithin(unit);
+
+ void _setCompletionFromBuilder(
+ DartChangeBuilder builder, PostfixCompletionKind kind,
+ [List args]) {
+ SourceChange change = builder.sourceChange;
+ if (change.edits.isEmpty) {
+ completion = null;
+ return;
+ }
+ change.message = formatList(kind.message, args);
+ completion = new PostfixCompletion(kind, change);
+ }
+}
« no previous file with comments | « pkg/analysis_server/lib/src/edit/edit_domain.dart ('k') | pkg/analysis_server/test/edit/postfix_completion_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698