[Analyzer] Add folding regions for comment blocks

Change-Id: Iafe70e977065dc68230503349b8ba3abe9b4c0c0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/179777
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/doc/api.html b/pkg/analysis_server/doc/api.html
index 0bce9d2..3bf7d05 100644
--- a/pkg/analysis_server/doc/api.html
+++ b/pkg/analysis_server/doc/api.html
@@ -109,7 +109,7 @@
 <body>
 <h1>Analysis Server API Specification</h1>
 <h1 style="color:#999999">Version
-  1.32.1
+  1.32.2
 </h1>
 <p>
   This document contains a specification of the API provided by the
@@ -236,9 +236,13 @@
   ignoring the item or treating it with some default/fallback handling.
 </p>
 <h3>Changelog</h3>
+<h4>1.32.2</h4>
+<ul>
+  <li>Added <tt>FoldingKind.COMMENT</tt> for folding regions for blocks of comments.</li>
+</ul>
 <h4>1.32.1</h4>
 <ul>
-  <li>Added <tt>CompletionSuggestionKind.PACKAGE_NAME</tt> for Pub package name completions in <tt>pubspec.yaml</tt></li>
+  <li>Added <tt>CompletionSuggestionKind.PACKAGE_NAME</tt> for Pub package name completions in <tt>pubspec.yaml</tt>.</li>
 </ul>
 <h3>Domains</h3>
 <p>
@@ -4226,7 +4230,7 @@
       An enumeration of the kinds of folding regions.
     </p>
     
-  <dl><dt class="value">ANNOTATIONS</dt><dt class="value">BLOCK</dt><dt class="value">CLASS_BODY</dt><dt class="value">DIRECTIVES</dt><dt class="value">DOCUMENTATION_COMMENT</dt><dt class="value">FILE_HEADER</dt><dt class="value">FUNCTION_BODY</dt><dt class="value">INVOCATION</dt><dt class="value">LITERAL</dt></dl></dd><dt class="typeDefinition"><a name="type_FoldingRegion">FoldingRegion: object</a></dt><dd>
+  <dl><dt class="value">ANNOTATIONS</dt><dt class="value">BLOCK</dt><dt class="value">CLASS_BODY</dt><dt class="value">COMMENT</dt><dt class="value">DIRECTIVES</dt><dt class="value">DOCUMENTATION_COMMENT</dt><dt class="value">FILE_HEADER</dt><dt class="value">FUNCTION_BODY</dt><dt class="value">INVOCATION</dt><dt class="value">LITERAL</dt></dl></dd><dt class="typeDefinition"><a name="type_FoldingRegion">FoldingRegion: object</a></dt><dd>
     <p>
       A description of a region that can be folded.
     </p>
diff --git a/pkg/analysis_server/lib/protocol/protocol_constants.dart b/pkg/analysis_server/lib/protocol/protocol_constants.dart
index 4ad5e63..68ea10f 100644
--- a/pkg/analysis_server/lib/protocol/protocol_constants.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_constants.dart
@@ -6,7 +6,7 @@
 // To regenerate the file, use the script
 // "pkg/analysis_server/tool/spec/generate_files".
 
-const String PROTOCOL_VERSION = '1.32.1';
+const String PROTOCOL_VERSION = '1.32.2';
 
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';
diff --git a/pkg/analysis_server/lib/src/computer/computer_folding.dart b/pkg/analysis_server/lib/src/computer/computer_folding.dart
index 725ea9a..2a8bc96 100644
--- a/pkg/analysis_server/lib/src/computer/computer_folding.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_folding.dart
@@ -49,7 +49,6 @@
 
   /// Returns a list of folding regions, not `null`.
   List<FoldingRegion> compute() {
-    _addFileHeaderRegion();
     _unit.accept(_DartUnitFoldingComputerVisitor(this));
 
     if (_firstDirective != null &&
@@ -61,39 +60,90 @@
           _lastDirective.end - _firstDirective.keyword.end));
     }
 
+    _addCommentRegions();
+
     return _foldingRegions;
   }
 
-  void _addFileHeaderRegion() {
-    var firstToken = _unit.beginToken;
-    while (firstToken?.type == TokenType.SCRIPT_TAG) {
-      firstToken = firstToken.next;
+  /// Create a folding region for the provided comment, reading forwards if neccesary.
+  ///
+  /// If [mayBeFileHeader] is true, the token will be considered a file header
+  /// if comment is a single-line-comment and there is a blank line or another
+  /// comment type after it.
+  ///
+  /// Returns the next comment to be processed or null if there are no more comments
+  /// to process in the chain.
+  Token _addCommentRegion(Token commentToken, {bool mayBeFileHeader = false}) {
+    int offset, end;
+    var isFileHeader = false;
+    Token nextComment;
+
+    if (commentToken.type == TokenType.MULTI_LINE_COMMENT) {
+      // Multiline comments already span all of their lines but the folding
+      // region should start at the end of the first line.
+      offset = commentToken.offset + (commentToken.eolOffset ?? 0);
+      end = commentToken.end;
+      nextComment = commentToken.next;
+    } else {
+      // Single line comments need grouping together explicitly but should
+      // only group if the prefix is the same and up to any blank line.
+      final isTripleSlash = commentToken.isTripleSlash;
+      // Track the last comment that belongs to this folding region.
+      var lastComment = commentToken;
+      var current = lastComment.next;
+      while (current != null &&
+          current.type == lastComment.type &&
+          current.isTripleSlash == isTripleSlash &&
+          !_hasBlankLineBetween(lastComment.end, current.offset)) {
+        lastComment = current;
+        current = current.next;
+      }
+
+      // For single line comments we prefer to start the range at the end of
+      // first token so the first line is still visible when the range is
+      // collapsed.
+      offset = commentToken.end;
+      end = lastComment.end;
+      nextComment = lastComment.next;
+
+      // Single line comments are file headers if they're followed by a different
+      // comment type of there's a blank line between them and the first token.
+      isFileHeader = mayBeFileHeader &&
+          (nextComment != null ||
+              _hasBlankLineBetween(end, _unit.beginToken.offset));
     }
 
-    final Token firstComment = firstToken?.precedingComments;
-    if (firstComment == null ||
-        firstComment.type != TokenType.SINGLE_LINE_COMMENT) {
-      return;
+    final kind = isFileHeader
+        ? FoldingKind.FILE_HEADER
+        : (commentToken.lexeme.startsWith('///') ||
+                commentToken.lexeme.startsWith('/**'))
+            ? FoldingKind.DOCUMENTATION_COMMENT
+            : FoldingKind.COMMENT;
+
+    _addRegion(offset, end, kind);
+
+    return nextComment;
+  }
+
+  void _addCommentRegions() {
+    var token = _unit.beginToken;
+    if (token.type == TokenType.SCRIPT_TAG) {
+      token = token.next;
     }
-
-    // Walk through the comments looking for a blank line to signal the end of
-    // the file header.
-    var lastComment = firstComment;
-    while (lastComment.next != null) {
-      lastComment = lastComment.next;
-
-      // If we ran out of tokens, use the original token as starting position.
-      final hasBlankLine =
-          _hasBlankLineBetween(lastComment, lastComment.next ?? firstToken);
-
-      // Also considered non-single-line-comments as the end
-      final nextCommentIsDifferentType = lastComment.next != null &&
-          lastComment.next.type != TokenType.SINGLE_LINE_COMMENT;
-
-      if (hasBlankLine || nextCommentIsDifferentType) {
-        _addRegion(firstComment.end, lastComment.end, FoldingKind.FILE_HEADER);
+    var isFirstToken = true;
+    while (token != null) {
+      Token commentToken = token.precedingComments;
+      while (commentToken != null) {
+        commentToken =
+            _addCommentRegion(commentToken, mayBeFileHeader: isFirstToken);
+      }
+      isFirstToken = false;
+      // Only exit the loop when hitting EOF *after* processing the token as
+      // the EOF token may have preceeding comments.
+      if (token.type == TokenType.EOF) {
         break;
       }
+      token = token.next;
     }
   }
 
@@ -114,9 +164,9 @@
     }
   }
 
-  bool _hasBlankLineBetween(Token first, Token second) {
-    final CharacterLocation firstLoc = _lineInfo.getLocation(first.end);
-    final CharacterLocation secondLoc = _lineInfo.getLocation(second.offset);
+  bool _hasBlankLineBetween(int offset, int end) {
+    final CharacterLocation firstLoc = _lineInfo.getLocation(offset);
+    final CharacterLocation secondLoc = _lineInfo.getLocation(end);
     return secondLoc.lineNumber - firstLoc.lineNumber > 1;
   }
 
@@ -162,15 +212,6 @@
   }
 
   @override
-  void visitComment(Comment node) {
-    if (node.isDocumentation) {
-      _computer._addRegion(
-          node.offset, node.end, FoldingKind.DOCUMENTATION_COMMENT);
-    }
-    super.visitComment(node);
-  }
-
-  @override
   void visitConstructorDeclaration(ConstructorDeclaration node) {
     _computer._addRegionForAnnotations(node.metadata);
     super.visitConstructorDeclaration(node);
@@ -303,3 +344,17 @@
     super.visitWhileStatement(node);
   }
 }
+
+extension _CommentTokenExtensions on Token {
+  static final _newlinePattern = RegExp(r'[\r\n]');
+
+  /// The offset of the first eol character or null
+  /// if no newlines were found.
+  int get eolOffset {
+    final offset = lexeme.indexOf(_newlinePattern);
+    return offset != -1 ? offset : null;
+  }
+
+  /// Whether this comment is a triple-slash single line comment.
+  bool get isTripleSlash => lexeme.startsWith('///');
+}
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index d02da72..fdde79c 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -1055,6 +1055,7 @@
 
 lsp.FoldingRangeKind toFoldingRangeKind(server.FoldingKind kind) {
   switch (kind) {
+    case server.FoldingKind.COMMENT:
     case server.FoldingKind.DOCUMENTATION_COMMENT:
     case server.FoldingKind.FILE_HEADER:
       return lsp.FoldingRangeKind.Comment;
diff --git a/pkg/analysis_server/test/integration/support/protocol_matchers.dart b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
index de9314a..cf6abe57 100644
--- a/pkg/analysis_server/test/integration/support/protocol_matchers.dart
+++ b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
@@ -723,6 +723,7 @@
 ///   ANNOTATIONS
 ///   BLOCK
 ///   CLASS_BODY
+///   COMMENT
 ///   DIRECTIVES
 ///   DOCUMENTATION_COMMENT
 ///   FILE_HEADER
@@ -734,6 +735,7 @@
   'ANNOTATIONS',
   'BLOCK',
   'CLASS_BODY',
+  'COMMENT',
   'DIRECTIVES',
   'DOCUMENTATION_COMMENT',
   'FILE_HEADER',
diff --git a/pkg/analysis_server/test/lsp/folding_test.dart b/pkg/analysis_server/test/lsp/folding_test.dart
index 1caf00a..8b4e389 100644
--- a/pkg/analysis_server/test/lsp/folding_test.dart
+++ b/pkg/analysis_server/test/lsp/folding_test.dart
@@ -44,7 +44,7 @@
 
   Future<void> test_comments() async {
     final content = '''
-    [[/// This is a comment
+    /// This is a comment[[
     /// that spans many lines]]
     class MyClass2 {}
     ''';
@@ -164,13 +164,6 @@
   }
 
   Future<void> test_headersImportsComments() async {
-    // TODO(dantup): Review why the file header and the method comment ranges
-    // are different... one spans only the range to collapse, but the other
-    // just starts at the logical block.
-    // The LSP spec doesn't give any guidance on whether the first part of
-    // the surrounded content should be visible or not after folding
-    // so we'll need to revisit this once there's clarification:
-    // https://github.com/Microsoft/language-server-protocol/issues/659
     final content = '''
     // Copyright some year by some people[[
     // See LICENCE etc.]]
@@ -178,7 +171,7 @@
     import[[ 'dart:io';
     import 'dart:async';]]
 
-    [[/// This is not the file header
+    /// This is not the file header[[
     /// It's just a comment]]
     main() {}
     ''';
diff --git a/pkg/analysis_server/test/src/computer/folding_computer_test.dart b/pkg/analysis_server/test/src/computer/folding_computer_test.dart
index 0f3fa04..03534e7 100644
--- a/pkg/analysis_server/test/src/computer/folding_computer_test.dart
+++ b/pkg/analysis_server/test/src/computer/folding_computer_test.dart
@@ -17,6 +17,12 @@
 
 @reflectiveTest
 class FoldingComputerTest extends AbstractContextTest {
+  static const commentKinds = {
+    FoldingKind.FILE_HEADER,
+    FoldingKind.COMMENT,
+    FoldingKind.DOCUMENTATION_COMMENT
+  };
+
   String sourcePath;
 
   @override
@@ -125,15 +131,72 @@
 
   Future<void> test_comment_is_not_considered_file_header() async {
     var content = """
-// This is not the file header
-// It's just a comment
+// This is not the file header/*1:EXC*/
+// It's just a comment/*1:INC:COMMENT*/
 main() {}
 """;
 
     // Since there are no region comment markers above
     // just check the length instead of the contents
     final regions = await _computeRegions(content);
-    expect(regions, hasLength(0));
+    _compareRegions(regions, content);
+  }
+
+  Future<void> test_comment_multiline() async {
+    var content = '''
+main() {
+/*/*1:EXC*/
+ * comment 1
+ *//*1:EXC:COMMENT*/
+
+/* this comment starts on the same line as delimeters/*2:EXC*/
+ * second line
+ *//*2:EXC:COMMENT*/
+}
+''';
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content, commentKinds);
+  }
+
+  Future<void> test_comment_singleFollowedByBlankLine() async {
+    var content = '''
+main() {
+// this is/*1:EXC*/
+// a comment/*1:INC:COMMENT*/
+/// this is not part of it
+}
+''';
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content, commentKinds);
+  }
+
+  Future<void> test_comment_singleFollowedByMulti() async {
+    var content = '''
+main() {
+  // this is/*1:EXC*/
+  // a comment/*1:INC:COMMENT*/
+  /* this is not part of it */
+  String foo;
+}
+''';
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content, commentKinds);
+  }
+
+  Future<void> test_comment_singleFollowedByTripleSlash() async {
+    var content = '''
+main() {
+// this is/*1:EXC*/
+// a comment/*1:INC:COMMENT*/
+/// this is not part of it
+}
+''';
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content, commentKinds);
   }
 
   Future<void> test_constructor_invocations() async {
@@ -164,7 +227,7 @@
 """;
 
     final regions = await _computeRegions(content);
-    _compareRegions(regions, content);
+    _compareRegions(regions, content, {FoldingKind.FILE_HEADER});
   }
 
   Future<void> test_file_header_does_not_include_block_comments() async {
@@ -179,7 +242,7 @@
 """;
 
     final regions = await _computeRegions(content);
-    expect(regions, hasLength(0));
+    _compareRegions(regions, content, {FoldingKind.FILE_HEADER});
   }
 
   Future<void> test_file_header_with_no_function_comment() async {
@@ -191,7 +254,7 @@
 ''';
 
     final regions = await _computeRegions(content);
-    _compareRegions(regions, content);
+    _compareRegions(regions, content, {FoldingKind.FILE_HEADER});
   }
 
   Future<void> test_file_header_with_non_end_of_line_comment() async {
@@ -204,7 +267,7 @@
 """;
 
     final regions = await _computeRegions(content);
-    _compareRegions(regions, content);
+    _compareRegions(regions, content, {FoldingKind.FILE_HEADER});
   }
 
   Future<void> test_file_header_with_script_prefix() async {
@@ -219,6 +282,19 @@
 """;
 
     final regions = await _computeRegions(content);
+    _compareRegions(regions, content, {FoldingKind.FILE_HEADER});
+  }
+
+  Future<void> test_fileHeader_singleFollowedByBlank() async {
+    var content = '''
+// this is/*1:EXC*/
+// a file header/*1:INC:FILE_HEADER*/
+
+// this is not part of it
+main() {}
+''';
+
+    final regions = await _computeRegions(content);
     _compareRegions(regions, content);
   }
 
@@ -263,7 +339,7 @@
     var content = '''
 // Content before
 
-/*1:EXC*//// This is a doc comment
+/// This is a doc comment/*1:EXC*/
 /// that spans lines/*1:INC:DOCUMENTATION_COMMENT*/
 main() {/*2:INC*/
   print("Hello, world!");
@@ -434,11 +510,19 @@
 
   /// Compares provided folding regions with expected
   /// regions extracted from the comments in the provided content.
-  void _compareRegions(List<FoldingRegion> regions, String content) {
+  ///
+  /// If [onlyKinds] is supplied only regions of that type will be compared.
+  void _compareRegions(List<FoldingRegion> regions, String content,
+      [Set<FoldingKind> onlyKinds]) {
     // Find all numeric markers for region starts.
     final regex = RegExp(r'/\*(\d+):(INC|EXC)\*/');
     final expectedRegions = regex.allMatches(content);
 
+    if (onlyKinds != null) {
+      regions =
+          regions.where((region) => onlyKinds.contains(region.kind)).toList();
+    }
+
     // Check we didn't get more than expected, since the loop below only
     // checks for the presence of matches, not absence.
     expect(regions, hasLength(expectedRegions.length));
diff --git a/pkg/analysis_server/tool/spec/generated/java/types/FoldingKind.java b/pkg/analysis_server/tool/spec/generated/java/types/FoldingKind.java
index be9f538..3b8b236 100644
--- a/pkg/analysis_server/tool/spec/generated/java/types/FoldingKind.java
+++ b/pkg/analysis_server/tool/spec/generated/java/types/FoldingKind.java
@@ -21,6 +21,8 @@
 
   public static final String CLASS_BODY = "CLASS_BODY";
 
+  public static final String COMMENT = "COMMENT";
+
   public static final String DIRECTIVES = "DIRECTIVES";
 
   public static final String DOCUMENTATION_COMMENT = "DOCUMENTATION_COMMENT";
diff --git a/pkg/analysis_server/tool/spec/spec_input.html b/pkg/analysis_server/tool/spec/spec_input.html
index 47b9e8f..7d118ec 100644
--- a/pkg/analysis_server/tool/spec/spec_input.html
+++ b/pkg/analysis_server/tool/spec/spec_input.html
@@ -7,7 +7,7 @@
 <body>
 <h1>Analysis Server API Specification</h1>
 <h1 style="color:#999999">Version
-  <version>1.32.1</version>
+  <version>1.32.2</version>
 </h1>
 <p>
   This document contains a specification of the API provided by the
@@ -134,9 +134,13 @@
   ignoring the item or treating it with some default/fallback handling.
 </p>
 <h3>Changelog</h3>
+<h4>1.32.2</h4>
+<ul>
+  <li>Added <tt>FoldingKind.COMMENT</tt> for folding regions for blocks of comments.</li>
+</ul>
 <h4>1.32.1</h4>
 <ul>
-  <li>Added <tt>CompletionSuggestionKind.PACKAGE_NAME</tt> for Pub package name completions in <tt>pubspec.yaml</tt></li>
+  <li>Added <tt>CompletionSuggestionKind.PACKAGE_NAME</tt> for Pub package name completions in <tt>pubspec.yaml</tt>.</li>
 </ul>
 <h3>Domains</h3>
 <p>
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart
index 346847c..2b61453 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart
@@ -1904,6 +1904,7 @@
 ///   ANNOTATIONS
 ///   BLOCK
 ///   CLASS_BODY
+///   COMMENT
 ///   DIRECTIVES
 ///   DOCUMENTATION_COMMENT
 ///   FILE_HEADER
@@ -1920,6 +1921,8 @@
 
   static const FoldingKind CLASS_BODY = FoldingKind._('CLASS_BODY');
 
+  static const FoldingKind COMMENT = FoldingKind._('COMMENT');
+
   static const FoldingKind DIRECTIVES = FoldingKind._('DIRECTIVES');
 
   static const FoldingKind DOCUMENTATION_COMMENT =
@@ -1938,6 +1941,7 @@
     ANNOTATIONS,
     BLOCK,
     CLASS_BODY,
+    COMMENT,
     DIRECTIVES,
     DOCUMENTATION_COMMENT,
     FILE_HEADER,
@@ -1959,6 +1963,8 @@
         return BLOCK;
       case 'CLASS_BODY':
         return CLASS_BODY;
+      case 'COMMENT':
+        return COMMENT;
       case 'DIRECTIVES':
         return DIRECTIVES;
       case 'DOCUMENTATION_COMMENT':
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
index 4ad5e63..68ea10f 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
@@ -6,7 +6,7 @@
 // To regenerate the file, use the script
 // "pkg/analysis_server/tool/spec/generate_files".
 
-const String PROTOCOL_VERSION = '1.32.1';
+const String PROTOCOL_VERSION = '1.32.2';
 
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';
diff --git a/pkg/analyzer_plugin/doc/api.html b/pkg/analyzer_plugin/doc/api.html
index 6aaac51..dd53e3b 100644
--- a/pkg/analyzer_plugin/doc/api.html
+++ b/pkg/analyzer_plugin/doc/api.html
@@ -1309,7 +1309,7 @@
       An enumeration of the kinds of folding regions.
     </p>
     
-  <dl><dt class="value">ANNOTATIONS</dt><dt class="value">BLOCK</dt><dt class="value">CLASS_BODY</dt><dt class="value">DIRECTIVES</dt><dt class="value">DOCUMENTATION_COMMENT</dt><dt class="value">FILE_HEADER</dt><dt class="value">FUNCTION_BODY</dt><dt class="value">INVOCATION</dt><dt class="value">LITERAL</dt></dl></dd><dt class="typeDefinition"><a name="type_FoldingRegion">FoldingRegion: object</a></dt><dd>
+  <dl><dt class="value">ANNOTATIONS</dt><dt class="value">BLOCK</dt><dt class="value">CLASS_BODY</dt><dt class="value">COMMENT</dt><dt class="value">DIRECTIVES</dt><dt class="value">DOCUMENTATION_COMMENT</dt><dt class="value">FILE_HEADER</dt><dt class="value">FUNCTION_BODY</dt><dt class="value">INVOCATION</dt><dt class="value">LITERAL</dt></dl></dd><dt class="typeDefinition"><a name="type_FoldingRegion">FoldingRegion: object</a></dt><dd>
     <p>
       A description of a region that can be folded.
     </p>
diff --git a/pkg/analyzer_plugin/lib/protocol/protocol_common.dart b/pkg/analyzer_plugin/lib/protocol/protocol_common.dart
index 56bcadd..89b8f94 100644
--- a/pkg/analyzer_plugin/lib/protocol/protocol_common.dart
+++ b/pkg/analyzer_plugin/lib/protocol/protocol_common.dart
@@ -1904,6 +1904,7 @@
 ///   ANNOTATIONS
 ///   BLOCK
 ///   CLASS_BODY
+///   COMMENT
 ///   DIRECTIVES
 ///   DOCUMENTATION_COMMENT
 ///   FILE_HEADER
@@ -1920,6 +1921,8 @@
 
   static const FoldingKind CLASS_BODY = FoldingKind._('CLASS_BODY');
 
+  static const FoldingKind COMMENT = FoldingKind._('COMMENT');
+
   static const FoldingKind DIRECTIVES = FoldingKind._('DIRECTIVES');
 
   static const FoldingKind DOCUMENTATION_COMMENT =
@@ -1938,6 +1941,7 @@
     ANNOTATIONS,
     BLOCK,
     CLASS_BODY,
+    COMMENT,
     DIRECTIVES,
     DOCUMENTATION_COMMENT,
     FILE_HEADER,
@@ -1959,6 +1963,8 @@
         return BLOCK;
       case 'CLASS_BODY':
         return CLASS_BODY;
+      case 'COMMENT':
+        return COMMENT;
       case 'DIRECTIVES':
         return DIRECTIVES;
       case 'DOCUMENTATION_COMMENT':
diff --git a/pkg/analyzer_plugin/test/integration/support/protocol_matchers.dart b/pkg/analyzer_plugin/test/integration/support/protocol_matchers.dart
index d495c4f..3ac543a 100644
--- a/pkg/analyzer_plugin/test/integration/support/protocol_matchers.dart
+++ b/pkg/analyzer_plugin/test/integration/support/protocol_matchers.dart
@@ -305,6 +305,7 @@
 ///   ANNOTATIONS
 ///   BLOCK
 ///   CLASS_BODY
+///   COMMENT
 ///   DIRECTIVES
 ///   DOCUMENTATION_COMMENT
 ///   FILE_HEADER
@@ -316,6 +317,7 @@
   'ANNOTATIONS',
   'BLOCK',
   'CLASS_BODY',
+  'COMMENT',
   'DIRECTIVES',
   'DOCUMENTATION_COMMENT',
   'FILE_HEADER',
diff --git a/pkg/analyzer_plugin/tool/spec/common_types_spec.html b/pkg/analyzer_plugin/tool/spec/common_types_spec.html
index b1c7056..df7fa1c 100644
--- a/pkg/analyzer_plugin/tool/spec/common_types_spec.html
+++ b/pkg/analyzer_plugin/tool/spec/common_types_spec.html
@@ -6,7 +6,7 @@
 </head>
 <body>
 <h1>Common Types</h1>
-<version>1.4.0</version>
+<version>1.4.1</version>
 <p>
   This document contains a specification of the types that are common between
   the analysis server wire protocol and the analysis server plugin wire
@@ -575,6 +575,7 @@
       <value><code>ANNOTATIONS</code></value>
       <value><code>BLOCK</code></value>
       <value><code>CLASS_BODY</code></value>
+      <value><code>COMMENT</code></value>
       <value><code>DIRECTIVES</code></value>
       <value><code>DOCUMENTATION_COMMENT</code></value>
       <value><code>FILE_HEADER</code></value>