Move folding regions to actual blocks

This puts the region inside the braces; eg. the bit the user would expect to disappear when the region is collapsed.

Bug: https://github.com/dart-lang/sdk/issues/33033
Change-Id: I58b347d2e1582bb4a6a35cf45c17085ea1739816
Reviewed-on: https://dart-review.googlesource.com/54402
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Danny Tuppeny <dantup@google.com>
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index b838056..110ad96 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -1275,15 +1275,13 @@
                 analysisServer, path, result.lineInfo, unit);
           });
         }
-        // TODO:(dantup) Uncomment this and equivilent in
-        // test/analysis/notification_folding_test.dart once the
-        // implementation is complete.
-        // if (analysisServer._hasAnalysisServiceSubscription(
-        //     AnalysisService.FOLDING, path)) {
-        //   _runDelayed(() {
-        //     sendAnalysisNotificationFolding(analysisServer, path, unit);
-        //   });
-        // }
+        if (analysisServer._hasAnalysisServiceSubscription(
+            AnalysisService.FOLDING, path)) {
+          _runDelayed(() {
+            sendAnalysisNotificationFolding(
+                analysisServer, path, result.lineInfo, unit);
+          });
+        }
         if (analysisServer._hasAnalysisServiceSubscription(
             AnalysisService.OUTLINE, path)) {
           _runDelayed(() {
diff --git a/pkg/analysis_server/lib/src/computer/computer_folding.dart b/pkg/analysis_server/lib/src/computer/computer_folding.dart
index 19b0c0d..cf76107 100644
--- a/pkg/analysis_server/lib/src/computer/computer_folding.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_folding.dart
@@ -3,19 +3,22 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/syntactic_entity.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart';
 
 /**
  * A computer for [CompilationUnit] folding.
  */
 class DartUnitFoldingComputer {
+  final LineInfo _lineInfo;
   final CompilationUnit _unit;
 
   Directive _firstDirective, _lastDirective;
   final List<FoldingRegion> _foldingRegions = [];
 
-  DartUnitFoldingComputer(this._unit);
+  DartUnitFoldingComputer(this._lineInfo, this._unit);
 
   /**
    * Returns a list of folding regions, not `null`.
@@ -26,8 +29,10 @@
     if (_firstDirective != null &&
         _lastDirective != null &&
         _firstDirective != _lastDirective) {
-      _foldingRegions.add(new FoldingRegion(FoldingKind.DIRECTIVES,
-          _firstDirective.offset, _lastDirective.end - _firstDirective.offset));
+      _foldingRegions.add(new FoldingRegion(
+          FoldingKind.DIRECTIVES,
+          _firstDirective.keyword.end,
+          _lastDirective.end - _firstDirective.keyword.end));
     }
 
     return _foldingRegions;
@@ -42,11 +47,76 @@
   _DartUnitFoldingComputerVisitor(this._computer);
 
   @override
+  Object visitBlockFunctionBody(BlockFunctionBody node) {
+    final FoldingKind kind = node.parent is ConstructorDeclaration ||
+            node.parent is MethodDeclaration
+        ? FoldingKind.CLASS_MEMBER
+        : FoldingKind.TOP_LEVEL_DECLARATION;
+    _addRegion(
+        node.block.leftBracket.end, node.block.rightBracket.offset, kind);
+    return super.visitBlockFunctionBody(node);
+  }
+
+  @override
+  Object visitClassDeclaration(ClassDeclaration node) {
+    _addRegion(node.leftBracket.end, node.rightBracket.offset,
+        FoldingKind.TOP_LEVEL_DECLARATION);
+    return super.visitClassDeclaration(node);
+  }
+
+  @override
+  Object visitComment(Comment node) {
+    final FoldingKind kind = node.isDocumentation
+        ? FoldingKind.DOCUMENTATION_COMMENT
+        : FoldingKind.COMMENT;
+    _addRegion(node.offset, node.end, kind);
+    return super.visitComment(node);
+  }
+
+  @override
+  Object visitExportDirective(ExportDirective node) {
+    _recordDirective(node);
+    return super.visitExportDirective(node);
+  }
+
+  @override
   visitImportDirective(ImportDirective node) {
-    if (_computer._firstDirective == null) {
-      _computer._firstDirective = node;
-    }
-    _computer._lastDirective = node;
+    _recordDirective(node);
     return super.visitImportDirective(node);
   }
+
+  @override
+  Object visitLibraryDirective(LibraryDirective node) {
+    _recordDirective(node);
+    return super.visitLibraryDirective(node);
+  }
+
+  @override
+  Object visitPartDirective(PartDirective node) {
+    _recordDirective(node);
+    return super.visitPartDirective(node);
+  }
+
+  @override
+  Object visitPartOfDirective(PartOfDirective node) {
+    _recordDirective(node);
+    return super.visitPartOfDirective(node);
+  }
+
+  _addRegion(int startOffset, int endOffset, FoldingKind kind) {
+    // TODO(dantup): This class is marked deprecated; find out what to change it to.
+    final LineInfo_Location start =
+        _computer._lineInfo.getLocation(startOffset);
+    final LineInfo_Location end = _computer._lineInfo.getLocation(endOffset);
+
+    if (start.lineNumber != end.lineNumber) {
+      _computer._foldingRegions
+          .add(new FoldingRegion(kind, startOffset, endOffset - startOffset));
+    }
+  }
+
+  _recordDirective(Directive node) {
+    _computer._firstDirective ??= node;
+    _computer._lastDirective = node;
+  }
 }
diff --git a/pkg/analysis_server/lib/src/operation/operation_analysis.dart b/pkg/analysis_server/lib/src/operation/operation_analysis.dart
index c08b148..24a4811 100644
--- a/pkg/analysis_server/lib/src/operation/operation_analysis.dart
+++ b/pkg/analysis_server/lib/src/operation/operation_analysis.dart
@@ -81,10 +81,10 @@
   });
 }
 
-void sendAnalysisNotificationFolding(
-    AnalysisServer server, String file, CompilationUnit dartUnit) {
+void sendAnalysisNotificationFolding(AnalysisServer server, String file,
+    LineInfo lineInfo, CompilationUnit dartUnit) {
   _sendNotification(server, () {
-    var regions = new DartUnitFoldingComputer(dartUnit).compute();
+    var regions = new DartUnitFoldingComputer(lineInfo, dartUnit).compute();
     var params = new protocol.AnalysisFoldingParams(file, regions);
     server.sendNotification(params.toNotification());
   });
diff --git a/pkg/analysis_server/test/analysis/notification_folding_test.dart b/pkg/analysis_server/test/analysis/notification_folding_test.dart
index 51a400b..6d43ddb 100644
--- a/pkg/analysis_server/test/analysis/notification_folding_test.dart
+++ b/pkg/analysis_server/test/analysis/notification_folding_test.dart
@@ -15,11 +15,7 @@
 
 main() {
   defineReflectiveSuite(() {
-    // TODO(dantup): Uncomment once implementation is complete.
-    // Cannot just mark the tests as @failingTest as they time out
-    // (no FOLDING notification ever) and failingTest doesn't seem
-    // to cover that.
-    // defineReflectiveTests(_AnalysisNotificationFoldingTest);
+    defineReflectiveTests(_AnalysisNotificationFoldingTest);
   });
 }
 
@@ -33,7 +29,9 @@
 ''';
 
   static final expectedResults = [
-    new FoldingRegion(FoldingKind.DIRECTIVES, 0, 40)
+    // We don't include the first "import" in the region because
+    // we want that to remain visible (not collapse).
+    new FoldingRegion(FoldingKind.DIRECTIVES, 6, 34)
   ];
 
   List<FoldingRegion> lastRegions;
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 c9d00a1..171926b 100644
--- a/pkg/analysis_server/test/src/computer/folding_computer_test.dart
+++ b/pkg/analysis_server/test/src/computer/folding_computer_test.dart
@@ -41,13 +41,13 @@
 
   test_multiple_import_directives() async {
     String content = """
-/*1*/import 'dart:async';
+import/*1:INC*/ 'dart:async';
 
 // We can have comments
 import 'package:a/b.dart';
 import 'package:b/c.dart';
 
-import '../a.dart';/*1:DIRECTIVES*/
+import '../a.dart';/*1:EXC:DIRECTIVES*/
 
 main() {}
 """;
@@ -56,11 +56,99 @@
     _compareRegions(regions, content);
   }
 
+  test_multiple_directive_types() async {
+    String content = """
+import/*1:INC*/ 'dart:async';
+
+// We can have comments
+import 'package:a/b.dart';
+import 'package:b/c.dart';
+
+export '../a.dart';/*1:EXC:DIRECTIVES*/
+
+main() {}
+""";
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content);
+  }
+
+  test_function() async {
+    String content = """
+// Content before
+
+main() {/*1:INC*/
+  print("Hello, world!");
+/*1:INC:TOP_LEVEL_DECLARATION*/}
+
+// Content after
+""";
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content);
+  }
+
+  test_function_with_dart_doc() async {
+    String content = """
+// Content before
+
+/*1:EXC*//// This is a doc comment
+/// that spans lines/*1:INC:DOCUMENTATION_COMMENT*/
+main() {/*2:INC*/
+  print("Hello, world!");
+/*2:INC:TOP_LEVEL_DECLARATION*/}
+
+// Content after
+""";
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content);
+  }
+
+  test_nested_function() async {
+    String content = """
+// Content before
+
+main() {/*1:INC*/
+  doPrint() {/*2:INC*/
+    print("Hello, world!");
+  /*2:INC:TOP_LEVEL_DECLARATION*/}
+  doPrint();
+/*1:INC:TOP_LEVEL_DECLARATION*/}
+
+// Content after
+""";
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content);
+  }
+
+  test_class() async {
+    String content = """
+// Content before
+
+class Person {/*1:INC*/
+  Person() {/*2:INC*/
+    print("Hello, world!");
+  /*2:INC:CLASS_MEMBER*/}
+
+  void sayHello() {/*3:INC*/
+    print("Hello, world!");
+  /*3:INC:CLASS_MEMBER*/}
+/*1:INC:TOP_LEVEL_DECLARATION*/}
+
+// Content after
+""";
+
+    final regions = await _computeRegions(content);
+    _compareRegions(regions, content);
+  }
+
   /// Compares provided folding regions with expected
   /// regions extracted from the comments in the provided content.
   void _compareRegions(List<FoldingRegion> regions, String content) {
     // Find all numeric markers for region starts.
-    final regex = new RegExp(r'/\*(\d+)\*/');
+    final regex = new RegExp(r'/\*(\d+):(INC|EXC)\*/');
     final expectedRegions = regex.allMatches(content);
 
     // Check we didn't get more than expected, since the loop below only
@@ -71,14 +159,22 @@
     // ensure it's in the results.
     expectedRegions.forEach((m) {
       final i = m.group(1);
+      final inclusiveStart = m.group(2) == "INC";
       // Find the end marker.
-      final endMatch = new RegExp('/\\*$i:(.+?)\\*/').firstMatch(content);
+      final endMatch =
+          new RegExp('/\\*$i:(INC|EXC):(.+?)\\*/').firstMatch(content);
 
-      final expectedStart = m.end;
-      final expectedLength = endMatch.start - expectedStart;
-      final expectedKindString = endMatch.group(1);
-      final expectedKind = FoldingKind.VALUES
-          .firstWhere((f) => f.toString() == 'FoldingKind.$expectedKindString');
+      final inclusiveEnd = endMatch.group(1) == "INC";
+      final expectedKindString = endMatch.group(2);
+      final expectedKind = FoldingKind.VALUES.firstWhere(
+          (f) => f.toString() == 'FoldingKind.$expectedKindString',
+          orElse: () => throw new Exception(
+              "Annotated test code references $expectedKindString but "
+              "this does not exist in FoldingKind"));
+
+      final expectedStart = inclusiveStart ? m.start : m.end;
+      final expectedLength =
+          (inclusiveEnd ? endMatch.end : endMatch.start) - expectedStart;
 
       expect(
           regions,
@@ -90,7 +186,8 @@
   Future<List<FoldingRegion>> _computeRegions(String sourceContent) async {
     newFile(sourcePath, content: sourceContent);
     ResolveResult result = await driver.getResult(sourcePath);
-    DartUnitFoldingComputer computer = new DartUnitFoldingComputer(result.unit);
+    DartUnitFoldingComputer computer =
+        new DartUnitFoldingComputer(result.lineInfo, result.unit);
     return computer.compute();
   }
 }