Reduce LSP completion JSON by removing optional fields set to defaults

This addresses some of the things mentioned in https://github.com/dart-lang/sdk/issues/37163 but doesn't entirely solve the issue (for example it doesn't touch docs yet).

Change-Id: Ib0a094695905120ac5e222dae52165b9a5f9a825
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/104860
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index a548530..bafec8c 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -165,13 +165,16 @@
     declaration.relevanceTags
         .forEach((t) => itemRelevance += (tagBoosts[t] ?? 0));
 
+  // Because we potentially send thousands of these items, we should minimise
+  // the generated JSON as much as possible - for example using nulls in place
+  // of empty lists/false where possible.
   return new lsp.CompletionItem(
     label,
     completionKind,
     getDeclarationCompletionDetail(declaration, completionKind, useDeprecated),
     asStringOrMarkupContent(formats, cleanDartdoc(declaration.docComplete)),
-    useDeprecated ? declaration.isDeprecated : null,
-    false, // preselect
+    useDeprecated && declaration.isDeprecated ? true : null,
+    null, // preselect
     // Relevance is a number, highest being best. LSP does text sort so subtract
     // from a large number so that a text sort will result in the correct order.
     // 555 -> 999455
@@ -180,16 +183,15 @@
     (1000000 - itemRelevance).toString(),
     null, // filterText uses label if not set
     null, // insertText is deprecated, but also uses label if not set
-    // We don't have completions that use snippets, so we always return PlainText.
-    lsp.InsertTextFormat.PlainText,
+    null, // insertTextFormat (we always use plain text so can ommit this)
     new lsp.TextEdit(
       // TODO(dantup): If `clientSupportsSnippets == true` then we should map
       // `selection` in to a snippet (see how Dart Code does this).
       toRange(lineInfo, replacementOffset, replacementLength),
       label,
     ),
-    [], // additionalTextEdits, used for adding imports, etc.
-    [], // commitCharacters
+    null, // additionalTextEdits, used for adding imports, etc.
+    null, // commitCharacters
     null, // command
     // data, used for completionItem/resolve.
     new lsp.CompletionItemResolutionInfo(
@@ -375,7 +377,7 @@
   } else if (hasParameterType) {
     return '$prefix${suggestion.parameterType}';
   } else {
-    return prefix;
+    return prefix.isNotEmpty ? prefix : null;
   }
 }
 
@@ -414,7 +416,7 @@
   } else if (hasReturnType) {
     return '$prefix${declaration.returnType}';
   } else {
-    return prefix;
+    return prefix.isNotEmpty ? prefix : null;
   }
 }
 
@@ -555,13 +557,16 @@
       : suggestionKindToCompletionItemKind(
           supportedCompletionItemKinds, suggestion.kind, label);
 
+  // Because we potentially send thousands of these items, we should minimise
+  // the generated JSON as much as possible - for example using nulls in place
+  // of empty lists/false where possible.
   return new lsp.CompletionItem(
     label,
     completionKind,
     getCompletionDetail(suggestion, completionKind, useDeprecated),
     asStringOrMarkupContent(formats, cleanDartdoc(suggestion.docComplete)),
-    useDeprecated ? suggestion.isDeprecated : null,
-    false, // preselect
+    useDeprecated && suggestion.isDeprecated ? true : null,
+    null, // preselect
     // Relevance is a number, highest being best. LSP does text sort so subtract
     // from a large number so that a text sort will result in the correct order.
     // 555 -> 999455
@@ -570,16 +575,15 @@
     (1000000 - suggestion.relevance).toString(),
     null, // filterText uses label if not set
     null, // insertText is deprecated, but also uses label if not set
-    // We don't have completions that use snippets, so we always return PlainText.
-    lsp.InsertTextFormat.PlainText,
+    null, // insertTextFormat (we always use plain text so can ommit this)
     new lsp.TextEdit(
       // TODO(dantup): If `clientSupportsSnippets == true` then we should map
       // `selection` in to a snippet (see how Dart Code does this).
       toRange(lineInfo, replacementOffset, replacementLength),
       suggestion.completion,
     ),
-    [], // additionalTextEdits, used for adding imports, etc.
-    [], // commitCharacters
+    null, // additionalTextEdits, used for adding imports, etc.
+    null, // commitCharacters
     null, // command
     null, // data, useful for if using lazy resolve, this comes back to us
   );
diff --git a/pkg/analysis_server/test/lsp/completion_test.dart b/pkg/analysis_server/test/lsp/completion_test.dart
index 8a378f4..e9a6b94 100644
--- a/pkg/analysis_server/test/lsp/completion_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_test.dart
@@ -217,7 +217,8 @@
     final res = await getCompletion(mainFileUri, positionFromMarker(content));
     expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
     final item = res.singleWhere((c) => c.label == 'abcdefghij');
-    expect(item.insertTextFormat, equals(InsertTextFormat.PlainText));
+    expect(item.insertTextFormat,
+        anyOf(equals(InsertTextFormat.PlainText), isNull));
     // ignore: deprecated_member_use_from_same_package
     expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
     final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
@@ -445,7 +446,8 @@
     final res = await getCompletion(mainFileUri, positionFromMarker(content));
     expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
     final item = res.singleWhere((c) => c.label == 'abcdefghij');
-    expect(item.insertTextFormat, equals(InsertTextFormat.PlainText));
+    expect(item.insertTextFormat,
+        anyOf(equals(InsertTextFormat.PlainText), isNull));
     // ignore: deprecated_member_use_from_same_package
     expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
     final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);