Add a skeleton for Dart Folding

Only currently adds a single region around Directives.

Bug: https://github.com/dart-lang/sdk/issues/33033
Change-Id: Ibde0400c5815c00b1f94bf592d8cc9eb7cb592cf
Reviewed-on: https://dart-review.googlesource.com/54232
Commit-Queue: Danny Tuppeny <dantup@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index 240ec38..b838056 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -1275,6 +1275,15 @@
                 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.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
new file mode 100644
index 0000000..19b0c0d
--- /dev/null
+++ b/pkg/analysis_server/lib/src/computer/computer_folding.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2018, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart';
+
+/**
+ * A computer for [CompilationUnit] folding.
+ */
+class DartUnitFoldingComputer {
+  final CompilationUnit _unit;
+
+  Directive _firstDirective, _lastDirective;
+  final List<FoldingRegion> _foldingRegions = [];
+
+  DartUnitFoldingComputer(this._unit);
+
+  /**
+   * Returns a list of folding regions, not `null`.
+   */
+  List<FoldingRegion> compute() {
+    _unit.accept(new _DartUnitFoldingComputerVisitor(this));
+
+    if (_firstDirective != null &&
+        _lastDirective != null &&
+        _firstDirective != _lastDirective) {
+      _foldingRegions.add(new FoldingRegion(FoldingKind.DIRECTIVES,
+          _firstDirective.offset, _lastDirective.end - _firstDirective.offset));
+    }
+
+    return _foldingRegions;
+  }
+}
+
+/**
+ * An AST visitor for [DartUnitFoldingComputer].
+ */
+class _DartUnitFoldingComputerVisitor extends RecursiveAstVisitor<Object> {
+  final DartUnitFoldingComputer _computer;
+  _DartUnitFoldingComputerVisitor(this._computer);
+
+  @override
+  visitImportDirective(ImportDirective node) {
+    if (_computer._firstDirective == null) {
+      _computer._firstDirective = node;
+    }
+    _computer._lastDirective = node;
+    return super.visitImportDirective(node);
+  }
+}
diff --git a/pkg/analysis_server/lib/src/operation/operation_analysis.dart b/pkg/analysis_server/lib/src/operation/operation_analysis.dart
index 3a9fbc2..c08b148 100644
--- a/pkg/analysis_server/lib/src/operation/operation_analysis.dart
+++ b/pkg/analysis_server/lib/src/operation/operation_analysis.dart
@@ -6,6 +6,7 @@
 
 import 'package:analysis_server/src/analysis_server.dart';
 import 'package:analysis_server/src/computer/computer_closingLabels.dart';
+import 'package:analysis_server/src/computer/computer_folding.dart';
 import 'package:analysis_server/src/computer/computer_highlights.dart';
 import 'package:analysis_server/src/computer/computer_highlights2.dart';
 import 'package:analysis_server/src/computer/computer_outline.dart';
@@ -80,6 +81,15 @@
   });
 }
 
+void sendAnalysisNotificationFolding(
+    AnalysisServer server, String file, CompilationUnit dartUnit) {
+  _sendNotification(server, () {
+    var regions = new DartUnitFoldingComputer(dartUnit).compute();
+    var params = new protocol.AnalysisFoldingParams(file, regions);
+    server.sendNotification(params.toNotification());
+  });
+}
+
 void sendAnalysisNotificationFlushResults(
     AnalysisServer server, List<String> files) {
   _sendNotification(server, () {
diff --git a/pkg/analysis_server/test/analysis/notification_folding_test.dart b/pkg/analysis_server/test/analysis/notification_folding_test.dart
new file mode 100644
index 0000000..51a400b
--- /dev/null
+++ b/pkg/analysis_server/test/analysis/notification_folding_test.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2018, 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/protocol/protocol.dart';
+import 'package:analysis_server/protocol/protocol_constants.dart';
+import 'package:analysis_server/protocol/protocol_generated.dart';
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../analysis_abstract.dart';
+
+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);
+  });
+}
+
+@reflectiveTest
+class _AnalysisNotificationFoldingTest extends AbstractAnalysisTest {
+  static const sampleCode = '''
+import 'dart:async';
+import 'dart:core';
+
+main async() {}
+''';
+
+  static final expectedResults = [
+    new FoldingRegion(FoldingKind.DIRECTIVES, 0, 40)
+  ];
+
+  List<FoldingRegion> lastRegions;
+
+  Completer _regionsReceived;
+
+  void processNotification(Notification notification) {
+    if (notification.event == ANALYSIS_NOTIFICATION_FOLDING) {
+      var params = new AnalysisFoldingParams.fromNotification(notification);
+      if (params.file == testFile) {
+        lastRegions = params.regions;
+        _regionsReceived.complete(null);
+      }
+    } else if (notification.event == SERVER_NOTIFICATION_ERROR) {
+      var params = new ServerErrorParams.fromNotification(notification);
+      throw "${params.message}\n${params.stackTrace}";
+    }
+  }
+
+  @override
+  void setUp() {
+    super.setUp();
+    createProject();
+  }
+
+  void subscribeForFolding() {
+    addAnalysisSubscription(AnalysisService.FOLDING, testFile);
+  }
+
+  test_afterAnalysis() async {
+    addTestFile(sampleCode);
+    await waitForTasksFinished();
+    expect(lastRegions, isNull);
+
+    await waitForFolding(() => subscribeForFolding());
+
+    expect(lastRegions, expectedResults);
+  }
+
+  test_afterUpdate() async {
+    addTestFile('');
+    // Currently required to get notifications on updates
+    setPriorityFiles([testFile]);
+
+    // Before subscribing, we shouldn't have had any folding regions.
+    await waitForTasksFinished();
+    expect(lastRegions, isNull);
+
+    // With no content, there should be zero regions.
+    await waitForFolding(() => subscribeForFolding());
+    expect(lastRegions, hasLength(0));
+
+    // With sample code there will be folding regions.
+    await waitForFolding(() => modifyTestFile(sampleCode));
+
+    expect(lastRegions, expectedResults);
+  }
+
+  Future waitForFolding(action()) {
+    _regionsReceived = new Completer();
+    action();
+    return _regionsReceived.future;
+  }
+}
diff --git a/pkg/analysis_server/test/analysis/test_all.dart b/pkg/analysis_server/test/analysis/test_all.dart
index 76e1f5b..fa72b86 100644
--- a/pkg/analysis_server/test/analysis/test_all.dart
+++ b/pkg/analysis_server/test/analysis/test_all.dart
@@ -13,6 +13,7 @@
     as notification_analyzedFiles_test;
 import 'notification_closingLabels_test.dart'
     as notification_closingLabels_test;
+import 'notification_folding_test.dart' as notification_folding_test;
 import 'notification_errors_test.dart' as notification_errors_test;
 import 'notification_highlights_test.dart' as notification_highlights_test;
 import 'notification_highlights_test2.dart' as notification_highlights_test2;
@@ -36,6 +37,7 @@
     notification_analysis_options_test.main();
     notification_analyzedFiles_test.main();
     notification_closingLabels_test.main();
+    notification_folding_test.main();
     notification_errors_test.main();
     notification_highlights_test.main();
     notification_highlights_test2.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
new file mode 100644
index 0000000..c9d00a1
--- /dev/null
+++ b/pkg/analysis_server/test/src/computer/folding_computer_test.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2018, 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/src/computer/computer_folding.dart';
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../../abstract_context.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FoldingComputerTest);
+  });
+}
+
+@reflectiveTest
+class FoldingComputerTest extends AbstractContextTest {
+  String sourcePath;
+
+  setUp() {
+    super.setUp();
+    sourcePath = resourceProvider.convertPath('/p/lib/source.dart');
+  }
+
+  test_single_import_directives() async {
+    String content = """
+import 'dart:async';
+
+main() {}
+""";
+
+    // Since there are no region comment markers above
+    final regions = await _computeRegions(content);
+    expect(regions, hasLength(0));
+  }
+
+  test_multiple_import_directives() async {
+    String content = """
+/*1*/import 'dart:async';
+
+// We can have comments
+import 'package:a/b.dart';
+import 'package:b/c.dart';
+
+import '../a.dart';/*1:DIRECTIVES*/
+
+main() {}
+""";
+
+    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 expectedRegions = regex.allMatches(content);
+
+    // 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));
+
+    // Go through each marker, find the expected region start/end and
+    // ensure it's in the results.
+    expectedRegions.forEach((m) {
+      final i = m.group(1);
+      // Find the end marker.
+      final endMatch = new RegExp('/\\*$i:(.+?)\\*/').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');
+
+      expect(
+          regions,
+          contains(
+              new FoldingRegion(expectedKind, expectedStart, expectedLength)));
+    });
+  }
+
+  Future<List<FoldingRegion>> _computeRegions(String sourceContent) async {
+    newFile(sourcePath, content: sourceContent);
+    ResolveResult result = await driver.getResult(sourcePath);
+    DartUnitFoldingComputer computer = new DartUnitFoldingComputer(result.unit);
+    return computer.compute();
+  }
+}
diff --git a/pkg/analysis_server/test/src/computer/test_all.dart b/pkg/analysis_server/test/src/computer/test_all.dart
index 4bf0a76..8159db8 100644
--- a/pkg/analysis_server/test/src/computer/test_all.dart
+++ b/pkg/analysis_server/test/src/computer/test_all.dart
@@ -5,6 +5,7 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import 'closingLabels_computer_test.dart' as closingLabels_computer_test;
+import 'folding_computer_test.dart' as folding_computer_test;
 import 'import_elements_computer_test.dart' as import_elements_computer_test;
 import 'imported_elements_computer_test.dart'
     as imported_elements_computer_test;
@@ -13,6 +14,7 @@
 main() {
   defineReflectiveSuite(() {
     closingLabels_computer_test.main();
+    folding_computer_test.main();
     import_elements_computer_test.main();
     imported_elements_computer_test.main();
     outline_computer_test.main();