Initial support for code completion in extension type declarations

Change-Id: I6acb71d0ac282135b6527431b162fad9a0dee8f7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/324900
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Brian Wilkerson
2023-09-08 14:30:20 +00:00
committed by Commit Queue
parent 1da590fc3f
commit b8a89b66a6
7 changed files with 170 additions and 13 deletions
@@ -94,6 +94,12 @@ final class KeywordSuggestion extends CandidateSuggestion {
);
}
/// Initialize a newly created candidate suggestion to suggest the [keyword].
factory KeywordSuggestion.fromPseudoKeyword(String keyword) {
return KeywordSuggestion._(
completion: keyword, selectionOffset: keyword.length);
}
/// Initialize a newly created candidate suggestion to suggest a keyword.
KeywordSuggestion._(
{required this.completion, required this.selectionOffset});
@@ -246,6 +246,16 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
}
}
@override
void visitCompilationUnit(CompilationUnit node) {
var followingMember = node.memberAfter(offset);
if (_forIncompletePreceedingUnitMember(node, followingMember)) {
// The preceeding member is incomplete, so assume that the user is
// completing it rather than starting a new member.
return;
}
}
@override
void visitConditionalExpression(ConditionalExpression node) {
// TODO(brianwilkerson) Consider adding a location for the condition.
@@ -428,10 +438,11 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
}
var name = node.name;
if (name != null && offset <= name.end) {
// TODO(brianwilkerson) We probably need to suggest `on`.
// TODO(brianwilkerson) We probably need to suggest `type` when extension
// types are supported.
// Don't suggest a name for the extension.
keywordHelper.addKeyword(Keyword.ON);
if (featureSet.isEnabled(Feature.inline_class)) {
keywordHelper.addPseudoKeyword('type');
}
// TODO(brianwilkerson) Suggest a name for the extension.
return;
}
if (offset <= node.leftBracket.offset) {
@@ -453,6 +464,14 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
_forExpression(node);
}
@override
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
if (offset >= node.representation.end &&
(offset <= node.leftBracket.offset || node.leftBracket.isSynthetic)) {
keywordHelper.addKeyword(Keyword.IMPLEMENTS);
}
}
@override
void visitFieldDeclaration(FieldDeclaration node) {
_forIncompletePreceedingClassMember(node);
@@ -1033,7 +1052,9 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
@override
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
if (_forIncompletePreceedingUnitMember(node)) {
var unit = node.parent;
if (unit is CompilationUnit &&
_forIncompletePreceedingUnitMember(unit, node)) {
return;
} else if (node.isSingleIdentifier) {
// The parser recovers from a simple identifier by assuming that it's a
@@ -1123,7 +1144,9 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
// The order of these conditions is critical. We need to check for an
// incomplete preceeding member even when the grandparent isn't a single
// identifier, but want to return only if both conditions are true.
if (_forIncompletePreceedingUnitMember(grandparent) &&
var unit = grandparent.parent;
if (unit is CompilationUnit &&
_forIncompletePreceedingUnitMember(unit, grandparent) &&
grandparent.isSingleIdentifier) {
return;
}
@@ -1356,14 +1379,11 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
/// then the user might be attempting to complete the preceeding member rather
/// than attempting to prepend something to the given [member], so add the
/// suggestions appropriate for that situation.
bool _forIncompletePreceedingUnitMember(AstNode member) {
if (offset <= member.beginToken.end) {
var parent = member.parent;
if (parent is! CompilationUnit) {
return false;
}
bool _forIncompletePreceedingUnitMember(
CompilationUnit parent, AstNode? member) {
if (member == null || offset <= member.beginToken.end) {
var members = parent.sortedDirectivesAndDeclarations;
var index = members.indexOf(member);
var index = member == null ? members.length : members.indexOf(member);
if (index <= 0) {
return false;
}
@@ -1378,6 +1398,11 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
keywordHelper.addClassDeclarationKeywords(declaration);
return true;
}
case ExtensionTypeDeclaration declaration:
if (declaration.hasNoBody) {
visitExtensionTypeDeclaration(declaration);
return true;
}
case ImportDirective directive:
if (directive.semicolon.isSynthetic) {
visitImportDirective(directive);
@@ -1569,6 +1594,20 @@ extension on ClassMember {
}
}
extension on CompilationUnit {
/// Return the member that is immediately after the given [offset] or `null`
/// if the offset isn't before a member.
AstNode? memberAfter(int offset) {
var members = sortedDirectivesAndDeclarations;
for (var member in members) {
if (offset < member.offset) {
return member;
}
}
return null;
}
}
extension on ExpressionStatement {
/// Return `true` if this statement consists of a single identifier.
bool get isSingleIdentifier {
@@ -1580,6 +1619,13 @@ extension on ExpressionStatement {
}
}
extension on ExtensionTypeDeclaration {
/// Return `true` if this class declaration doesn't have a body.
bool get hasNoBody {
return leftBracket.isSynthetic && rightBracket.isSynthetic;
}
}
extension on FieldDeclaration {
/// Return `true` if this field declaration consists of a single identifier.
bool get isSingleIdentifier {
@@ -145,6 +145,10 @@ class _KeywordVisitor extends SimpleAstVisitor<void> {
return;
}
}
if (previousMember is ExtensionTypeDeclaration) {
// Already handled by the in-scope completion pass.
return;
}
if (previousMember == null || previousMember is Directive) {
if (previousMember == null &&
!node.directives.any((d) => d is LibraryDirective)) {
@@ -290,6 +290,9 @@ class KeywordHelper {
void addExtensionDeclarationKeywords(ExtensionDeclaration node) {
if (node.onKeyword.isSynthetic) {
addKeyword(Keyword.ON);
if (node.name == null && featureSet.isEnabled(Feature.inline_class)) {
addPseudoKeyword('type');
}
}
}
@@ -461,6 +464,11 @@ class KeywordHelper {
addVariablePatternKeywords();
}
/// Add a keyword suggestion to suggest the [keyword].
void addPseudoKeyword(String keyword) {
collector.addSuggestion(KeywordSuggestion.fromPseudoKeyword(keyword));
}
/// Add the keywords that are appropriate when the selection is at the
/// beginning of a statement. The [node] provides context to determine which
/// keywords to include.
@@ -36,6 +36,8 @@ extension ^
suggestions
on
kind: keyword
type
kind: keyword
''');
}
@@ -49,4 +51,29 @@ suggestions
kind: keyword
''');
}
Future<void> test_afterName_beforeEof_partial() async {
await computeSuggestions('''
extension o^
''');
if (isProtocolVersion2) {
assertResponse(r'''
replacement
left: 1
suggestions
on
kind: keyword
''');
} else {
assertResponse(r'''
replacement
left: 1
suggestions
on
kind: keyword
type
kind: keyword
''');
}
}
}
@@ -0,0 +1,64 @@
// Copyright (c) 2023, 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 'package:test_reflective_loader/test_reflective_loader.dart';
import '../../../../client/completion_driver_test.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(ExtensionTypeDeclarationTest1);
defineReflectiveTests(ExtensionTypeDeclarationTest2);
});
}
@reflectiveTest
class ExtensionTypeDeclarationTest1 extends AbstractCompletionDriverTest
with ExtensionTypeDeclarationTestCases {
@override
TestingCompletionProtocol get protocol => TestingCompletionProtocol.version1;
}
@reflectiveTest
class ExtensionTypeDeclarationTest2 extends AbstractCompletionDriverTest
with ExtensionTypeDeclarationTestCases {
@override
TestingCompletionProtocol get protocol => TestingCompletionProtocol.version2;
}
mixin ExtensionTypeDeclarationTestCases on AbstractCompletionDriverTest {
Future<void> test_afterRepresentationField_beforeEof() async {
await computeSuggestions('''
extension type E(int i) ^
''');
assertResponse(r'''
suggestions
implements
kind: keyword
''');
}
Future<void> test_afterRepresentationField_beforeEof_partial() async {
await computeSuggestions('''
extension type E(int i) i^
''');
assertResponse(r'''
replacement
left: 1
suggestions
implements
kind: keyword
''');
}
@FailingTest(reason: 'The AstBuilder drops the incomplete extension type')
Future<void> test_afterType_beforeEof() async {
await computeSuggestions('''
extension type ^
''');
assertResponse(r'''
suggestions
''');
}
}
@@ -21,6 +21,7 @@ import 'enum_test.dart' as enum_;
import 'extends_clause_test.dart' as extends_clause;
import 'extension_body_test.dart' as extension_body;
import 'extension_declaration_test.dart' as extension_declaration;
import 'extension_type_declaration_test.dart' as extension_type_declaration;
import 'field_declaration_test.dart' as field_declaration;
import 'field_formal_parameter_test.dart' as field_formal_parameter;
import 'for_element_test.dart' as for_element;
@@ -88,6 +89,7 @@ void main() {
extends_clause.main();
extension_body.main();
extension_declaration.main();
extension_type_declaration.main();
field_declaration.main();
field_formal_parameter.main();
for_element.main();