Add support for wrapping command descriptions and other help text.
diff --git a/lib/command_runner.dart b/lib/command_runner.dart
index 3135dbf..62f7fe7 100644
--- a/lib/command_runner.dart
+++ b/lib/command_runner.dart
@@ -39,7 +39,7 @@
   ///
   /// This includes usage for the global arguments as well as a list of
   /// top-level commands.
-  String get usage => "$description\n\n$_usageWithoutDescription";
+  String get usage => _wrap("$description\n\n") + _usageWithoutDescription;
 
   /// An optional footer for [usage].
   ///
@@ -49,17 +49,20 @@
 
   /// Returns [usage] with [description] removed from the beginning.
   String get _usageWithoutDescription {
+    const usagePrefix = "Usage:";
     var usage = '''
-Usage: $invocation
+$usagePrefix ${_wrap(invocation, hangingIndent: usagePrefix.length)}
 
-Global options:
+${_wrap('Global options:')}
 ${argParser.usage}
 
-${_getCommandUsage(_commands)}
+${_getCommandUsage(_commands, lineLength: argParser.usageLineLength)}
 
-Run "$executableName help <command>" for more information about a command.''';
+${_wrap('Run "$executableName help <command>" for more information about a command.')}''';
 
-    if (usageFooter != null) usage += "\n$usageFooter";
+    if (usageFooter != null) {
+      usage += "\n${_wrap(usageFooter)}";
+    }
     return usage;
   }
 
@@ -193,6 +196,9 @@
 
     return (await command.run()) as T;
   }
+
+  String _wrap(String text, {int hangingIndent}) => wrapText(text,
+      length: argParser.usageLineLength, hangingIndent: hangingIndent);
 }
 
 /// A single command.
@@ -277,7 +283,7 @@
   ///
   /// This includes usage for the command's arguments as well as a list of
   /// subcommands, if there are any.
-  String get usage => "$description\n\n$_usageWithoutDescription";
+  String get usage => _wrap("$description\n\n") + _usageWithoutDescription;
 
   /// An optional footer for [usage].
   ///
@@ -285,23 +291,36 @@
   /// added to the end of [usage].
   String get usageFooter => null;
 
+  String _wrap(String text, {int hangingIndent}) {
+    return wrapText(text,
+        length: argParser.usageLineLength, hangingIndent: hangingIndent);
+  }
+
   /// Returns [usage] with [description] removed from the beginning.
   String get _usageWithoutDescription {
+    final length = argParser.usageLineLength;
+    const usagePrefix = "Usage: ";
     var buffer = new StringBuffer()
-      ..writeln('Usage: $invocation')
+      ..writeln(
+          usagePrefix + _wrap(invocation, hangingIndent: usagePrefix.length))
       ..writeln(argParser.usage);
 
     if (_subcommands.isNotEmpty) {
       buffer.writeln();
-      buffer.writeln(_getCommandUsage(_subcommands, isSubcommand: true));
+      buffer.writeln(_getCommandUsage(
+        _subcommands,
+        isSubcommand: true,
+        lineLength: length,
+      ));
     }
 
     buffer.writeln();
-    buffer.write('Run "${runner.executableName} help" to see global options.');
+    buffer.write(
+        _wrap('Run "${runner.executableName} help" to see global options.'));
 
     if (usageFooter != null) {
       buffer.writeln();
-      buffer.write(usageFooter);
+      buffer.write(_wrap(usageFooter));
     }
 
     return buffer.toString();
@@ -355,7 +374,8 @@
   /// The return value is wrapped in a `Future` if necessary and returned by
   /// [CommandRunner.runCommand].
   FutureOr<T> run() {
-    throw new UnimplementedError("Leaf command $this must implement run().");
+    throw new UnimplementedError(
+        _wrap("Leaf command $this must implement run()."));
   }
 
   /// Adds [Command] as a subcommand of this.
@@ -376,7 +396,7 @@
 
   /// Throws a [UsageException] with [message].
   void usageException(String message) =>
-      throw new UsageException(message, _usageWithoutDescription);
+      throw new UsageException(_wrap(message), _usageWithoutDescription);
 }
 
 /// Returns a string representation of [commands] fit for use in a usage string.
@@ -384,7 +404,7 @@
 /// [isSubcommand] indicates whether the commands should be called "commands" or
 /// "subcommands".
 String _getCommandUsage(Map<String, Command> commands,
-    {bool isSubcommand: false}) {
+    {bool isSubcommand: false, int lineLength}) {
   // Don't include aliases.
   var names =
       commands.keys.where((name) => !commands[name].aliases.contains(name));
@@ -400,13 +420,15 @@
   var buffer =
       new StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:');
   for (var name in names) {
-    var lines = commands[name].summary.split("\n");
+    var columnStart = length + 5;
+    var lines = wrapTextAsLines(commands[name].summary,
+        start: columnStart, length: lineLength);
     buffer.writeln();
     buffer.write('  ${padRight(name, length)}   ${lines.first}');
 
     for (var line in lines.skip(1)) {
       buffer.writeln();
-      buffer.write(' ' * (length + 5));
+      buffer.write(' ' * columnStart);
       buffer.write(line);
     }
   }
diff --git a/lib/src/usage.dart b/lib/src/usage.dart
index 8a1e764..01775ff 100644
--- a/lib/src/usage.dart
+++ b/lib/src/usage.dart
@@ -5,6 +5,7 @@
 import 'dart:math' as math;
 
 import '../args.dart';
+import 'utils.dart';
 
 /// Takes an [ArgParser] and generates a string of usage (i.e. help) text for
 /// its defined options.
@@ -177,45 +178,6 @@
     numHelpLines = 0;
   }
 
-  /// Wraps a single line of text into lines no longer than [lineLength],
-  /// starting at the [start] column.
-  ///
-  /// Tries to split at whitespace, but if that's not good enough to keep it
-  /// under the limit, then splits in the middle of a word.
-  List<String> _wrap(String text, int start) {
-    assert(lineLength != null, "Should wrap when given a length.");
-    assert(start >= 0);
-
-    text = text.trim();
-
-    var length = math.max(lineLength - start, 10);
-    if (text.length <= length) return [text];
-
-    var result = <String>[];
-    var currentLineStart = 0;
-    int lastWhitespace;
-    for (var i = 0; i < text.length; ++i) {
-      if (_isWhitespace(text, i)) lastWhitespace = i;
-
-      if (i - currentLineStart >= length) {
-        // Back up to the last whitespace, unless there wasn't any, in which
-        // case we just split where we are.
-        if (lastWhitespace != null) i = lastWhitespace;
-
-        result.add(text.substring(currentLineStart, i));
-
-        // Skip any intervening whitespace.
-        while (_isWhitespace(text, i) && i < text.length) i++;
-
-        currentLineStart = i;
-        lastWhitespace = null;
-      }
-    }
-
-    result.add(text.substring(currentLineStart));
-    return result;
-  }
-
   void write(int column, String text) {
     var lines = text.split('\n');
     // If we are writing the last column, word wrap it to fit.
@@ -226,7 +188,8 @@
           .reduce((start, width) => start += width);
 
       for (var line in lines) {
-        wrappedLines.addAll(_wrap(line, start));
+        wrappedLines
+            .addAll(wrapTextAsLines(line, start: start, length: lineLength));
       }
 
       lines = wrappedLines;
@@ -307,23 +270,3 @@
     return allowedBuffer.toString();
   }
 }
-
-/// Returns true if the code unit at [index] in [text] is a whitespace
-/// character.
-///
-/// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
-bool _isWhitespace(String text, int index) {
-  var rune = text.codeUnitAt(index);
-  return rune >= 0x0009 && rune <= 0x000D ||
-      rune == 0x0020 ||
-      rune == 0x0085 ||
-      rune == 0x1680 ||
-      rune == 0x180E ||
-      rune >= 0x2000 && rune <= 0x200A ||
-      rune == 0x2028 ||
-      rune == 0x2029 ||
-      rune == 0x202F ||
-      rune == 0x205F ||
-      rune == 0x3000 ||
-      rune == 0xFEFF;
-}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 2469fac..9c3972f 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -1,7 +1,147 @@
 // Copyright (c) 2014, 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:math' as math;
 
 /// Pads [source] to [length] by adding spaces at the end.
 String padRight(String source, int length) =>
     source + ' ' * (length - source.length);
+
+/// Wraps a block of text into lines no longer than [length].
+///
+/// Tries to split at whitespace, but if that's not good enough to keep it
+/// under the limit, then it splits in the middle of a word.
+///
+/// Preserves indentation (leading whitespace) for each line (delimited by '\n')
+/// in the input, and will indent wrapped lines the same amount.
+///
+/// If [hangingIndent] is supplied, then that many spaces will be added to each
+/// line, except for the first line. This is useful for flowing text with a
+/// heading prefix (e.g. "Usage: "):
+///
+/// ```dart
+/// var prefix = "Usage: ";
+/// print(prefix + wrapText(invocation, hangingIndent: prefix.length, length: 40));
+/// ```
+///
+/// yields:
+/// ```
+/// Usage: app main_command <subcommand>
+///        [arguments]
+/// ```
+///
+/// If [length] is not specified, then no wrapping occurs, and the original
+/// [text] is returned unchanged.
+String wrapText(String text, {int length, int hangingIndent}) {
+  if (length == null) return text;
+  hangingIndent ??= 0;
+  var splitText = text.split('\n');
+  var result = <String>[];
+  for (var line in splitText) {
+    var trimmedText = line.trimLeft();
+    final leadingWhitespace =
+        line.substring(0, line.length - trimmedText.length);
+    var notIndented;
+    if (hangingIndent != 0) {
+      // When we have a hanging indent, we want to wrap the first line at one
+      // width, and the rest at another (offset by hangingIndent), so we wrap
+      // them twice and recombine.
+      final firstLineWrap = wrapTextAsLines(
+        trimmedText,
+        length: length - leadingWhitespace.length,
+      );
+      notIndented = <String>[firstLineWrap.removeAt(0)];
+      trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
+      if (firstLineWrap.isNotEmpty) {
+        notIndented.addAll(wrapTextAsLines(
+          trimmedText,
+          length: length - leadingWhitespace.length - hangingIndent,
+        ));
+      }
+    } else {
+      notIndented = wrapTextAsLines(
+        trimmedText,
+        length: length - leadingWhitespace.length,
+      );
+    }
+    String hangingIndentString;
+    result.addAll(notIndented.map<String>(
+      (String line) {
+        // Don't return any lines with just whitespace on them.
+        if (line.isEmpty) return "";
+        var result = "${hangingIndentString ?? ""}$leadingWhitespace$line";
+        hangingIndentString ??= " " * hangingIndent;
+        return result;
+      },
+    ));
+  }
+  return result.join('\n');
+}
+
+/// Wraps a block of text into lines no longer than [length],
+/// starting at the [start] column, and returns the result as a list of strings.
+///
+/// Tries to split at whitespace, but if that's not good enough to keep it
+/// under the limit, then splits in the middle of a word. Preserves embedded
+/// newlines, but not indentation (it trims whitespace from each line).
+///
+/// If [length] is not specified, then no wrapping occurs, and the original
+/// [text] is returned after splitting it on newlines. Whitespace is not trimmed
+/// in this case.
+List<String> wrapTextAsLines(String text, {int start = 0, int length}) {
+  assert(start >= 0);
+
+  /// Returns true if the code unit at [index] in [text] is a whitespace
+  /// character.
+  ///
+  /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
+  bool isWhitespace(String text, int index) {
+    final rune = text.codeUnitAt(index);
+    return rune >= 0x0009 && rune <= 0x000D ||
+        rune == 0x0020 ||
+        rune == 0x0085 ||
+        rune == 0x1680 ||
+        rune == 0x180E ||
+        rune >= 0x2000 && rune <= 0x200A ||
+        rune == 0x2028 ||
+        rune == 0x2029 ||
+        rune == 0x202F ||
+        rune == 0x205F ||
+        rune == 0x3000 ||
+        rune == 0xFEFF;
+  }
+
+  if (length == null) return text.split('\n');
+
+  final result = <String>[];
+  final effectiveLength = math.max(length - start, 10);
+  for (var line in text.split('\n')) {
+    line = line.trim();
+    if (line.length <= effectiveLength) {
+      result.add(line);
+      continue;
+    }
+
+    var currentLineStart = 0;
+    var lastWhitespace;
+    for (var i = 0; i < line.length; ++i) {
+      if (isWhitespace(line, i)) lastWhitespace = i;
+
+      if (i - currentLineStart >= effectiveLength) {
+        // Back up to the last whitespace, unless there wasn't any, in which
+        // case we just split where we are.
+        if (lastWhitespace != null) i = lastWhitespace;
+
+        result.add(line.substring(currentLineStart, i).trim());
+
+        // Skip any intervening whitespace.
+        while (isWhitespace(line, i) && i < line.length) i++;
+
+        currentLineStart = i;
+        lastWhitespace = null;
+      }
+    }
+    result.add(line.substring(currentLineStart).trim());
+  }
+  return result;
+}
diff --git a/test/args_test.dart b/test/args_test.dart
index 8741ad8..d738f00 100644
--- a/test/args_test.dart
+++ b/test/args_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:test/test.dart';
 import 'package:args/args.dart';
-import 'utils.dart';
+import 'test_utils.dart';
 
 void main() {
   group('ArgParser.addFlag()', () {
diff --git a/test/command_parse_test.dart b/test/command_parse_test.dart
index 269ed01..a5a1f56 100644
--- a/test/command_parse_test.dart
+++ b/test/command_parse_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:test/test.dart';
 import 'package:args/args.dart';
-import 'utils.dart';
+import 'test_utils.dart';
 
 void main() {
   group('ArgParser.addCommand()', () {
diff --git a/test/command_runner_test.dart b/test/command_runner_test.dart
index ebb2f61..0799927 100644
--- a/test/command_runner_test.dart
+++ b/test/command_runner_test.dart
@@ -5,7 +5,7 @@
 import 'package:args/command_runner.dart';
 import 'package:test/test.dart';
 
-import 'utils.dart';
+import 'test_utils.dart';
 
 const _DEFAULT_USAGE = """
 Usage: test <command> [arguments]
@@ -330,6 +330,59 @@
     });
   });
 
+  group("with a footer and wrapping", () {
+    setUp(() {
+      runner = new CommandRunnerWithFooterAndWrapping(
+          "test", "A test command runner.");
+    });
+    test("includes the footer in the usage string", () {
+      expect(runner.usage, equals("""
+A test command runner.
+
+Usage: test <command> [arguments]
+
+Global options:
+-h, --help    Print this usage
+              information.
+
+Available commands:
+  help   Display help information for
+         test.
+
+Run "test help <command>" for more
+information about a command.
+LONG footer! This is a long footer, so
+we can check wrapping on long footer
+messages.
+
+And make sure that they preserve
+newlines properly."""));
+    });
+
+    test("includes the footer in usage errors", () {
+      expect(runner.run(["--bad"]),
+          throwsUsageException('Could not find an option named "bad".', """
+Usage: test <command> [arguments]
+
+Global options:
+-h, --help    Print this usage
+              information.
+
+Available commands:
+  help   Display help information for
+         test.
+
+Run "test help <command>" for more
+information about a command.
+LONG footer! This is a long footer, so
+we can check wrapping on long footer
+messages.
+
+And make sure that they preserve
+newlines properly."""));
+    });
+  });
+
   group("throws a useful error when", () {
     test("arg parsing fails", () {
       expect(
diff --git a/test/command_test.dart b/test/command_test.dart
index c4f29d4..4c99fa2 100644
--- a/test/command_test.dart
+++ b/test/command_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:args/command_runner.dart';
 import 'package:test/test.dart';
-import 'utils.dart';
+import 'test_utils.dart';
 
 void main() {
   var foo;
@@ -87,6 +87,58 @@
 
 Run "test help" to see global options."""));
     });
+
+    test("wraps long command descriptions with subcommands", () {
+      var wrapping = new WrappingCommand();
+
+      // Make sure [Command.runner] is set up.
+      new CommandRunner("longtest", "A long-lined test command runner.")
+          .addCommand(wrapping);
+
+      wrapping.addSubcommand(new LongCommand());
+      expect(wrapping.usage, equals("""
+This command overrides the argParser so
+that it will wrap long lines.
+
+Usage: longtest wrapping <subcommand>
+       [arguments]
+-h, --help    Print this usage
+              information.
+
+Available subcommands:
+  long   This command has a long
+         description that needs to be
+         wrapped sometimes.
+
+Run "longtest help" to see global
+options."""));
+    });
+
+    test("wraps long command descriptions", () {
+      var longCommand = new LongCommand();
+
+      // Make sure [Command.runner] is set up.
+      new CommandRunner("longtest", "A long-lined test command runner.")
+          .addCommand(longCommand);
+
+      expect(longCommand.usage, equals("""
+This command has a long description that
+needs to be wrapped sometimes.
+It has embedded newlines,
+     and indented lines that also need
+     to be wrapped and have their
+     indentation preserved.
+0123456789012345678901234567890123456789
+0123456789012345678901234567890123456789
+01234567890123456789
+
+Usage: longtest long [arguments]
+-h, --help    Print this usage
+              information.
+
+Run "longtest help" to see global
+options."""));
+    });
   });
 
   test("usageException splits up the message and usage", () {
diff --git a/test/parse_test.dart b/test/parse_test.dart
index d212cf0..3fa2065 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:test/test.dart';
 import 'package:args/args.dart';
-import 'utils.dart';
+import 'test_utils.dart';
 
 void main() {
   group('ArgParser.parse()', () {
diff --git a/test/utils.dart b/test/test_utils.dart
similarity index 66%
rename from test/utils.dart
rename to test/test_utils.dart
index d9e4f70..99a6ba0 100644
--- a/test/utils.dart
+++ b/test/test_utils.dart
@@ -16,6 +16,20 @@
       : super(executableName, description);
 }
 
+class CommandRunnerWithFooterAndWrapping extends CommandRunner {
+  @override
+  String get usageFooter => "LONG footer! "
+      "This is a long footer, so we can check wrapping on long footer messages.\n\n"
+      "And make sure that they preserve newlines properly.";
+
+  @override
+  ArgParser get argParser => _argParser;
+  final _argParser = new ArgParser(usageLineLength: 40);
+
+  CommandRunnerWithFooterAndWrapping(String executableName, String description)
+      : super(executableName, description);
+}
+
 class FooCommand extends Command {
   var hasRun = false;
 
@@ -80,6 +94,55 @@
   }
 }
 
+class WrappingCommand extends Command {
+  var hasRun = false;
+
+  @override
+  ArgParser get argParser => _argParser;
+  final _argParser = new ArgParser(usageLineLength: 40);
+
+  @override
+  final name = "wrapping";
+
+  @override
+  final description =
+      "This command overrides the argParser so that it will wrap long lines.";
+
+  @override
+  final takesArguments = false;
+
+  @override
+  void run() {
+    hasRun = true;
+  }
+}
+
+class LongCommand extends Command {
+  var hasRun = false;
+
+  @override
+  ArgParser get argParser => _argParser;
+  final _argParser = new ArgParser(usageLineLength: 40);
+
+  @override
+  final name = "long";
+
+  @override
+  final description = "This command has a long description that needs to be "
+      "wrapped sometimes.\nIt has embedded newlines,\n"
+      "     and indented lines that also need to be wrapped and have their "
+      "indentation preserved.\n" +
+      ("0123456789" * 10);
+
+  @override
+  final takesArguments = false;
+
+  @override
+  void run() {
+    hasRun = true;
+  }
+}
+
 class MultilineSummaryCommand extends MultilineCommand {
   @override
   String get summary => description;
diff --git a/test/usage_test.dart b/test/usage_test.dart
index c556b6d..11b0f34 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -335,25 +335,37 @@
       parser.addFlag('solid',
           help:
               'The-flag-with-no-whitespace-that-will-be-wrapped-by-splitting-a-word.');
+      parser.addFlag('longWhitespace',
+          help:
+              '           The flag with a really long help text and whitespace at the start.');
+      parser.addFlag('longTrailspace',
+          help:
+              'The flag with a really long help text and whitespace at the end.             ');
       parser.addFlag('small1', help: ' a ');
       parser.addFlag('small2', help: ' a');
       parser.addFlag('small3', help: 'a ');
       validateUsage(parser, '''
-          --[no-]long           The flag with a really long help text
-                                that will be wrapped.
-
-          --[no-]longNewline    The flag with a really long help text
-                                and newlines
-                                
-                                that will still be wrapped because it
-                                is really long.
-
-          --[no-]solid          The-flag-with-no-whitespace-that-will-
-                                be-wrapped-by-splitting-a-word.
-
-          --[no-]small1         a
-          --[no-]small2         a
-          --[no-]small3         a
+          --[no-]long              The flag with a really long help
+                                   text that will be wrapped.
+          
+          --[no-]longNewline       The flag with a really long help
+                                   text and newlines
+                                   
+                                   that will still be wrapped because
+                                   it is really long.
+          
+          --[no-]solid             The-flag-with-no-whitespace-that-wi
+                                   ll-be-wrapped-by-splitting-a-word.
+          
+          --[no-]longWhitespace    The flag with a really long help
+                                   text and whitespace at the start.
+          
+          --[no-]longTrailspace    The flag with a really long help
+                                   text and whitespace at the end.
+          
+          --[no-]small1            a
+          --[no-]small2            a
+          --[no-]small3            a
           ''');
     });
 
@@ -370,67 +382,43 @@
       parser.addFlag('solid',
           help:
               'The-flag-with-no-whitespace-that-will-be-wrapped-by-splitting-a-word.');
-      parser.addFlag('longWhitespace',
-          help:
-              '           The flag with a really long help text and whitespace at the start.');
-      parser.addFlag('longTrailspace',
-          help:
-              'The flag with a really long help text and whitespace at the end.             ');
       parser.addFlag('small1', help: ' a ');
       parser.addFlag('small2', help: ' a');
       parser.addFlag('small3', help: 'a ');
       validateUsage(parser, '''
-          --[no-]long              The flag
-                                   with a
-                                   really
-                                   long help
-                                   text that
-                                   will be
-                                   wrapped.
+          --[no-]long           The flag
+                                with a
+                                really
+                                long help
+                                text that
+                                will be
+                                wrapped.
           
-          --[no-]longNewline       The flag
-                                   with a
-                                   really
-                                   long help
-                                   text and
-                                   newlines
-                                   
-                                   that will
-                                   still be
-                                   wrapped
-                                   because it
-                                   is really
-                                   long.
+          --[no-]longNewline    The flag
+                                with a
+                                really
+                                long help
+                                text and
+                                newlines
+                                
+                                that will
+                                still be
+                                wrapped
+                                because it
+                                is really
+                                long.
           
-          --[no-]solid             The-flag-w
-                                   ith-no-whi
-                                   tespace-th
-                                   at-will-be
-                                   -wrapped-b
-                                   y-splittin
-                                   g-a-word.
+          --[no-]solid          The-flag-w
+                                ith-no-whi
+                                tespace-th
+                                at-will-be
+                                -wrapped-b
+                                y-splittin
+                                g-a-word.
           
-          --[no-]longWhitespace    The flag
-                                   with a
-                                   really
-                                   long help
-                                   text and
-                                   whitespace
-                                   at the
-                                   start.
-          
-          --[no-]longTrailspace    The flag
-                                   with a
-                                   really
-                                   long help
-                                   text and
-                                   whitespace
-                                   at the
-                                   end.
-          
-          --[no-]small1            a
-          --[no-]small2            a
-          --[no-]small3            a
+          --[no-]small1         a
+          --[no-]small2         a
+          --[no-]small3         a
           ''');
     });
 
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..7c6463a
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,217 @@
+// 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:test/test.dart';
+
+import '../lib/src/utils.dart';
+
+const _lineLength = 40;
+const _longLine = "This is a long line that needs to be wrapped.";
+final _longLineWithNewlines = "This is a long line with newlines that\n"
+    "needs to be wrapped.\n\n" +
+    "0123456789" * 5;
+final _indentedLongLineWithNewlines =
+    "    This is an indented long line with newlines that\n"
+        "needs to be wrapped.\n\tAnd preserves tabs.\n      \n  " +
+        "0123456789" * 5;
+const _shortLine = "Short line.";
+const _indentedLongLine = "    This is an indented long line that needs to be "
+    "wrapped and indentation preserved.";
+
+void main() {
+  group("padding", () {
+    test("can pad on the right.", () {
+      expect(padRight("foo", 6), equals("foo   "));
+    });
+  });
+  group("text wrapping", () {
+    test("doesn't wrap short lines.", () {
+      expect(wrapText(_shortLine, length: _lineLength), equals(_shortLine));
+    });
+    test("doesn't wrap at all if not given a length", () {
+      expect(wrapText(_longLine), equals(_longLine));
+    });
+    test("able to wrap long lines", () {
+      expect(wrapText(_longLine, length: _lineLength), equals("""
+This is a long line that needs to be
+wrapped."""));
+    });
+    test("wrap long lines with no whitespace", () {
+      expect(wrapText("0123456789" * 5, length: _lineLength), equals("""
+0123456789012345678901234567890123456789
+0123456789"""));
+    });
+    test("refuses to wrap to a column smaller than 10 characters", () {
+      expect(wrapText("$_longLine " + "0123456789" * 4, length: 1), equals("""
+This is a
+long line
+that needs
+to be
+wrapped.
+0123456789
+0123456789
+0123456789
+0123456789"""));
+    });
+    test("preserves indentation", () {
+      expect(wrapText(_indentedLongLine, length: _lineLength), equals("""
+    This is an indented long line that
+    needs to be wrapped and indentation
+    preserved."""));
+    });
+    test("preserves indentation and stripping trailing whitespace", () {
+      expect(wrapText("$_indentedLongLine   ", length: _lineLength), equals("""
+    This is an indented long line that
+    needs to be wrapped and indentation
+    preserved."""));
+    });
+    test("wraps text with newlines", () {
+      expect(wrapText(_longLineWithNewlines, length: _lineLength), equals("""
+This is a long line with newlines that
+needs to be wrapped.
+
+0123456789012345678901234567890123456789
+0123456789"""));
+    });
+    test("preserves indentation in the presence of newlines", () {
+      expect(wrapText(_indentedLongLineWithNewlines, length: _lineLength),
+          equals("""
+    This is an indented long line with
+    newlines that
+needs to be wrapped.
+\tAnd preserves tabs.
+
+  01234567890123456789012345678901234567
+  890123456789"""));
+    });
+    test("removes trailing whitespace when wrapping", () {
+      expect(wrapText("$_longLine     \t", length: _lineLength), equals("""
+This is a long line that needs to be
+wrapped."""));
+    });
+    test("preserves trailing whitespace when not wrapping", () {
+      expect(wrapText("$_longLine     \t"), equals("$_longLine     \t"));
+    });
+    test("honors hangingIndent parameter", () {
+      expect(
+          wrapText(_longLine, length: _lineLength, hangingIndent: 6), equals("""
+This is a long line that needs to be
+      wrapped."""));
+    });
+    test("handles hangingIndent with a single unwrapped line.", () {
+      expect(wrapText(_shortLine, length: _lineLength, hangingIndent: 6),
+          equals("""
+Short line."""));
+    });
+    test(
+        "handles hangingIndent with two unwrapped lines and the second is empty.",
+        () {
+      expect(wrapText("$_shortLine\n", length: _lineLength, hangingIndent: 6),
+          equals("""
+Short line.
+"""));
+    });
+    test("honors hangingIndent parameter on already indented line.", () {
+      expect(wrapText(_indentedLongLine, length: _lineLength, hangingIndent: 6),
+          equals("""
+    This is an indented long line that
+          needs to be wrapped and
+          indentation preserved."""));
+    });
+    test("honors hangingIndent parameter on already indented line.", () {
+      expect(
+          wrapText(_indentedLongLineWithNewlines,
+              length: _lineLength, hangingIndent: 6),
+          equals("""
+    This is an indented long line with
+          newlines that
+needs to be wrapped.
+	And preserves tabs.
+
+  01234567890123456789012345678901234567
+        890123456789"""));
+    });
+  });
+  group("text wrapping as lines", () {
+    test("doesn't wrap short lines.", () {
+      expect(wrapTextAsLines(_shortLine, length: _lineLength),
+          equals([_shortLine]));
+    });
+    test("doesn't wrap at all if not given a length", () {
+      expect(wrapTextAsLines(_longLine), equals([_longLine]));
+    });
+    test("able to wrap long lines", () {
+      expect(wrapTextAsLines(_longLine, length: _lineLength),
+          equals(["This is a long line that needs to be", "wrapped."]));
+    });
+    test("wrap long lines with no whitespace", () {
+      expect(wrapTextAsLines("0123456789" * 5, length: _lineLength),
+          equals(["0123456789012345678901234567890123456789", "0123456789"]));
+    });
+
+    test("refuses to wrap to a column smaller than 10 characters", () {
+      expect(
+          wrapTextAsLines("$_longLine " + "0123456789" * 4, length: 1),
+          equals([
+            "This is a",
+            "long line",
+            "that needs",
+            "to be",
+            "wrapped.",
+            "0123456789",
+            "0123456789",
+            "0123456789",
+            "0123456789"
+          ]));
+    });
+    test("doesn't preserve indentation", () {
+      expect(
+          wrapTextAsLines(_indentedLongLine, length: _lineLength),
+          equals([
+            "This is an indented long line that needs",
+            "to be wrapped and indentation preserved."
+          ]));
+    });
+    test("strips trailing whitespace", () {
+      expect(
+          wrapTextAsLines("$_indentedLongLine   ", length: _lineLength),
+          equals([
+            "This is an indented long line that needs",
+            "to be wrapped and indentation preserved."
+          ]));
+    });
+    test("splits text with newlines properly", () {
+      expect(
+          wrapTextAsLines(_longLineWithNewlines, length: _lineLength),
+          equals([
+            "This is a long line with newlines that",
+            "needs to be wrapped.",
+            "",
+            "0123456789012345678901234567890123456789",
+            "0123456789"
+          ]));
+    });
+    test("does not preserves indentation in the presence of newlines", () {
+      expect(
+          wrapTextAsLines(_indentedLongLineWithNewlines, length: _lineLength),
+          equals([
+            "This is an indented long line with",
+            "newlines that",
+            "needs to be wrapped.",
+            "And preserves tabs.",
+            "",
+            "0123456789012345678901234567890123456789",
+            "0123456789"
+          ]));
+    });
+    test("removes trailing whitespace when wrapping", () {
+      expect(wrapTextAsLines("$_longLine     \t", length: _lineLength),
+          equals(["This is a long line that needs to be", "wrapped."]));
+    });
+    test("preserves trailing whitespace when not wrapping", () {
+      expect(
+          wrapTextAsLines("$_longLine     \t"), equals(["$_longLine     \t"]));
+    });
+  });
+}