Implement LSP code folding

Change-Id: I2b076b1792c229cbbce610d7a195fa03eb29e6bd
Reviewed-on: https://dart-review.googlesource.com/c/89503
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/analysis_server_abstract.dart b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
index f784b71..7dd292f 100644
--- a/pkg/analysis_server/lib/src/analysis_server_abstract.dart
+++ b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
@@ -239,4 +239,13 @@
         .getResult(path, sendCachedToStream: sendCachedToStream)
         .catchError((_) => null);
   }
+
+  /// Return the unresolved unit for the file with the given [path].
+  ParsedUnitResult getParsedUnit(String path) {
+    if (!AnalysisEngine.isDartFileName(path)) {
+      return null;
+    }
+
+    return getAnalysisDriver(path)?.currentSession?.getParsedUnit(path);
+  }
 }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart
index 4161f20..674201f 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart
@@ -33,7 +33,7 @@
     final path = arguments.single;
     final docIdentifier = server.getVersionedDocumentIdentifier(path);
 
-    final result = await requireUnit(path);
+    final result = await requireResolvedUnit(path);
     return result.mapResult((result) {
       final code = result.content;
       final unit = result.unit;
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
index 41d3014..3788e2c 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
@@ -43,7 +43,7 @@
         capabilities?.codeActionLiteralSupport?.codeActionKind?.valueSet ?? []);
 
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
 
     return unit.mapResult((unit) {
       final startOffset = toOffset(unit.lineInfo, params.range.start);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
index bc81078..1f845ef 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -60,7 +60,7 @@
 
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
     return offset.mapResult((offset) => _getItems(
           completionCapabilities,
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_definition.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_definition.dart
index c3e9246..90912c6 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_definition.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_definition.dart
@@ -26,7 +26,7 @@
       TextDocumentPositionParams params) async {
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
 
     return offset.mapResult((offset) {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
index 9f6822e..cc21c40 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
@@ -25,7 +25,7 @@
       TextDocumentPositionParams params) async {
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
 
     return offset.mapResult((requestedOffset) {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_symbols.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_symbols.dart
index e1fda79..7810b7f 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_symbols.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_symbols.dart
@@ -61,7 +61,7 @@
         symbolCapabilities?.hierarchicalDocumentSymbolSupport ?? false;
 
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     return unit.mapResult((unit) => _getSymbols(clientSupportedSymbolKinds,
         clientSupportsDocumentSymbol, path.result, unit));
   }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart
new file mode 100644
index 0000000..96e1a4a
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2019, 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 'dart:async';
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/computer/computer_folding.dart';
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
+import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
+import 'package:analysis_server/src/lsp/mapping.dart';
+
+class FoldingHandler
+    extends MessageHandler<FoldingRangeParams, List<FoldingRange>> {
+  FoldingHandler(LspAnalysisServer server) : super(server);
+  Method get handlesMessage => Method.textDocument_foldingRange;
+
+  @override
+  FoldingRangeParams convertParams(Map<String, dynamic> json) =>
+      FoldingRangeParams.fromJson(json);
+
+  Future<ErrorOr<List<FoldingRange>>> handle(FoldingRangeParams params) async {
+    final path = pathOfDoc(params.textDocument);
+    final unit = await path.mapResult(requireUnresolvedUnit);
+
+    return unit.mapResult((unit) {
+      final lineInfo = unit.lineInfo;
+      final regions =
+          new DartUnitFoldingComputer(lineInfo, unit.unit).compute();
+
+      return success(
+        regions.map((region) => toFoldingRange(lineInfo, region)).toList(),
+      );
+    });
+  }
+}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
index 6fb1c2f..a2a1218 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
@@ -31,7 +31,7 @@
   Future<ErrorOr<List<TextEdit>>> handle(
       DocumentOnTypeFormattingParams params) async {
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     return unit.mapResult((unit) => formatFile(path.result, unit));
   }
 }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
index e226bc4..0d4cb66 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
@@ -31,7 +31,7 @@
   Future<ErrorOr<List<TextEdit>>> handle(
       DocumentFormattingParams params) async {
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     return unit.mapResult((unit) => formatFile(path.result, unit));
   }
 }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_hover.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_hover.dart
index a5396e5..d9302ae 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_hover.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_hover.dart
@@ -26,7 +26,7 @@
   Future<ErrorOr<Hover>> handle(TextDocumentPositionParams params) async {
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
     return offset.mapResult((offset) => _getHover(unit.result, offset));
   }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
index e1a72f6..5b47079 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
@@ -94,7 +94,7 @@
             : Either2<bool, RenameOptions>.t1(true),
         null,
         null,
-        null,
+        Either3<bool, FoldingRangeProviderOptions, dynamic>.t1(true),
         new ExecuteCommandOptions(Commands.serverSupportedCommands),
         new ServerCapabilitiesWorkspace(
             new ServerCapabilitiesWorkspaceFolders(true, true)),
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_references.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_references.dart
index f6f803f..5f3fe45 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_references.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_references.dart
@@ -31,7 +31,7 @@
   Future<ErrorOr<List<Location>>> handle(ReferenceParams params) async {
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
     return offset.mapResult(
         (offset) => _getRefererences(path.result, offset, params, unit.result));
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_rename.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_rename.dart
index 3a9075e..5433077 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_rename.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_rename.dart
@@ -24,7 +24,7 @@
       TextDocumentPositionParams params) async {
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
 
     return offset.mapResult((offset) async {
@@ -84,7 +84,7 @@
         params.textDocument is VersionedTextDocumentIdentifier
             ? params.textDocument
             : server.getVersionedDocumentIdentifier(path)));
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
 
     return offset.mapResult((offset) async {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_signature_help.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_signature_help.dart
index 2fe40ef..3f3e195 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_signature_help.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_signature_help.dart
@@ -24,7 +24,7 @@
       TextDocumentPositionParams params) async {
     final pos = params.position;
     final path = pathOfDoc(params.textDocument);
-    final unit = await path.mapResult(requireUnit);
+    final unit = await path.mapResult(requireResolvedUnit);
     final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
 
     return offset.mapResult((offset) {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
index 9375b8b..ead989c 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
@@ -13,6 +13,7 @@
 import 'package:analysis_server/src/lsp/handlers/handler_document_highlights.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_document_symbols.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_execute_command.dart';
+import 'package:analysis_server/src/lsp/handlers/handler_folding.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_format_on_type.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_formatting.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_hover.dart';
@@ -64,6 +65,7 @@
     registerHandler(new WorkspaceFoldersHandler(server));
     registerHandler(new PrepareRenameHandler(server));
     registerHandler(new RenameHandler(server));
+    registerHandler(new FoldingHandler(server));
   }
 }
 
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
index 9fd98d8..5125008 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
@@ -38,7 +38,7 @@
   ErrorOr<R> failure<R>(ErrorOr<dynamic> error) =>
       new ErrorOr<R>.error(error.error);
 
-  Future<ErrorOr<ResolvedUnitResult>> requireUnit(String path) async {
+  Future<ErrorOr<ResolvedUnitResult>> requireResolvedUnit(String path) async {
     final result = await server.getResolvedUnit(path);
     if (result?.state != ResultState.VALID) {
       return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
@@ -46,6 +46,14 @@
     return success(result);
   }
 
+  ErrorOr<ParsedUnitResult> requireUnresolvedUnit(String path) {
+    final result = server.getParsedUnit(path);
+    if (result?.state != ResultState.VALID) {
+      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+    }
+    return success(result);
+  }
+
   ErrorOr<R> success<R>([R t]) => new ErrorOr<R>.success(t);
 }
 
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index d69d15d..3f1e394 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -417,6 +417,28 @@
   }
 }
 
+lsp.FoldingRange toFoldingRange(
+    server.LineInfo lineInfo, server.FoldingRegion region) {
+  final range = toRange(lineInfo, region.offset, region.length);
+  return new lsp.FoldingRange(range.start.line, range.start.character,
+      range.end.line, range.end.character, toFoldingRangeKind(region.kind));
+}
+
+lsp.FoldingRangeKind toFoldingRangeKind(server.FoldingKind kind) {
+  switch (kind) {
+    case server.FoldingKind.DOCUMENTATION_COMMENT:
+    case server.FoldingKind.FILE_HEADER:
+      return lsp.FoldingRangeKind.Comment;
+    case server.FoldingKind.DIRECTIVES:
+      return lsp.FoldingRangeKind.Imports;
+    default:
+      // null (actually undefined in LSP, the toJson() takes care of that) is
+      // valid, and actually the value used for the majority of folds
+      // (class/functions/etc.).
+      return null;
+  }
+}
+
 List<lsp.DocumentHighlight> toHighlights(
     server.LineInfo lineInfo, server.Occurrences occurrences) {
   return occurrences.offsets
diff --git a/pkg/analysis_server/test/lsp/folding_test.dart b/pkg/analysis_server/test/lsp/folding_test.dart
new file mode 100644
index 0000000..0b41612
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/folding_test.dart
@@ -0,0 +1,113 @@
+// Copyright (c) 2019, 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:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'server_abstract.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FoldingTest);
+  });
+}
+
+@reflectiveTest
+class FoldingTest extends AbstractLspAnalysisServerTest {
+  test_class() async {
+    final content = '''
+    class MyClass2 {[[
+      // Class content
+    ]]}
+    ''';
+
+    final range1 = rangeFromMarkers(content);
+    final expectedRegions = [
+      new FoldingRange(
+        range1.start.line,
+        range1.start.character,
+        range1.end.line,
+        range1.end.character,
+        null,
+      )
+    ];
+
+    await initialize();
+    await openFile(mainFileUri, withoutMarkers(content));
+
+    final regions = await getFoldingRegions(mainFileUri);
+    expect(regions, unorderedEquals(expectedRegions));
+  }
+
+  test_comments() async {
+    final content = '''
+    [[/// This is a comment
+    /// that spans many lines]]
+    class MyClass2 {}
+    ''';
+
+    final range1 = rangeFromMarkers(content);
+    final expectedRegions = [
+      new FoldingRange(
+        range1.start.line,
+        range1.start.character,
+        range1.end.line,
+        range1.end.character,
+        FoldingRangeKind.Comment,
+      )
+    ];
+
+    await initialize();
+    await openFile(mainFileUri, withoutMarkers(content));
+
+    final regions = await getFoldingRegions(mainFileUri);
+    expect(regions, unorderedEquals(expectedRegions));
+  }
+
+  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.]]
+
+    import[[ 'dart:io';
+    import 'dart:async';]]
+
+    [[/// This is not the file header
+    /// It's just a comment]]
+    main() {}
+    ''';
+
+    final ranges = rangesFromMarkers(content);
+
+    final expectedRegions = [
+      _toFoldingRange(ranges[0], FoldingRangeKind.Comment),
+      _toFoldingRange(ranges[1], FoldingRangeKind.Imports),
+      _toFoldingRange(ranges[2], FoldingRangeKind.Comment),
+    ];
+
+    await initialize();
+    await openFile(mainFileUri, withoutMarkers(content));
+
+    final regions = await getFoldingRegions(mainFileUri);
+    expect(regions, unorderedEquals(expectedRegions));
+  }
+
+  FoldingRange _toFoldingRange(Range range, FoldingRangeKind kind) {
+    return new FoldingRange(
+      range.start.line,
+      range.start.character,
+      range.end.line,
+      range.end.character,
+      kind,
+    );
+  }
+}
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index ecae12a..d4a1e2f 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -72,45 +72,6 @@
     );
   }
 
-  /// Validates the document versions for a set of edits match the versions in
-  /// the supplied map.
-  void expectDocumentVersions(
-    Either2<List<TextDocumentEdit>,
-            List<Either4<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>>
-        documentChanges,
-    Map<String, int> expectedVersions,
-  ) {
-    documentChanges.map(
-      // Validate versions on simple doc edits
-      (edits) => edits
-          .forEach((edit) => expectDocumentVersion(edit, expectedVersions)),
-      // For resource changes, we only need to validate changes since
-      // creates/renames/deletes do not supply versions.
-      (changes) => changes.forEach((change) {
-            change.map(
-              (edit) => expectDocumentVersion(edit, expectedVersions),
-              (create) => {},
-              (rename) {},
-              (delete) {},
-            );
-          }),
-    );
-  }
-
-  void expectDocumentVersion(
-    TextDocumentEdit edit,
-    Map<String, int> expectedVersions,
-  ) {
-    final path = Uri.parse(edit.textDocument.uri).toFilePath();
-    final expectedVersion = expectedVersions[path];
-
-    if (edit.textDocument is VersionedTextDocumentIdentifier) {
-      expect(edit.textDocument.version, equals(expectedVersion));
-    } else {
-      throw 'Document identifier for $path was not versioned (expected version $expectedVersion)';
-    }
-  }
-
   void applyResourceChanges(
     Map<String, String> oldFileContent,
     List<Either4<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>> changes,
@@ -234,6 +195,45 @@
     return expectSuccessfulResponseTo(request);
   }
 
+  void expectDocumentVersion(
+    TextDocumentEdit edit,
+    Map<String, int> expectedVersions,
+  ) {
+    final path = Uri.parse(edit.textDocument.uri).toFilePath();
+    final expectedVersion = expectedVersions[path];
+
+    if (edit.textDocument is VersionedTextDocumentIdentifier) {
+      expect(edit.textDocument.version, equals(expectedVersion));
+    } else {
+      throw 'Document identifier for $path was not versioned (expected version $expectedVersion)';
+    }
+  }
+
+  /// Validates the document versions for a set of edits match the versions in
+  /// the supplied map.
+  void expectDocumentVersions(
+    Either2<List<TextDocumentEdit>,
+            List<Either4<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>>
+        documentChanges,
+    Map<String, int> expectedVersions,
+  ) {
+    documentChanges.map(
+      // Validate versions on simple doc edits
+      (edits) => edits
+          .forEach((edit) => expectDocumentVersion(edit, expectedVersions)),
+      // For resource changes, we only need to validate changes since
+      // creates/renames/deletes do not supply versions.
+      (changes) => changes.forEach((change) {
+            change.map(
+              (edit) => expectDocumentVersion(edit, expectedVersions),
+              (create) => {},
+              (rename) {},
+              (delete) {},
+            );
+          }),
+    );
+  }
+
   Future<T> expectErrorNotification<T>(
     FutureOr<void> f(), {
     Duration timeout = const Duration(seconds: 5),
@@ -363,6 +363,14 @@
     return expectSuccessfulResponseTo(request);
   }
 
+  Future<List<FoldingRange>> getFoldingRegions(Uri uri) {
+    final request = makeRequest(
+      Method.textDocument_foldingRange,
+      new FoldingRangeParams(new TextDocumentIdentifier(uri.toString())),
+    );
+    return expectSuccessfulResponseTo<List<FoldingRange>>(request);
+  }
+
   Future<Hover> getHover(Uri uri, Position pos) {
     final request = makeRequest(
       Method.textDocument_hover,
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index d1e29ad..2745a6b 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -23,6 +23,7 @@
 import 'rename_test.dart' as rename_test;
 import 'server_test.dart' as server_test;
 import 'signature_help_test.dart' as signature_help_test;
+import 'folding_test.dart' as folding_test;
 
 main() {
   defineReflectiveSuite(() {
@@ -44,5 +45,6 @@
     assists_code_action_tests.main();
     packet_transformer_tests.main();
     rename_test.main();
+    folding_test.main();
   }, name: 'lsp');
 }
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index 7eb9e39..b696b00 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -77,6 +77,6 @@
 | textDocument/onTypeFormatting | ✅ | ✅ | ✅ | ✅ |
 | textDocument/rename | ✅ | ✅ | ✅ | ✅ |
 | textDocument/prepareRename | | | | |
-| textDocument/foldingRange | | | | |
+| textDocument/foldingRange | ✅ | ✅ | ✅ | ✅ |