Convert a few more methods in KeywordContributor
Change-Id: I905a4855f67db227ab654c3d4dc7441b7ee2bf58
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/315540
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart b/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart
index 30474ea..a90fed6 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart
@@ -305,7 +305,7 @@
}
var state = CompletionState(request.libraryElement, selection);
var pass = InScopeCompletionPass(state: state, collector: collector);
- selection.coveringNode.accept(pass);
+ pass.computeSuggestions();
builder.suggestFromCandidates(collector.suggestions);
}
}
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart b/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart
index 5e7dd47..d472f36 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart
@@ -5,8 +5,10 @@
import 'package:analysis_server/src/services/completion/dart/completion_state.dart';
import 'package:analysis_server/src/services/completion/dart/keyword_helper.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_collector.dart';
+import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
/// A completion pass that will create candidate suggestions based on the
@@ -39,12 +41,99 @@
/// Return the offset at which completion was requested.
int get offset => state.selection.offset;
+ /// Return the node that should be used as the context in which completion is
+ /// occurring.
+ ///
+ /// This is normally the covering node, but if the covering node begins with
+ /// an identifier (or keyword) and the [offset] is covered by the identifier,
+ /// then we look for the highest node that also begins with the same token and
+ /// use the parent of that node.
+ ///
+ /// This allows us more context for completing what the user might be trying
+ /// to write and also reduces the complexity of the visitor and reduces the
+ /// amount of code duplication.
+ AstNode get _completionNode {
+ var selection = state.selection;
+ var coveringNode = selection.coveringNode;
+ var beginToken = coveringNode.beginToken;
+ if (!beginToken.isKeywordOrIdentifier ||
+ !selection.isCoveredByToken(beginToken)) {
+ return coveringNode;
+ }
+ var child = coveringNode;
+ var parent = child.parent;
+ while (parent != null && parent.beginToken == beginToken) {
+ child = parent;
+ parent = child.parent;
+ }
+ // The [child] is now the highest node that starts with the [beginToken].
+ if (parent != null) {
+ return parent;
+ }
+ return child;
+ }
+
+ /// Compute the candidate suggestions associated with this pass.
+ void computeSuggestions() {
+ _completionNode.accept(this);
+ }
+
@override
void visitAdjacentStrings(AdjacentStrings node) {
_visitParentIfAtOrBeforeNode(node);
}
@override
+ void visitAssignmentExpression(AssignmentExpression node) {
+ collector.completionLocation = 'AssignmentExpression_rightHandSide';
+ _forExpression(node);
+ }
+
+ @override
+ void visitAwaitExpression(AwaitExpression node) {
+ collector.completionLocation = 'AwaitExpression_expression';
+ _forExpression(node);
+ }
+
+ @override
+ void visitBinaryExpression(BinaryExpression node) {
+ var operator = node.operator.lexeme;
+ collector.completionLocation = 'BinaryExpression_${operator}_rightOperand';
+ _forExpression(node);
+ }
+
+ @override
+ void visitClassDeclaration(ClassDeclaration node) {
+ if (offset < node.classKeyword.offset) {
+ keywordHelper.addClassModifiers(node);
+ return;
+ }
+ if (offset <= node.classKeyword.end) {
+ keywordHelper.addKeyword(Keyword.CLASS);
+ return;
+ }
+ if (offset <= node.name.end) {
+ // TODO(brianwilkerson) Suggest a name for the class.
+ return;
+ }
+ if (offset <= node.leftBracket.offset) {
+ keywordHelper.addClassDeclarationKeywords(node);
+ return;
+ }
+ if (offset >= node.leftBracket.end && offset <= node.rightBracket.offset) {
+ collector.completionLocation = 'ClassDeclaration_member';
+ _forClassMember();
+ var element = node.members.elementBefore(offset);
+ if (element is MethodDeclaration) {
+ var body = element.body;
+ if (body.isEmpty) {
+ keywordHelper.addFunctionBodyModifiers(body);
+ }
+ }
+ }
+ }
+
+ @override
void visitConstantPattern(ConstantPattern node) {
var expression = node.expression;
if (expression is SimpleIdentifier) {
@@ -155,8 +244,9 @@
}
@override
- void visitRethrowExpression(RethrowExpression node) {
- _forExpression(node);
+ void visitReturnStatement(ReturnStatement node) {
+ collector.completionLocation = 'ReturnStatement_expression';
+ _forExpression(node.expression ?? node);
}
@override
@@ -169,21 +259,29 @@
}
@override
- void visitSimpleIdentifier(SimpleIdentifier node) {
- node.parent?.accept(this);
- }
-
- @override
void visitSimpleStringLiteral(SimpleStringLiteral node) {
_visitParentIfAtOrBeforeNode(node);
}
@override
+ void visitSpreadElement(SpreadElement node) {
+ collector.completionLocation = 'SpreadElement_expression';
+ _forExpression(node);
+ }
+
+ @override
void visitStringInterpolation(StringInterpolation node) {
_visitParentIfAtOrBeforeNode(node);
}
/// Add the suggestions that are appropriate when the selection is at the
+ /// beginning of a class member.
+ void _forClassMember() {
+ keywordHelper.addClassMemberKeywords();
+ // TODO(brianwilkerson) Suggest type names.
+ }
+
+ /// Add the suggestions that are appropriate when the selection is at the
/// beginning of an element in a collection [literal], with the given
/// [elements].
void _forCollectionElement(
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/keyword_contributor.dart b/pkg/analysis_server/lib/src/services/completion/dart/keyword_contributor.dart
index e40283b..8add169 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/keyword_contributor.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/keyword_contributor.dart
@@ -128,21 +128,6 @@
}
@override
- void visitAssignmentExpression(AssignmentExpression node) {
- _addExpressionKeywords(node);
- }
-
- @override
- void visitAwaitExpression(AwaitExpression node) {
- _addExpressionKeywords(node);
- }
-
- @override
- void visitBinaryExpression(BinaryExpression node) {
- _addExpressionKeywords(node);
- }
-
- @override
void visitBlock(Block node) {
var prevStmt = OpType.getPreviousStatement(node, entity);
if (prevStmt is TryStatement) {
@@ -201,70 +186,6 @@
}
@override
- void visitClassDeclaration(ClassDeclaration node) {
- final entity = this.entity;
- // Don't suggest class name
- if (entity == node.name) {
- return;
- }
- if (entity == node.classKeyword) {
- var previous = node.findPrevious(node.classKeyword);
- if (previous != null) {
- if (previous.keyword != Keyword.BASE &&
- previous.keyword != Keyword.FINAL &&
- previous.keyword != Keyword.INTERFACE &&
- previous.keyword != Keyword.MIXIN &&
- previous.keyword != Keyword.SEALED) {
- if (previous.keyword != Keyword.ABSTRACT) {
- if (request.featureSet.isEnabled(Feature.sealed_class)) {
- _addSuggestion(Keyword.SEALED);
- }
- } else {
- // abstract ^ class A {}
- if (request.featureSet.isEnabled(Feature.class_modifiers)) {
- _addSuggestions([
- Keyword.BASE,
- Keyword.FINAL,
- Keyword.INTERFACE,
- Keyword.MIXIN
- ]);
- }
- }
- }
- if (request.featureSet.isEnabled(Feature.class_modifiers) &&
- previous.keyword == Keyword.BASE) {
- // base ^ class A {}
- // abstract base ^ class A {}
- _addSuggestion(Keyword.MIXIN);
- }
- }
- } else if (entity == node.mixinKeyword) {
- var previous = node.findPrevious(node.mixinKeyword!);
- if (previous != null) {
- if (previous.keyword != Keyword.BASE) {
- // abstract ^ mixin class A {}
- if (request.featureSet.isEnabled(Feature.class_modifiers)) {
- _addSuggestion(Keyword.BASE);
- }
- }
- }
- } else if (entity == node.rightBracket) {
- _addClassBodyKeywords();
- } else if (entity is ClassMember) {
- _addClassBodyKeywords();
- var index = node.members.indexOf(entity);
- var previous = index > 0 ? node.members[index - 1] : null;
- if (previous is MethodDeclaration && previous.body.isEmpty) {
- _addSuggestion(Keyword.ASYNC);
- _addSuggestion2(ASYNC_STAR);
- _addSuggestion2(SYNC_STAR);
- }
- } else {
- _addClassDeclarationKeywords(node);
- }
- }
-
- @override
void visitCompilationUnit(CompilationUnit node) {
SyntacticEntity? previousMember;
for (var member in node.childEntities) {
@@ -911,13 +832,6 @@
}
@override
- void visitReturnStatement(ReturnStatement node) {
- if (entity == node.expression) {
- _addExpressionKeywords(node);
- }
- }
-
- @override
void visitSimpleFormalParameter(SimpleFormalParameter node) {
var entity = this.entity;
if (node.type == entity && entity is GenericFunctionType) {
@@ -939,16 +853,6 @@
}
@override
- void visitSpreadElement(SpreadElement node) {
- _addExpressionKeywords(node);
- }
-
- @override
- void visitSuperExpression(SuperExpression node) {
- _addExpressionKeywords(node);
- }
-
- @override
void visitSwitchCase(SwitchCase node) {
_addStatementKeywords(node);
}
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/keyword_helper.dart b/pkg/analysis_server/lib/src/services/completion/dart/keyword_helper.dart
index aa7db85..94889a1 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/keyword_helper.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/keyword_helper.dart
@@ -31,8 +31,10 @@
/// declaration between the name of the class and the body. The [node] is the
/// class declaration containing the selection point.
void addClassDeclarationKeywords(ClassDeclaration node) {
- // Very simplistic suggestion because analyzer will warn if the extends,
- // with, and implements keywords are out of order.
+ // We intentionally add all keywords, even when they would be out of order,
+ // in order to help users discover what keywords are available. If the
+ // keywords are in the wrong order a diagnostic (and fix) will help them get
+ // the keywords in the correct location.
if (node.extendsClause == null) {
addKeyword(Keyword.EXTENDS);
}
@@ -67,18 +69,6 @@
/// declaration before the `class` keyword. The [node] is the class
/// declaration containing the selection point.
void addClassModifiers(ClassDeclaration node) {
- // TODO(brianwilkerson) These next 10 lines duplicate
- // `addClassDeclarationKeywords`, and should probably be removed.
- // Very simplistic suggestion because analyzer will warn if
- // the extends / with / implements keywords are out of order
- if (node.extendsClause == null) {
- addKeyword(Keyword.EXTENDS);
- } else if (node.withClause == null) {
- addKeyword(Keyword.WITH);
- }
- if (node.implementsClause == null) {
- addKeyword(Keyword.IMPLEMENTS);
- }
if (featureSet.isEnabled(Feature.class_modifiers) &&
featureSet.isEnabled(Feature.sealed_class)) {
if (node.baseKeyword == null &&
@@ -96,7 +86,7 @@
addKeyword(Keyword.MIXIN);
}
}
- if (node.baseKeyword != null) {
+ if (node.baseKeyword != null && node.mixinKeyword == null) {
// base ^ class A {}
// abstract base ^ class A {}
addKeyword(Keyword.MIXIN);
@@ -178,8 +168,10 @@
/// declaration between the name of the enum and the body. The [node] is the
/// enum declaration containing the selection point.
void addEnumDeclarationKeywords(EnumDeclaration node) {
- // Very simplistic suggestion because analyzer will warn if the with and
- // implements keywords are out of order.
+ // We intentionally add all keywords, even when they would be out of order,
+ // in order to help users discover what keywords are available. If the
+ // keywords are in the wrong order a diagnostic (and fix) will help them get
+ // the keywords in the correct location.
if (node.withClause == null) {
addKeyword(Keyword.WITH);
}
@@ -207,11 +199,19 @@
/// beginning of an expression. The [node] provides context to determine which
/// keywords to include.
void addExpressionKeywords(AstNode? node) {
+ /// Return `true` if `const` should be suggested for the given [node].
+ bool constIsValid(AstNode? node) {
+ if (node is CollectionElement && node is! Expression) {
+ node = node.parent;
+ }
+ return node is Expression && !node.inConstantContext;
+ }
+
addKeyword(Keyword.FALSE);
addKeyword(Keyword.NULL);
addKeyword(Keyword.TRUE);
if (node != null) {
- if (node is Expression && !node.inConstantContext) {
+ if (constIsValid(node)) {
addKeyword(Keyword.CONST);
}
if (node.inClassMemberBody) {
@@ -314,8 +314,10 @@
/// declaration between the name of the mixin and the body. The [node] is the
/// mixin declaration containing the selection point.
void addMixinDeclarationKeywords(MixinDeclaration node) {
- // Very simplistic suggestion because analyzer will warn if the on and
- //implements clauses are out of order.
+ // We intentionally add all keywords, even when they would be out of order,
+ // in order to help users discover what keywords are available. If the
+ // keywords are in the wrong order a diagnostic (and fix) will help them get
+ // the keywords in the correct location.
if (node.onClause == null) {
addKeyword(Keyword.ON);
}
diff --git a/pkg/analysis_server/test/services/completion/dart/location/class_declaration_test.dart b/pkg/analysis_server/test/services/completion/dart/location/class_declaration_test.dart
index 205f142..17fed24 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/class_declaration_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/class_declaration_test.dart
@@ -108,6 +108,8 @@
suggestions
extends
kind: keyword
+ with
+ kind: keyword
''');
}
@@ -124,12 +126,22 @@
await computeSuggestions('''
class A e^ implements foo { }
''');
+ // TODO(brianwilkerson) The keyword `with` should not be suggested when
+ // using protocol 2 (so this these should require a conditional check).
+ // The reason it is being suggested is as follows: The `e` is ignored by
+ // the parser so it doesn't show up in the AST. As a result, the "entity"
+ // is the implements clause, and the code doesn't find the unattached token
+ // for `e`. As a result, there is no prefix, so the fuzzy matcher returns 1
+ // (a perfect match). We need to improve the detection of a prefix in order
+ // to fix this bug.
assertResponse(r'''
replacement
left: 1
suggestions
extends
kind: keyword
+ with
+ kind: keyword
''');
}
@@ -143,6 +155,8 @@
suggestions
extends
kind: keyword
+ with
+ kind: keyword
''');
}
@@ -165,6 +179,8 @@
kind: keyword
implements
kind: keyword
+ with
+ kind: keyword
''');
}
@@ -239,6 +255,8 @@
kind: keyword
implements
kind: keyword
+ with
+ kind: keyword
''');
}
}
diff --git a/pkg/analysis_server/test/services/completion/dart/location/compilation_unit_member_test.dart b/pkg/analysis_server/test/services/completion/dart/location/compilation_unit_member_test.dart
index 9a72553..749a746 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/compilation_unit_member_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/compilation_unit_member_test.dart
@@ -813,6 +813,8 @@
''');
assertResponse(r'''
suggestions
+ base
+ kind: keyword
''');
}
diff --git a/pkg/analysis_server/test/services/completion/dart/location/object_pattern_test.dart b/pkg/analysis_server/test/services/completion/dart/location/object_pattern_test.dart
index ccce124..71332b7 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/object_pattern_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/object_pattern_test.dart
@@ -118,10 +118,14 @@
kind: field
f11
kind: field
+ final
+ kind: keyword
g01
kind: getter
g11
kind: getter
+ var
+ kind: keyword
''');
}
}
@@ -216,10 +220,14 @@
kind: field
f11
kind: field
+ final
+ kind: keyword
g01
kind: getter
g11
kind: getter
+ var
+ kind: keyword
''');
}
}