Support completion in for loops with patterns

Change-Id: I9d5c57e4e9c42abfbdc2a7dd85fc998826fa8342
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/282390
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
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 5af44c5..0b49665 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
@@ -394,6 +394,8 @@
       } else {
         _addSuggestion(Keyword.IN);
       }
+    } else if (!node.inKeyword.isSynthetic && node.iterable.isSynthetic) {
+      _addSuggestion(Keyword.AWAIT);
     }
   }
 
@@ -504,7 +506,25 @@
     // Actual: for (va^)
     // Parsed: for (va^; ;)
     if (node.forLoopParts == entity) {
-      _addSuggestion(Keyword.VAR);
+      _addSuggestions([Keyword.FINAL, Keyword.VAR]);
+    } else if (node.rightParenthesis == entity) {
+      var parts = node.forLoopParts;
+      if (parts is ForPartsWithDeclarations) {
+        var variables = parts.variables;
+        var keyword = variables.keyword;
+        if (variables.variables.length == 1 &&
+            variables.variables[0].name.isSynthetic &&
+            keyword != null &&
+            parts.leftSeparator.isSynthetic) {
+          var afterKeyword = keyword.next!;
+          if (afterKeyword.type == TokenType.OPEN_PAREN) {
+            var endGroup = afterKeyword.endGroup;
+            if (endGroup != null && request.offset >= endGroup.end) {
+              _addSuggestion(Keyword.IN);
+            }
+          }
+        }
+      }
     }
   }
 
@@ -935,12 +955,16 @@
   void visitVariableDeclarationList(VariableDeclarationList node) {
     var keyword = node.keyword;
     var variables = node.variables;
-    if (variables.isNotEmpty &&
-        entity == variables[0] &&
-        node.type == null &&
-        (keyword == null || keyword.lexeme != 'var')) {
-      _addSuggestion(Keyword.DYNAMIC);
-      _addSuggestion(Keyword.VOID);
+    if (variables.isNotEmpty && entity == variables[0]) {
+      var type = node.type;
+      if (type == null && keyword?.keyword != Keyword.VAR) {
+        _addSuggestion(Keyword.DYNAMIC);
+        _addSuggestion(Keyword.VOID);
+      } else if (type is RecordTypeAnnotation) {
+        // This might be a record pattern that happens to look like a type, in
+        // which case the user might be typing `in`.
+        _addSuggestion(Keyword.IN);
+      }
     }
   }
 
diff --git a/pkg/analysis_server/test/services/completion/dart/keyword_contributor_test.dart b/pkg/analysis_server/test/services/completion/dart/keyword_contributor_test.dart
index 36107e8..76cab69 100644
--- a/pkg/analysis_server/test/services/completion/dart/keyword_contributor_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/keyword_contributor_test.dart
@@ -1177,12 +1177,6 @@
     assertSuggestKeywords(EXPRESSION_START_NO_INSTANCE);
   }
 
-  Future<void> test_for_initialization_var() async {
-    addTestSource('void f() {for (^)}');
-    await computeSuggestions();
-    assertSuggestKeywords([Keyword.VAR]);
-  }
-
   Future<void> test_formalParameterList_beforeFunctionType() async {
     addTestSource('void f(^void Function() g) {}');
     await computeSuggestions();
diff --git a/pkg/analysis_server/test/services/completion/dart/location/for_statement_test.dart b/pkg/analysis_server/test/services/completion/dart/location/for_statement_test.dart
new file mode 100644
index 0000000..dbc1091
--- /dev/null
+++ b/pkg/analysis_server/test/services/completion/dart/location/for_statement_test.dart
@@ -0,0 +1,110 @@
+// 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(ForStatementTest1);
+    defineReflectiveTests(ForStatementTest2);
+  });
+}
+
+/// Tests specific to a for-each statement.
+mixin ForEachPartsTestCases on AbstractCompletionDriverTest {
+  Future<void> test_afterIn() async {
+    await computeSuggestions('''
+void f(List<(int, int)> x01) {
+  for (final (a, b) in ^)
+}
+''');
+    assertResponse('''
+suggestions
+  x01
+    kind: parameter
+  await
+    kind: keyword
+''');
+  }
+
+  Future<void> test_afterPattern() async {
+    await computeSuggestions('''
+void f(Object x) {
+  for (final (a, b) ^)
+}
+''');
+    assertResponse('''
+suggestions
+  in
+    kind: keyword
+''');
+  }
+
+  Future<void> test_afterPattern_partial() async {
+    await computeSuggestions('''
+void f(List<(int, int)> rl) {
+  for (final (a, b) i^)
+}
+''');
+    assertResponse('''
+replacement
+  left: 1
+suggestions
+  in
+    kind: keyword
+''');
+  }
+}
+
+/// Tests specific to a traditional for statement.
+mixin ForPartsTestCases on AbstractCompletionDriverTest {}
+
+@reflectiveTest
+class ForStatementTest1 extends AbstractCompletionDriverTest
+    with ForEachPartsTestCases, ForPartsTestCases, ForStatementTestCases {
+  @override
+  TestingCompletionProtocol get protocol => TestingCompletionProtocol.version1;
+}
+
+@reflectiveTest
+class ForStatementTest2 extends AbstractCompletionDriverTest
+    with ForEachPartsTestCases, ForPartsTestCases, ForStatementTestCases {
+  @override
+  TestingCompletionProtocol get protocol => TestingCompletionProtocol.version2;
+}
+
+/// Tests that apply to both traditional for statements and for-each statements.
+mixin ForStatementTestCases on AbstractCompletionDriverTest {
+  Future<void> test_forParts_empty() async {
+    await computeSuggestions('''
+void f(Object x) {
+  for (^)
+}
+''');
+    assertResponse('''
+suggestions
+  var
+    kind: keyword
+  final
+    kind: keyword
+''');
+  }
+
+  Future<void> test_statements_empty() async {
+    await computeSuggestions('''
+void f(Object x) {
+  for (^)
+}
+''');
+    assertResponse('''
+suggestions
+  var
+    kind: keyword
+  final
+    kind: keyword
+''');
+  }
+}
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 dc4ec61..2ad6556 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
@@ -13,6 +13,7 @@
 import 'enum_constant_test.dart' as enum_constant;
 import 'enum_test.dart' as enum_;
 import 'field_formal_parameter_test.dart' as field_formal_parameter;
+import 'for_statement_test.dart' as for_statement;
 import 'if_element_test.dart' as if_element;
 import 'if_statement_test.dart' as if_statement;
 import 'list_pattern_test.dart' as list_pattern;
@@ -41,6 +42,7 @@
     enum_constant.main();
     enum_.main();
     field_formal_parameter.main();
+    for_statement.main();
     if_element.main();
     if_statement.main();
     list_pattern.main();
diff --git a/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart b/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart
index 1f40b42..675bd62 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart
@@ -723,6 +723,17 @@
   }
 
   @override
+  void visitForEachPartsWithPattern(ForEachPartsWithPattern node) {
+    if (identical(entity, node.iterable)) {
+      optype.completionLocation = 'visitForEachPartsWithPattern_iterable';
+      optype.includeReturnValueSuggestions = true;
+      optype.includeTypeNameSuggestions = true;
+    } else {
+      visitForEachParts(node);
+    }
+  }
+
+  @override
   void visitForElement(ForElement node) {
     // for (^) {}
     // for (Str^ str = null;) {}