Version 2.14.0-72.0.dev

Merge commit '58413c5d5481e4c4b525582d3bd6152e50d2afef' into 'dev'
diff --git a/pkg/analysis_server/lib/src/computer/computer_hover.dart b/pkg/analysis_server/lib/src/computer/computer_hover.dart
index f40542f..b6850c2 100644
--- a/pkg/analysis_server/lib/src/computer/computer_hover.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_hover.dart
@@ -102,7 +102,7 @@
           }
         }
         // documentation
-        hover.dartdoc = computeDocumentation(_dartdocInfo, element);
+        hover.dartdoc = computeDocumentation(_dartdocInfo, element)?.full;
       }
       // parameter
       hover.parameter = _elementDisplayString(
@@ -141,8 +141,9 @@
         withNullability: _unit.isNonNullableByDefault);
   }
 
-  static String? computeDocumentation(
-      DartdocDirectiveInfo dartdocInfo, Element elementBeingDocumented) {
+  static Documentation? computeDocumentation(
+      DartdocDirectiveInfo dartdocInfo, Element elementBeingDocumented,
+      {bool includeSummary = false}) {
     // TODO(dantup) We're reusing this in parameter information - move it
     // somewhere shared?
     Element? element = elementBeingDocumented;
@@ -192,12 +193,14 @@
     if (rawDoc == null) {
       return null;
     }
-    var result = dartdocInfo.processDartdoc(rawDoc);
+    var result =
+        dartdocInfo.processDartdoc(rawDoc, includeSummary: includeSummary);
 
     var documentedElementClass = documentedElement.enclosingElement;
     if (documentedElementClass != null &&
         documentedElementClass != element.enclosingElement) {
-      result += '\n\nCopied from `${documentedElementClass.displayName}`.';
+      var documentedClass = documentedElementClass.displayName;
+      result.full = '${result.full}\n\nCopied from `$documentedClass`.';
     }
 
     return result;
diff --git a/pkg/analysis_server/lib/src/computer/computer_signature.dart b/pkg/analysis_server/lib/src/computer/computer_signature.dart
index 9eb563a..55ce1a9 100644
--- a/pkg/analysis_server/lib/src/computer/computer_signature.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_signature.dart
@@ -62,7 +62,8 @@
 
     return AnalysisGetSignatureResult(name, parameters,
         dartdoc: DartUnitHoverComputer.computeDocumentation(
-            _dartdocInfo, execElement));
+                _dartdocInfo, execElement)
+            ?.full);
   }
 
   ParameterInfo _convertParam(ParameterElement param) {
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart b/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart
index a7062c3..179e807 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart
@@ -20,7 +20,7 @@
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/dart/element/nullability_suffix.dart';
 import 'package:analyzer/dart/element/type.dart';
-import 'package:analyzer/src/util/comment.dart';
+import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/utilities/range_factory.dart';
 
@@ -1111,10 +1111,11 @@
   /// documentation fields.
   void _setDocumentation(CompletionSuggestion suggestion, Element element) {
     var doc = DartUnitHoverComputer.computeDocumentation(
-        request.dartdocDirectiveInfo, element);
-    if (doc != null) {
-      suggestion.docComplete = doc;
-      suggestion.docSummary = getDartDocSummary(doc);
+        request.dartdocDirectiveInfo, element,
+        includeSummary: true);
+    if (doc is DocumentationWithSummary) {
+      suggestion.docComplete = doc.full;
+      suggestion.docSummary = doc.summary;
     }
   }
 }
diff --git a/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart b/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart
index 85f2e08..540d866 100644
--- a/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart
+++ b/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart
@@ -60,82 +60,143 @@
   /// Macro directives are replaced with the body of the corresponding template.
   ///
   /// Youtube and animation directives are replaced with markdown hyperlinks.
-  String processDartdoc(String comment) {
+  Documentation processDartdoc(String comment, {bool includeSummary = false}) {
     List<String> lines = _stripDelimiters(comment);
+    var firstBlankLine = lines.length;
     for (int i = lines.length - 1; i >= 0; i--) {
       String line = lines[i];
-      var match = macroRegExp.firstMatch(line);
-      if (match != null) {
-        var name = match.group(1)!;
-        var value = templateMap[name];
-        if (value != null) {
-          lines[i] = value;
-        }
-        continue;
-      }
-
-      match = videoRegExp.firstMatch(line);
-      if (match != null) {
-        var uri = match.group(2);
-        if (uri != null && uri.isNotEmpty) {
-          String label = uri;
-          if (label.startsWith('https://')) {
-            label = label.substring('https://'.length);
+      if (line.isEmpty) {
+        // Because we're iterating from the last line to the first, the last
+        // blank line we find is the first.
+        firstBlankLine = i;
+      } else {
+        var match = macroRegExp.firstMatch(line);
+        if (match != null) {
+          var name = match.group(1)!;
+          var value = templateMap[name];
+          if (value != null) {
+            lines[i] = value;
           }
-          lines[i] = '[$label]($uri)';
+          continue;
         }
-        continue;
+
+        match = videoRegExp.firstMatch(line);
+        if (match != null) {
+          var uri = match.group(2);
+          if (uri != null && uri.isNotEmpty) {
+            String label = uri;
+            if (label.startsWith('https://')) {
+              label = label.substring('https://'.length);
+            }
+            lines[i] = '[$label]($uri)';
+          }
+          continue;
+        }
       }
     }
-    return lines.join('\n');
+    if (includeSummary) {
+      var full = lines.join('\n');
+      var summary = firstBlankLine == lines.length
+          ? full
+          : lines.getRange(0, firstBlankLine).join('\n').trim();
+      return DocumentationWithSummary(full: full, summary: summary);
+    }
+    return Documentation(full: lines.join('\n'));
+  }
+
+  bool _isWhitespace(String comment, int index, bool includeEol) {
+    if (comment.startsWith(' ', index) ||
+        comment.startsWith('\t', index) ||
+        (includeEol && comment.startsWith('\n', index))) {
+      return true;
+    }
+    return false;
+  }
+
+  int _skipWhitespaceBackward(String comment, int start, int end,
+      [bool skipEol = false]) {
+    while (start < end && _isWhitespace(comment, end, skipEol)) {
+      end--;
+    }
+    return end;
+  }
+
+  int _skipWhitespaceForward(String comment, int start, int end,
+      [bool skipEol = false]) {
+    while (start < end && _isWhitespace(comment, start, skipEol)) {
+      start++;
+    }
+    return start;
   }
 
   /// Remove the delimiters from the given [comment].
   List<String> _stripDelimiters(String comment) {
-    //
-    // Remove /** */.
-    //
+    var start = 0;
+    var end = comment.length;
     if (comment.startsWith('/**')) {
-      comment = comment.substring(3);
+      start = _skipWhitespaceForward(comment, 3, end, true);
+      if (comment.endsWith('*/')) {
+        end = _skipWhitespaceBackward(comment, start, end - 2, true);
+      }
     }
-    if (comment.endsWith('*/')) {
-      comment = comment.substring(0, comment.length - 2);
-    }
-    comment = comment.trim();
-    //
-    // Remove leading '* ' and '/// '.
-    //
-    List<String> lines = comment.split('\n');
-    int firstNonEmpty = lines.length + 1;
-    int lastNonEmpty = -1;
-    for (var i = 0; i < lines.length; i++) {
-      String line = lines[i];
-      line = line.trim();
-      if (line.startsWith('*')) {
-        line = line.substring(1);
-        if (line.startsWith(' ')) {
-          line = line.substring(1);
+    var line = -1;
+    var firstNonEmpty = -1;
+    var lastNonEmpty = -1;
+    var lines = <String>[];
+    while (start < end) {
+      line++;
+      var eolIndex = comment.indexOf('\n', start);
+      if (eolIndex < 0) {
+        eolIndex = end;
+      }
+      var lineStart = _skipWhitespaceForward(comment, start, eolIndex);
+      if (comment.startsWith('///', lineStart)) {
+        lineStart += 3;
+        if (_isWhitespace(comment, lineStart, false)) {
+          lineStart++;
         }
-      } else if (line.startsWith('///')) {
-        line = line.substring(3);
-        if (line.startsWith(' ')) {
-          line = line.substring(1);
+      } else if (comment.startsWith('*', lineStart)) {
+        lineStart += 1;
+        if (_isWhitespace(comment, lineStart, false)) {
+          lineStart++;
         }
       }
-      if (line.isNotEmpty) {
-        if (i < firstNonEmpty) {
-          firstNonEmpty = i;
+      var lineEnd =
+          _skipWhitespaceBackward(comment, lineStart, eolIndex - 1) + 1;
+      if (lineStart < lineEnd) {
+        // If the line is not empty, update the line range.
+        if (firstNonEmpty < 0) {
+          firstNonEmpty = line;
         }
-        if (i > lastNonEmpty) {
-          lastNonEmpty = i;
+        if (line > lastNonEmpty) {
+          lastNonEmpty = line;
         }
+        lines.add(comment.substring(lineStart, lineEnd));
+      } else {
+        lines.add('');
       }
-      lines[i] = line;
+      start = eolIndex + 1;
     }
     if (lastNonEmpty < firstNonEmpty) {
       // All of the lines are empty.
-      return <String>[];
+      return const <String>[];
     }
     return lines.sublist(firstNonEmpty, lastNonEmpty + 1);
   }
 }
+
+/// A representation of the documentation for an element.
+class Documentation {
+  String full;
+
+  Documentation({required this.full});
+}
+
+/// A representation of the documentation for an element that includes a
+/// summary.
+class DocumentationWithSummary extends Documentation {
+  final String summary;
+
+  DocumentationWithSummary({required String full, required this.summary})
+      : super(full: full);
+}
diff --git a/pkg/analyzer/test/src/dartdoc/dartdoc_directive_info_test.dart b/pkg/analyzer/test/src/dartdoc/dartdoc_directive_info_test.dart
index e796527..05f4cfd 100644
--- a/pkg/analyzer/test/src/dartdoc/dartdoc_directive_info_test.dart
+++ b/pkg/analyzer/test/src/dartdoc/dartdoc_directive_info_test.dart
@@ -17,11 +17,11 @@
   DartdocDirectiveInfo info = DartdocDirectiveInfo();
 
   test_processDartdoc_animation_directive() {
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4}
 ''');
     expect(
-        result,
+        result.full,
         '[flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4]'
         '(https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4)');
   }
@@ -34,13 +34,13 @@
  * template.
  * {@endtemplate}
  */''');
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /**
  * Before macro.
  * {@macro foo}
  * After macro.
  */''');
-    expect(result, '''
+    expect(result.full, '''
 Before macro.
 Body of the
 template.
@@ -48,11 +48,11 @@
   }
 
   test_processDartdoc_macro_undefined() {
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /**
  * {@macro foo}
  */''');
-    expect(result, '''
+    expect(result.full, '''
 {@macro foo}''');
   }
 
@@ -67,7 +67,7 @@
 /// {@template bar}
 /// Second template.
 /// {@endtemplate}''');
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /**
  * Before macro.
  * {@macro foo}
@@ -75,7 +75,7 @@
  * {@macro bar}
  * After macro.
  */''');
-    expect(result, '''
+    expect(result.full, '''
 Before macro.
 First template.
 Between macros.
@@ -84,27 +84,51 @@
   }
 
   test_processDartdoc_noMacro() {
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /**
  * Comment without a macro.
  */''');
-    expect(result, '''
+    expect(result.full, '''
+Comment without a macro.''');
+  }
+
+  test_processDartdoc_summary_different() {
+    var result = info.processDartdoc('''
+/// Comment without a macro.
+///
+/// Has content after summary.
+''', includeSummary: true) as DocumentationWithSummary;
+    expect(result.full, '''
+Comment without a macro.
+
+Has content after summary.''');
+    expect(result.summary, '''
+Comment without a macro.''');
+  }
+
+  test_processDartdoc_summary_same() {
+    var result = info.processDartdoc('''
+/// Comment without a macro.
+''', includeSummary: true) as DocumentationWithSummary;
+    expect(result.full, '''
+Comment without a macro.''');
+    expect(result.summary, '''
 Comment without a macro.''');
   }
 
   test_processDartdoc_youtube_directive() {
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /// {@youtube 560 315 https://www.youtube.com/watch?v=2uaoEDOgk_I}
 ''');
-    expect(result, '''
+    expect(result.full, '''
 [www.youtube.com/watch?v=2uaoEDOgk_I](https://www.youtube.com/watch?v=2uaoEDOgk_I)''');
   }
 
   test_processDartdoc_youtube_malformed() {
-    String result = info.processDartdoc('''
+    var result = info.processDartdoc('''
 /// {@youtube 560x315 https://www.youtube.com/watch?v=2uaoEDOgk_I}
 ''');
-    expect(result,
+    expect(result.full,
         '{@youtube 560x315 https://www.youtube.com/watch?v=2uaoEDOgk_I}');
   }
 }
diff --git a/pkg/analyzer/test/src/services/available_declarations_test.dart b/pkg/analyzer/test/src/services/available_declarations_test.dart
index ec8b732..9829096 100644
--- a/pkg/analyzer/test/src/services/available_declarations_test.dart
+++ b/pkg/analyzer/test/src/services/available_declarations_test.dart
@@ -1178,7 +1178,7 @@
 /// Before macro.
 /// {@macro foo}
 /// After macro.''');
-    expect(result, '''
+    expect(result.full, '''
 Before macro.
 Body of the template.
 After macro.''');
diff --git a/tools/VERSION b/tools/VERSION
index 62e3603..811a318 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 71
+PRERELEASE 72
 PRERELEASE_PATCH 0
\ No newline at end of file