[analyzer] Suggest named args between partially-typed names and values

Fixes https://github.com/dart-lang/sdk/issues/35414.

Change-Id: I847aa87c56de248e8e704790c163de4f83813bbf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/189081
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/arglist_contributor.dart b/pkg/analysis_server/lib/src/services/completion/dart/arglist_contributor.dart
index 0741ff5..271b7fb 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/arglist_contributor.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/arglist_contributor.dart
@@ -62,9 +62,15 @@
       {int replacementLength}) {
     var name = parameter.name;
 
+    // Check whether anything after the caret is being replaced. If so, we will
+    // suppress inserting colons/commas. We check only replacing _after_ the
+    // caret as some replacements (before) will still want colons, for example:
+    //     foo(mySt^'bar');
+    var replacementEnd = request.replacementRange.offset +
+        (replacementLength ?? request.replacementRange.length);
     var willReplace =
         request.completionPreference == CompletionPreference.replace &&
-            (replacementLength ?? request.replacementRange.length) > 0;
+            replacementEnd > request.offset;
 
     if (name != null && name.isNotEmpty && !namedArgs.contains(name)) {
       builder.suggestNamedArgument(parameter,
@@ -154,8 +160,27 @@
   bool _isAddingLabelToPositional() {
     if (argumentList != null) {
       var entity = request.target.entity;
-      if (entity is! NamedExpression && request.offset <= entity.offset) {
-        return true;
+      if (entity is! NamedExpression) {
+        // Caret is in front of a value
+        //     f(one: 1, ^2);
+        if (request.offset <= entity.offset) {
+          return true;
+        }
+
+        // Caret is in the between two values that are not seperated by a comma.
+        //     f(one: 1, tw^'foo');
+        // must be at least two and the target not last.
+        var args = argumentList.arguments;
+        if (args.length >= 2 && entity != args.last) {
+          var index = args.indexOf(entity);
+          if (index != -1) {
+            var next = args[index + 1];
+            // Check the two tokens are adjacent without any comma.
+            if (entity.end == next.offset) {
+              return true;
+            }
+          }
+        }
       }
     }
     return false;
diff --git a/pkg/analysis_server/test/domain_completion_test.dart b/pkg/analysis_server/test/domain_completion_test.dart
index 95ab896..bfb2c73 100644
--- a/pkg/analysis_server/test/domain_completion_test.dart
+++ b/pkg/analysis_server/test/domain_completion_test.dart
@@ -102,6 +102,24 @@
     expect(replacementLength, equals(0));
   }
 
+  Future<void> test_ArgumentList_function_named_partiallyTyped() async {
+    addTestFile('''
+    class C {
+      void m(String firstString, {String secondString}) {}
+
+      void n() {
+        m('a', se^'b');
+      }
+    }
+    ''');
+    await getSuggestions();
+    assertHasResult(CompletionSuggestionKind.NAMED_ARGUMENT, 'secondString: ');
+    expect(suggestions, hasLength(1));
+    // Ensure we replace the correct section.
+    expect(replacementOffset, equals(completionOffset - 2));
+    expect(replacementLength, equals(2));
+  }
+
   Future<void> test_ArgumentList_imported_function_named_param() async {
     addTestFile('main() { int.parse("16", ^);}');
     await getSuggestions();
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 78e5d60..4ad9170 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -708,19 +708,19 @@
       String expectedInsert,
     }) async {
       final content = '''
-class A { const A({int argOne, int argTwo}); }
+class A { const A({int argOne, int argTwo, String argThree}); }
 final varOne = '';
 $code
 main() { }
 ''';
       final expectedReplaced = '''
-class A { const A({int argOne, int argTwo}); }
+class A { const A({int argOne, int argTwo, String argThree}); }
 final varOne = '';
 $expectedReplace
 main() { }
 ''';
       final expectedInserted = '''
-class A { const A({int argOne, int argTwo}); }
+class A { const A({int argOne, int argTwo, String argThree}); }
 final varOne = '';
 $expectedInsert
 main() { }
@@ -779,6 +779,15 @@
       expectedReplace: '@A(argTwo: ^, argOne: 1)',
       expectedInsert: '@A(argTwo: ^, argOne: 1)',
     );
+
+    // Partially typed names in front of values (that aren't considered part of
+    // the same identifier) should also suggest name labels.
+    await check(
+      '''@A(argOne: 1, argTh^'Foo')''',
+      'argThree: ',
+      expectedReplace: '''@A(argOne: 1, argThree: 'Foo')''',
+      expectedInsert: '''@A(argOne: 1, argThree: 'Foo')''',
+    );
   }
 
   Future<void> test_namedArg_offsetBeforeCompletionTarget() async {