Completion. Issue 32677. Suggest overrides when partial '@override' is typed.
Bug: https://github.com/dart-lang/sdk/issues/32677
Change-Id: I44d19f4ba8a4a38b6ec6f770b0c0b45abe5b6e03
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/356640
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
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 000710a..5edd7da 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
@@ -441,15 +441,21 @@
/// Whether `super` should be invoked in the body of the override.
final bool shouldInvokeSuper;
- /// The soruce range that should be replaced by the override.
+ /// If `true`, `@override` is already present, at least partially.
+ /// So, `@` is already present, and the override text does not need it.
+ final bool skipAt;
+
+ /// The source range that should be replaced by the override.
final SourceRange replacementRange;
/// Initialize a newly created candidate suggestion to suggest the [element] by
/// inserting the [shouldInvokeSuper].
- OverrideSuggestion(
- {required this.element,
- required this.shouldInvokeSuper,
- required this.replacementRange});
+ OverrideSuggestion({
+ required this.element,
+ required this.shouldInvokeSuper,
+ required this.skipAt,
+ required this.replacementRange,
+ });
@override
// TODO(brianwilkerson): This needs to be replaced with code to compute the
@@ -693,8 +699,12 @@
case NameSuggestion():
suggestName(suggestion.name);
case OverrideSuggestion():
- suggestOverride2(suggestion.element, suggestion.shouldInvokeSuper,
- suggestion.replacementRange);
+ suggestOverride(
+ element: suggestion.element,
+ invokeSuper: suggestion.shouldInvokeSuper,
+ replacementRange: suggestion.replacementRange,
+ skipAt: suggestion.skipAt,
+ );
case PropertyAccessSuggestion():
var inheritanceDistance = 0.0;
var referencingClass = suggestion.referencingClass;
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 c4ad7b8..0391b27 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
@@ -199,6 +199,25 @@
@override
void visitAnnotation(Annotation node) {
_forAnnotation(node);
+
+ // Look for `@override` with an empty line before the next token.
+ // So, it is not an annotation for a method, but override request.
+ if (node.constructorName == null) {
+ if (node.name case SimpleIdentifier name) {
+ var classNode = node.parent.parent;
+ if (classNode is Declaration) {
+ var lineInfo = state.request.unit.lineInfo;
+ var nameLocation = lineInfo.getLocation(name.offset);
+ var nextToken = name.token.next;
+ if (nextToken != null) {
+ var nextLocation = lineInfo.getLocation(nextToken.offset);
+ if (nextLocation.lineNumber > nameLocation.lineNumber + 1) {
+ _tryOverrideAnnotation(name.token, classNode);
+ }
+ }
+ }
+ }
+ }
}
@override
@@ -489,18 +508,14 @@
keywordHelper.addClassDeclarationKeywords(node);
} else if (offset >= node.leftBracket.end &&
offset <= node.rightBracket.offset) {
- var members = node.members;
- // TODO(brianwilkerson): Generalize this to check for unattatched
- // annotations in other places.
- var preceedingMember = members.elementBefore(offset);
- var token = preceedingMember?.beginToken ?? node.leftBracket.next!;
- if (token.type == TokenType.AT) {
- // The user is completing at the beginning of an annotation.
- // TODO(brianwilkerson): We need to check the next token to see whether
- // part of the annotation is already there.
- _forAnnotation(node);
+ if (_tryAnnotationAtEndOfClassBody(node)) {
return;
- } else if (token.keyword == Keyword.FINAL) {
+ }
+
+ var members = node.members;
+ var precedingMember = members.elementBefore(offset);
+ var token = precedingMember?.beginToken ?? node.leftBracket.next!;
+ if (token.keyword == Keyword.FINAL) {
// The user is completing after the keyword `final`, so they're likely
// trying to declare a field.
_forTypeAnnotation(node);
@@ -515,8 +530,6 @@
keywordHelper.addFunctionBodyModifiers(body);
}
}
- // TODO(brianwilkerson): Consider enabling the generation of overrides in
- // this location.
} else {
// The cursor is immediately to the right of the right bracket, so the
// user is starting a new top-level declaration.
@@ -1535,6 +1548,10 @@
return;
}
if (offset >= node.leftBracket.end && offset <= node.rightBracket.offset) {
+ if (_tryAnnotationAtEndOfClassBody(node)) {
+ return;
+ }
+
collector.completionLocation = 'MixinDeclaration_member';
_forMixinMember(node);
var element = node.members.elementBefore(offset);
@@ -2274,11 +2291,13 @@
keywordHelper.addKeyword(Keyword.SET);
}
if (grandparent.isSingleIdentifier) {
- _suggestOverridesFor(switch (container) {
- ClassDeclaration() => container.declaredElement,
- MixinDeclaration() => container.declaredElement,
- _ => null,
- });
+ _suggestOverridesFor(
+ element: switch (container) {
+ ClassDeclaration() => container.declaredElement,
+ MixinDeclaration() => container.declaredElement,
+ _ => null,
+ },
+ );
}
} else if (grandparent is TopLevelVariableDeclaration) {
if (grandparent.externalKeyword == null) {
@@ -2411,7 +2430,9 @@
void _forClassMember(ClassDeclaration node) {
keywordHelper.addClassMemberKeywords();
declarationHelper(mustBeType: true).addLexicalDeclarations(node);
- _suggestOverridesFor(node.declaredElement);
+ _suggestOverridesFor(
+ element: node.declaredElement,
+ );
}
/// Add the suggestions that are appropriate when the selection is at the
@@ -2637,7 +2658,9 @@
void _forMixinMember(MixinDeclaration node) {
keywordHelper.addMixinMemberKeywords();
declarationHelper(mustBeType: true).addLexicalDeclarations(node);
- _suggestOverridesFor(node.declaredElement);
+ _suggestOverridesFor(
+ element: node.declaredElement,
+ );
}
/// Adds the suggestions that are appropriate when the selection is in the
@@ -2868,11 +2891,49 @@
}
/// If allowed, suggest overrides in the context of the given [element].
- void _suggestOverridesFor(InterfaceElement? element) {
+ void _suggestOverridesFor({
+ required InterfaceElement? element,
+ bool skipAt = false,
+ }) {
// TODO(brianwilkerson): Check whether there's sufficient remaining time
// before computing suggestions for overrides.
if (suggestOverrides && element != null) {
- overrideHelper.computeOverridesFor(element, SourceRange(offset, 0));
+ overrideHelper.computeOverridesFor(
+ interfaceElement: element,
+ replacementRange: SourceRange(offset, 0),
+ skipAt: skipAt,
+ );
+ }
+ }
+
+ /// Check for typing `@override` inside a class body, before `}`.
+ /// There is no node, recover using tokens.
+ bool _tryAnnotationAtEndOfClassBody(Declaration node) {
+ var displaced = state.request.target.entity;
+ if (displaced is Token && displaced.type == TokenType.CLOSE_CURLY_BRACKET) {
+ var identifier = displaced.previous;
+ if (identifier != null && identifier.type == TokenType.IDENTIFIER) {
+ if (identifier.previous?.type == TokenType.AT) {
+ _forAnnotation(node);
+ _tryOverrideAnnotation(identifier, node);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /// If [identifier] at `@` is a start of `@override`, suggest overrides.
+ void _tryOverrideAnnotation(Token identifier, Declaration node) {
+ var lexeme = identifier.lexeme;
+ if (lexeme.isNotEmpty && 'override'.startsWith(lexeme)) {
+ var declaredElement = node.declaredElement;
+ if (declaredElement is InterfaceElement) {
+ _suggestOverridesFor(
+ element: declaredElement,
+ skipAt: true,
+ );
+ }
}
}
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/override_helper.dart b/pkg/analysis_server/lib/src/services/completion/dart/override_helper.dart
index 9b292fc..019a1c2 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/override_helper.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/override_helper.dart
@@ -26,8 +26,11 @@
OverrideHelper({required this.state, required this.collector})
: inheritanceManager = state.request.inheritanceManager;
- void computeOverridesFor(
- InterfaceElement interfaceElement, SourceRange replacementRange) {
+ void computeOverridesFor({
+ required InterfaceElement interfaceElement,
+ required SourceRange replacementRange,
+ required bool skipAt,
+ }) {
var interface = inheritanceManager.getInterface(interfaceElement);
var interfaceMap = interface.map;
var namesToOverride =
@@ -39,10 +42,14 @@
// Gracefully degrade if the overridden element has not been resolved.
if (element != null) {
var invokeSuper = interface.isSuperImplemented(name);
- collector.addSuggestion(OverrideSuggestion(
+ collector.addSuggestion(
+ OverrideSuggestion(
element: element,
shouldInvokeSuper: invokeSuper,
- replacementRange: replacementRange));
+ skipAt: skipAt,
+ replacementRange: replacementRange,
+ ),
+ );
}
}
}
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart b/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart
index 478ffab..576bb43 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart
@@ -18,14 +18,12 @@
import 'package:analysis_server/src/utilities/extensions/element.dart';
import 'package:analysis_server/src/utilities/flutter.dart';
import 'package:analyzer/dart/ast/ast.dart';
-import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
-import 'package:analyzer_plugin/utilities/range_factory.dart';
/// A container with enough information to do filtering, and if necessary
/// build the [CompletionSuggestion] instance.
@@ -941,16 +939,12 @@
/// Add a suggestion to replace the [targetId] with an override of the given
/// [element]. If [invokeSuper] is `true`, then the override will contain an
/// invocation of an overridden member.
- Future<void> suggestOverride(
- Token targetId, ExecutableElement element, bool invokeSuper) async {
- await suggestOverride2(element, invokeSuper, range.token(targetId));
- }
-
- /// Add a suggestion to replace the [targetId] with an override of the given
- /// [element]. If [invokeSuper] is `true`, then the override will contain an
- /// invocation of an overridden member.
- Future<void> suggestOverride2(ExecutableElement element, bool invokeSuper,
- SourceRange replacementRange) async {
+ Future<void> suggestOverride({
+ required ExecutableElement element,
+ required bool invokeSuper,
+ required SourceRange replacementRange,
+ required bool skipAt,
+ }) async {
var displayTextBuffer = StringBuffer();
var overrideImports = <Uri>{};
var builder = ChangeBuilder(session: request.analysisSession);
@@ -983,6 +977,9 @@
completion.startsWith(overrideAnnotation)) {
completion = completion.substring(overrideAnnotation.length).trim();
}
+ if (skipAt && completion.startsWith(overrideAnnotation)) {
+ completion = completion.substring('@'.length);
+ }
if (completion.isEmpty) {
return;
}
@@ -1612,9 +1609,10 @@
static String _textToMatchOverride(ExecutableElement element) {
if (element.isOperator) {
- return 'operator';
+ return 'override_operator';
}
- return element.displayName;
+ // Add "override" to match filter when `@override`.
+ return 'override_${element.displayName}';
}
}
diff --git a/pkg/analysis_server/test/services/completion/dart/location/class_body_test.dart b/pkg/analysis_server/test/services/completion/dart/location/class_body_test.dart
index 877097c..4e41e17 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/class_body_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/class_body_test.dart
@@ -646,8 +646,11 @@
printerConfiguration = printer.Configuration(
filter: (suggestion) {
- if (suggestion.kind == CompletionSuggestionKind.OVERRIDE) {
- return suggestion.completion.contains('foo0');
+ switch (suggestion.kind) {
+ case CompletionSuggestionKind.IDENTIFIER:
+ return suggestion.completion == 'override';
+ case CompletionSuggestionKind.OVERRIDE:
+ return suggestion.completion.contains('foo0');
}
return false;
},
@@ -655,6 +658,244 @@
);
}
+ Future<void> test_class_atOverride_afterField_beforeRightBrace() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ final int bar01 = 0;
+
+ @over^
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void>
+ test_class_atOverride_afterLeftBrace_beforeField_prevLine() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ @over^
+ final int bar01 = 0;
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+''');
+ }
+
+ Future<void>
+ test_class_atOverride_afterLeftBrace_beforeField_skipLine() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ @over^
+
+ final int bar01 = 0;
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void> test_class_atOverride_afterLeftBrace_beforeRightBrace() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ @over^
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void> test_class_atOverride_afterMethod_beforeMethod_prevLine() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ void bar01() {}
+
+ @over^
+ void bar02() {}
+}
+''');
+
+ // Note, no `foo01` override suggestion.
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+''');
+ }
+
+ Future<void> test_class_atOverride_afterMethod_beforeMethod_skipLine() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ void bar01() {}
+
+ @over^
+
+ void bar02() {}
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void> test_class_atOverride_afterMethod_beforeRightBrace() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+class B extends A {
+ void bar() {}
+ @over^
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void> test_class_atOverride_operator() async {
+ await computeSuggestions('''
+class A {
+ @over^
+}
+''');
+
+ printerConfiguration.filter = (suggestion) {
+ switch (suggestion.kind) {
+ case CompletionSuggestionKind.IDENTIFIER:
+ return suggestion.completion == 'override';
+ case CompletionSuggestionKind.OVERRIDE:
+ return suggestion.completion.contains('==');
+ }
+ return false;
+ };
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ bool operator ==(Object other) {
+ // TODO: implement ==
+ return super == other;
+ }
+ kind: override
+ displayText: ==(Object other) { … }
+ selection: 74 22
+''');
+ }
+
Future<void> test_class_inComment() async {
await computeSuggestions('''
class A {
@@ -1139,6 +1380,116 @@
''');
}
+ Future<void> test_mixin_beforeMethod_atOverride_skipLine() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+mixin B on A {
+ @over^
+
+ void bar() {}
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void> test_mixin_beforeRightBrace() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+mixin B on A {
+ ^
+}
+''');
+
+ assertResponse(r'''
+suggestions
+ @override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 60 14
+''');
+ }
+
+ Future<void> test_mixin_beforeRightBrace_atOverride() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+mixin B on A {
+ @over^
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ override
+ kind: topLevelVariable
+ displayText: null
+ override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 59 14
+''');
+ }
+
+ Future<void> test_mixin_beforeRightBrace_partial() async {
+ await computeSuggestions('''
+class A {
+ void foo01() {}
+}
+
+mixin B on A {
+ foo0^
+}
+''');
+
+ assertResponse(r'''
+replacement
+ left: 4
+suggestions
+ @override
+ void foo01() {
+ // TODO: implement foo01
+ super.foo01();
+ }
+ kind: override
+ displayText: foo01() { … }
+ selection: 60 14
+''');
+ }
+
Future<void> test_mixin_method_fromConstraints_alreadyOverridden() async {
await computeSuggestions('''
class A {