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:
committed by
Commit Queue
parent
1da590fc3f
commit
b8a89b66a6
@@ -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});
|
||||
|
||||
+59
-13
@@ -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.
|
||||
|
||||
+27
@@ -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
|
||||
''');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user