diff --git a/pkg/analysis_server/lib/src/services/completion/dart/candidate_suggestion.dart b/pkg/analysis_server/lib/src/services/completion/dart/candidate_suggestion.dart index 349f19b2498..45bd9cef25b 100644 --- a/pkg/analysis_server/lib/src/services/completion/dart/candidate_suggestion.dart +++ b/pkg/analysis_server/lib/src/services/completion/dart/candidate_suggestion.dart @@ -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}); 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 b6d0530c99a..25870c3416b 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 @@ -246,6 +246,16 @@ class InScopeCompletionPass extends SimpleAstVisitor { } } + @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 { } 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 { _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 { @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 { // 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 { /// 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 { 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 { 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 e9e78782727..5a853adf100 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 @@ -145,6 +145,10 @@ class _KeywordVisitor extends SimpleAstVisitor { 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)) { 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 149eef197bf..3a43f4a6bfb 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 @@ -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. diff --git a/pkg/analysis_server/test/services/completion/dart/location/extension_declaration_test.dart b/pkg/analysis_server/test/services/completion/dart/location/extension_declaration_test.dart index 14d72fab66c..04f3c5cb5f2 100644 --- a/pkg/analysis_server/test/services/completion/dart/location/extension_declaration_test.dart +++ b/pkg/analysis_server/test/services/completion/dart/location/extension_declaration_test.dart @@ -36,6 +36,8 @@ extension ^ suggestions on kind: keyword + type + kind: keyword '''); } @@ -49,4 +51,29 @@ suggestions kind: keyword '''); } + + Future 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 +'''); + } + } } diff --git a/pkg/analysis_server/test/services/completion/dart/location/extension_type_declaration_test.dart b/pkg/analysis_server/test/services/completion/dart/location/extension_type_declaration_test.dart new file mode 100644 index 00000000000..7ddde38bf41 --- /dev/null +++ b/pkg/analysis_server/test/services/completion/dart/location/extension_type_declaration_test.dart @@ -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 test_afterRepresentationField_beforeEof() async { + await computeSuggestions(''' +extension type E(int i) ^ +'''); + assertResponse(r''' +suggestions + implements + kind: keyword +'''); + } + + Future 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 test_afterType_beforeEof() async { + await computeSuggestions(''' +extension type ^ +'''); + assertResponse(r''' +suggestions +'''); + } +} diff --git a/pkg/analysis_server/test/services/completion/dart/location/test_all.dart b/pkg/analysis_server/test/services/completion/dart/location/test_all.dart index 89aa0603bb3..992cd33137d 100644 --- a/pkg/analysis_server/test/services/completion/dart/location/test_all.dart +++ b/pkg/analysis_server/test/services/completion/dart/location/test_all.dart @@ -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();