[dart analyze] wrap longer error correction messages

Change-Id: Ibed3e696c1eef76034ecc66b0156fe3cb7e61d35
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/174862
Commit-Queue: Devon Carew <devoncarew@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/dartdev/lib/src/commands/analyze.dart b/pkg/dartdev/lib/src/commands/analyze.dart
index 55984a9..5e533f4 100644
--- a/pkg/dartdev/lib/src/commands/analyze.dart
+++ b/pkg/dartdev/lib/src/commands/analyze.dart
@@ -24,6 +24,8 @@
   /// message. The width left for the severity label plus the separator width.
   static const int _bodyIndentWidth = _severityWidth + 3;
 
+  static final String _bodyIndent = ' ' * _bodyIndentWidth;
+
   static final int _pipeCodeUnit = '|'.codeUnitAt(0);
 
   static final int _slashCodeUnit = '\\'.codeUnitAt(0);
@@ -62,6 +64,7 @@
 
   @override
   String get invocation => '${super.invocation} [<directory>]';
+
   @override
   FutureOr<int> run() async {
     if (argResults.rest.length > 1) {
@@ -167,6 +170,10 @@
 
     log.stdout('');
 
+    final wrapWidth = dartdevUsageLineLength == null
+        ? null
+        : (dartdevUsageLineLength - _bodyIndentWidth);
+
     for (final AnalysisError error in errors) {
       // error • Message ... at path.dart:line:col • (code)
 
@@ -183,19 +190,28 @@
         '(${error.code})',
       );
 
-      var padding = ' ' * _bodyIndentWidth;
       if (verbose) {
         for (var message in error.contextMessages) {
-          log.stdout('$padding${message.message} '
-              'at ${message.filePath}:${message.line}:${message.column}');
+          // Wrap longer context messages.
+          var contextMessage = wrapText(
+              '${message.message} at '
+              '${message.filePath}:${message.line}:${message.column}',
+              width: wrapWidth);
+          log.stdout('$_bodyIndent'
+              '${contextMessage.replaceAll('\n', '\n$_bodyIndent')}');
         }
       }
+
       if (error.correction != null) {
-        log.stdout('$padding${error.correction}');
+        // Wrap longer correction messages.
+        var correction = wrapText(error.correction, width: wrapWidth);
+        log.stdout(
+            '$_bodyIndent${correction.replaceAll('\n', '\n$_bodyIndent')}');
       }
+
       if (verbose) {
         if (error.url != null) {
-          log.stdout('$padding${error.url}');
+          log.stdout('$_bodyIndent${error.url}');
         }
       }
     }
diff --git a/pkg/dartdev/lib/src/utils.dart b/pkg/dartdev/lib/src/utils.dart
index 88b80ed..43249c3 100644
--- a/pkg/dartdev/lib/src/utils.dart
+++ b/pkg/dartdev/lib/src/utils.dart
@@ -68,3 +68,39 @@
 
   bool get isDartFile => this is File && p.extension(path) == '.dart';
 }
+
+/// Wraps [text] to the given [width], if provided.
+String wrapText(String text, {int width}) {
+  if (width == null) {
+    return text;
+  }
+
+  var buffer = StringBuffer();
+  var lineMaxEndIndex = width;
+  var lineStartIndex = 0;
+
+  while (true) {
+    if (lineMaxEndIndex >= text.length) {
+      buffer.write(text.substring(lineStartIndex, text.length));
+      break;
+    } else {
+      var lastSpaceIndex = text.lastIndexOf(' ', lineMaxEndIndex);
+      if (lastSpaceIndex == -1 || lastSpaceIndex <= lineStartIndex) {
+        // No space between [lineStartIndex] and [lineMaxEndIndex]. Get the
+        // _next_ space.
+        lastSpaceIndex = text.indexOf(' ', lineMaxEndIndex);
+        if (lastSpaceIndex == -1) {
+          // No space at all after [lineStartIndex].
+          lastSpaceIndex = text.length;
+          buffer.write(text.substring(lineStartIndex, lastSpaceIndex));
+          break;
+        }
+      }
+      buffer.write(text.substring(lineStartIndex, lastSpaceIndex));
+      buffer.writeln();
+      lineStartIndex = lastSpaceIndex + 1;
+    }
+    lineMaxEndIndex = lineStartIndex + width;
+  }
+  return buffer.toString();
+}
diff --git a/pkg/dartdev/test/utils_test.dart b/pkg/dartdev/test/utils_test.dart
index 9740dd1..51f6d5f 100644
--- a/pkg/dartdev/test/utils_test.dart
+++ b/pkg/dartdev/test/utils_test.dart
@@ -6,7 +6,7 @@
 import 'dart:io';
 
 import 'package:dartdev/src/utils.dart';
-import 'package:path/path.dart';
+import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 
 void main() {
@@ -32,7 +32,7 @@
 
     test('nested', () {
       var dir = Directory('foo');
-      expect(relativePath(join(dir.absolute.path, 'path'), dir), 'path');
+      expect(relativePath(path.join(dir.absolute.path, 'path'), dir), 'path');
     });
   });
 
@@ -93,10 +93,10 @@
     test('name', () {
       expect(Directory('').name, '');
       expect(Directory('dirName').name, 'dirName');
-      expect(Directory('dirName$separator').name, 'dirName');
+      expect(Directory('dirName${path.separator}').name, 'dirName');
       expect(File('').name, '');
       expect(File('foo.dart').name, 'foo.dart');
-      expect(File('${separator}foo.dart').name, 'foo.dart');
+      expect(File('${path.separator}foo.dart').name, 'foo.dart');
       expect(File('bar.bart').name, 'bar.bart');
     });
   });
@@ -133,6 +133,66 @@
           orderedEquals(['pub', 'publish', '--help']));
     });
   });
+
+  group('wrapText', () {
+    test('oneLine_wordLongerThanLine', () {
+      expect(wrapText('http://long-url', width: 10), equals('http://long-url'));
+    });
+
+    test('singleLine', () {
+      expect(wrapText('one two', width: 10), equals('one two'));
+    });
+
+    test('singleLine_exactLength', () {
+      expect(wrapText('one twoooo', width: 10), equals('one twoooo'));
+    });
+
+    test('singleLine_exactLength_minusOne', () {
+      expect(wrapText('one twooo', width: 10), equals('one twooo'));
+    });
+
+    test('singleLine_exactLength_plusOne', () {
+      expect(wrapText('one twooooo', width: 10), equals('one\ntwooooo'));
+    });
+
+    test('twoLines_exactLength', () {
+      expect(wrapText('one two three four', width: 10),
+          equals('one two\nthree four'));
+    });
+
+    test('twoLines_exactLength_minusOne', () {
+      expect(wrapText('one two three fou', width: 10),
+          equals('one two\nthree fou'));
+    });
+
+    test('twoLines_exactLength_plusOne', () {
+      expect(wrapText('one two three fourr', width: 10),
+          equals('one two\nthree\nfourr'));
+    });
+
+    test('twoLines_lastLineEndsWithSpace', () {
+      expect(wrapText('one two three ', width: 10), equals('one two\nthree '));
+    });
+
+    test('twoLines_multipleSpacesAtSplit', () {
+      expect(
+          wrapText('one two.  Three', width: 10), equals('one two. \nThree'));
+    });
+
+    test('twoLines_noSpaceLastLine', () {
+      expect(wrapText('one two three', width: 10), equals('one two\nthree'));
+    });
+
+    test('twoLines_wordLongerThanLine_firstLine', () {
+      expect(wrapText('http://long-url word', width: 10),
+          equals('http://long-url\nword'));
+    });
+
+    test('twoLines_wordLongerThanLine_lastLine', () {
+      expect(wrapText('word http://long-url', width: 10),
+          equals('word\nhttp://long-url'));
+    });
+  });
 }
 
 const String _packageData = '''{