Re-land text wrapping/color PR (#22831)

This attempts to re-land #22656.

There are two changes from the original:

I turned off wrapping completely when not sending output to a terminal. Previously I had defaulted to wrapping at and arbitrary 100 chars in that case, just to keep long messages from being too long, but that turns out the be a bad idea because there are tests that are relying on the specific form of the output. It's also pretty arbitrary, and mostly people sending output to a non-terminal will want unwrapped text.

I found a better way to terminate ANSI color/bold sequences, so that they can be embedded within each other without needed quite as complex a dance with removing redundant sequences.

As part of these changes, I removed the Logger.supportsColor setter so that the one source of truth for color support is in AnsiTerminal.supportsColor.

*     Turn on line wrapping again in usage and status messages, adds ANSI color to doctor and analysis messages. (#22656)

    This turns on text wrapping for usage messages and status messages. When on a terminal, wraps to the width of the terminal. When writing to a non-terminal, wrap lines at a default column width (currently defined to be 100 chars). If --no-wrap is specified, then no wrapping occurs. If --wrap-column is specified, wraps to that column (if --wrap is on).

    Adds ANSI color to the doctor and analysis output on terminals. This is in this PR with the wrapping, since wrapping needs to know how to count visible characters in the presence of ANSI sequences. (This is just one more step towards re-implementing all of Curses for Flutter. :-)) Will not print ANSI sequences when sent to a non-terminal, or of --no-color is specified.

    Fixes ANSI color and bold sequences so that they can be combined (bold, colored text), and a small bug in indentation calculation for wrapping.

    Since wrapping is now turned on, also removed many redundant '\n's in the code.
diff --git a/packages/flutter_tools/lib/src/android/android_studio_validator.dart b/packages/flutter_tools/lib/src/android/android_studio_validator.dart
index b12d338..bf12cfb 100644
--- a/packages/flutter_tools/lib/src/android/android_studio_validator.dart
+++ b/packages/flutter_tools/lib/src/android/android_studio_validator.dart
@@ -80,7 +80,7 @@
         'Android Studio not found; download from https://developer.android.com/studio/index.html\n'
         '(or visit https://flutter.io/setup/#android-setup for detailed instructions).'));
 
-    return ValidationResult(ValidationType.missing, messages,
+    return ValidationResult(ValidationType.notAvailable, messages,
         statusInfo: 'not installed');
   }
 }
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index dd6f0a1..4187472 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert' show LineSplitter;
 
 import 'package:meta/meta.dart';
 
@@ -22,32 +21,60 @@
   bool quiet = false;
 
   bool get supportsColor => terminal.supportsColor;
-  set supportsColor(bool value) {
-    terminal.supportsColor = value;
-  }
 
   bool get hasTerminal => stdio.hasTerminal;
 
-  /// Display an error level message to the user. Commands should use this if they
+  /// Display an error [message] to the user. Commands should use this if they
   /// fail in some way.
+  ///
+  /// The [message] argument is printed to the stderr in red by default.
+  /// The [stackTrace] argument is the stack trace that will be printed if
+  /// supplied.
+  /// The [emphasis] argument will cause the output message be printed in bold text.
+  /// The [color] argument will print the message in the supplied color instead
+  /// of the default of red. Colors will not be printed if the output terminal
+  /// doesn't support them.
+  /// The [indent] argument specifies the number of spaces to indent the overall
+  /// message. If wrapping is enabled in [outputPreferences], then the wrapped
+  /// lines will be indented as well.
+  /// If [hangingIndent] is specified, then any wrapped lines will be indented
+  /// by this much more than the first line, if wrapping is enabled in
+  /// [outputPreferences].
   void printError(
     String message, {
     StackTrace stackTrace,
     bool emphasis,
     TerminalColor color,
+    int indent,
+    int hangingIndent,
   });
 
   /// Display normal output of the command. This should be used for things like
   /// progress messages, success messages, or just normal command output.
   ///
-  /// If [newline] is null, then it defaults to "true".  If [emphasis] is null,
-  /// then it defaults to "false".
+  /// The [message] argument is printed to the stderr in red by default.
+  /// The [stackTrace] argument is the stack trace that will be printed if
+  /// supplied.
+  /// If the [emphasis] argument is true, it will cause the output message be
+  /// printed in bold text. Defaults to false.
+  /// The [color] argument will print the message in the supplied color instead
+  /// of the default of red. Colors will not be printed if the output terminal
+  /// doesn't support them.
+  /// If [newline] is true, then a newline will be added after printing the
+  /// status. Defaults to true.
+  /// The [indent] argument specifies the number of spaces to indent the overall
+  /// message. If wrapping is enabled in [outputPreferences], then the wrapped
+  /// lines will be indented as well.
+  /// If [hangingIndent] is specified, then any wrapped lines will be indented
+  /// by this much more than the first line, if wrapping is enabled in
+  /// [outputPreferences].
   void printStatus(
     String message, {
     bool emphasis,
     TerminalColor color,
     bool newline,
     int indent,
+    int hangingIndent,
   });
 
   /// Use this for verbose tracing output. Users can turn this output on in order
@@ -82,8 +109,11 @@
     StackTrace stackTrace,
     bool emphasis,
     TerminalColor color,
+    int indent,
+    int hangingIndent,
   }) {
     message ??= '';
+    message = wrapText(message, indent: indent, hangingIndent: hangingIndent);
     _status?.cancel();
     _status = null;
     if (emphasis == true)
@@ -102,19 +132,16 @@
     TerminalColor color,
     bool newline,
     int indent,
+    int hangingIndent,
   }) {
     message ??= '';
+    message = wrapText(message, indent: indent, hangingIndent: hangingIndent);
     _status?.cancel();
     _status = null;
     if (emphasis == true)
       message = terminal.bolden(message);
     if (color != null)
       message = terminal.color(message, color);
-    if (indent != null && indent > 0) {
-      message = LineSplitter.split(message)
-          .map<String>((String line) => ' ' * indent + line)
-          .join('\n');
-    }
     if (newline != false)
       message = '$message\n';
     writeToStdOut(message);
@@ -203,8 +230,13 @@
     StackTrace stackTrace,
     bool emphasis,
     TerminalColor color,
+    int indent,
+    int hangingIndent,
   }) {
-    _error.writeln(terminal.color(message, color ?? TerminalColor.red));
+    _error.writeln(terminal.color(
+      wrapText(message, indent: indent, hangingIndent: hangingIndent),
+      color ?? TerminalColor.red,
+    ));
   }
 
   @override
@@ -214,11 +246,12 @@
     TerminalColor color,
     bool newline,
     int indent,
+    int hangingIndent,
   }) {
     if (newline != false)
-      _status.writeln(message);
+      _status.writeln(wrapText(message, indent: indent, hangingIndent: hangingIndent));
     else
-      _status.write(message);
+      _status.write(wrapText(message, indent: indent, hangingIndent: hangingIndent));
   }
 
   @override
@@ -262,8 +295,14 @@
     StackTrace stackTrace,
     bool emphasis,
     TerminalColor color,
+    int indent,
+    int hangingIndent,
   }) {
-    _emit(_LogType.error, message, stackTrace);
+    _emit(
+      _LogType.error,
+      wrapText(message, indent: indent, hangingIndent: hangingIndent),
+      stackTrace,
+    );
   }
 
   @override
@@ -273,8 +312,9 @@
     TerminalColor color,
     bool newline,
     int indent,
+    int hangingIndent,
   }) {
-    _emit(_LogType.status, message);
+    _emit(_LogType.status, wrapText(message, indent: indent, hangingIndent: hangingIndent));
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart
index 65a5a46..bd7e349 100644
--- a/packages/flutter_tools/lib/src/base/terminal.dart
+++ b/packages/flutter_tools/lib/src/base/terminal.dart
@@ -11,6 +11,7 @@
 import 'context.dart';
 import 'io.dart' as io;
 import 'platform.dart';
+import 'utils.dart';
 
 final AnsiTerminal _kAnsiTerminal = AnsiTerminal();
 
@@ -30,9 +31,58 @@
   grey,
 }
 
+final OutputPreferences _kOutputPreferences = OutputPreferences();
+
+OutputPreferences get outputPreferences => (context == null || context[OutputPreferences] == null)
+    ? _kOutputPreferences
+    : context[OutputPreferences];
+
+/// A class that contains the context settings for command text output to the
+/// console.
+class OutputPreferences {
+  OutputPreferences({
+    bool wrapText,
+    int wrapColumn,
+    bool showColor,
+  })  : wrapText = wrapText ?? io.stdio?.hasTerminal ?? const io.Stdio().hasTerminal,
+        wrapColumn = wrapColumn ?? io.stdio?.terminalColumns ?? const io.Stdio().terminalColumns ?? kDefaultTerminalColumns,
+        showColor = showColor ?? platform.stdoutSupportsAnsi ?? false;
+
+  /// If [wrapText] is true, then any text sent to the context's [Logger]
+  /// instance (e.g. from the [printError] or [printStatus] functions) will be
+  /// wrapped (newlines added between words) to be no longer than the
+  /// [wrapColumn] specifies. Defaults to true if there is a terminal. To
+  /// determine if there's a terminal, [OutputPreferences] asks the context's
+  /// stdio to see, and if that's not set, it tries creating a new [io.Stdio]
+  /// and asks it if there is a terminal.
+  final bool wrapText;
+
+  /// The column at which output sent to the context's [Logger] instance
+  /// (e.g. from the [printError] or [printStatus] functions) will be wrapped.
+  /// Ignored if [wrapText] is false. Defaults to the width of the output
+  /// terminal, or to [kDefaultTerminalColumns] if not writing to a terminal.
+  /// To find out if we're writing to a terminal, it tries the context's stdio,
+  /// and if that's not set, it tries creating a new [io.Stdio] and asks it, if
+  /// that doesn't have an idea of the terminal width, then we just use a
+  /// default of 100. It will be ignored if wrapText is false.
+  final int wrapColumn;
+
+  /// Whether or not to output ANSI color codes when writing to the output
+  /// terminal. Defaults to whatever [platform.stdoutSupportsAnsi] says if
+  /// writing to a terminal, and false otherwise.
+  final bool showColor;
+
+  @override
+  String toString() {
+    return '$runtimeType[wrapText: $wrapText, wrapColumn: $wrapColumn, showColor: $showColor]';
+  }
+}
+
 class AnsiTerminal {
   static const String bold = '\u001B[1m';
-  static const String reset = '\u001B[0m';
+  static const String resetAll = '\u001B[0m';
+  static const String resetColor = '\u001B[39m';
+  static const String resetBold = '\u001B[22m';
   static const String clear = '\u001B[2J\u001B[H';
 
   static const String red = '\u001b[31m';
@@ -55,15 +105,21 @@
 
   static String colorCode(TerminalColor color) => _colorMap[color];
 
-  bool supportsColor = platform.stdoutSupportsAnsi ?? false;
+  bool get supportsColor => platform.stdoutSupportsAnsi ?? false;
+  final RegExp _boldControls = RegExp('(${RegExp.escape(resetBold)}|${RegExp.escape(bold)})');
 
   String bolden(String message) {
     assert(message != null);
     if (!supportsColor || message.isEmpty)
       return message;
     final StringBuffer buffer = StringBuffer();
-    for (String line in message.split('\n'))
-      buffer.writeln('$bold$line$reset');
+    for (String line in message.split('\n')) {
+      // If there were bolds or resetBolds in the string before, then nuke them:
+      // they're redundant. This prevents previously embedded resets from
+      // stopping the boldness.
+      line = line.replaceAll(_boldControls, '');
+      buffer.writeln('$bold$line$resetBold');
+    }
     final String result = buffer.toString();
     // avoid introducing a new newline to the emboldened text
     return (!message.endsWith('\n') && result.endsWith('\n'))
@@ -76,8 +132,14 @@
     if (!supportsColor || color == null || message.isEmpty)
       return message;
     final StringBuffer buffer = StringBuffer();
-    for (String line in message.split('\n'))
-      buffer.writeln('${_colorMap[color]}$line$reset');
+    final String colorCodes = _colorMap[color];
+    for (String line in message.split('\n')) {
+      // If there were resets in the string before, then keep them, but
+      // restart the color right after. This prevents embedded resets from
+      // stopping the colors, and allows nesting of colors.
+      line = line.replaceAll(resetColor, '$resetColor$colorCodes');
+      buffer.writeln('$colorCodes$line$resetColor');
+    }
     final String result = buffer.toString();
     // avoid introducing a new newline to the colored text
     return (!message.endsWith('\n') && result.endsWith('\n'))
@@ -111,13 +173,14 @@
     return _broadcastStdInString;
   }
 
-  /// Prompts the user to input a character within the accepted list.
-  /// Reprompts if inputted character is not in the list.
+  /// Prompts the user to input a character within the accepted list. Re-prompts
+  /// if entered character is not in the list.
   ///
-  /// `prompt` is the text displayed prior to waiting for user input each time.
-  /// `defaultChoiceIndex`, if given, will be the character in `acceptedCharacters`
-  ///     in the index given if the user presses enter without any key input.
-  /// `displayAcceptedCharacters` prints also the accepted keys next to the `prompt` if true.
+  /// The [prompt] is the text displayed prior to waiting for user input. The
+  /// [defaultChoiceIndex], if given, will be the character appearing in
+  /// [acceptedCharacters] in the index given if the user presses enter without
+  /// any key input. Setting [displayAcceptedCharacters] also prints the
+  /// accepted keys next to the [prompt].
   ///
   /// Throws a [TimeoutException] if a `timeout` is provided and its duration
   /// expired without user input. Duration resets per key press.
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index e74fd49..a7fe6fa 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:math' show Random;
+import 'dart:math' show Random, max;
 
 import 'package:crypto/crypto.dart';
 import 'package:intl/intl.dart';
@@ -13,7 +13,9 @@
 import '../globals.dart';
 import 'context.dart';
 import 'file_system.dart';
+import 'io.dart' as io;
 import 'platform.dart';
+import 'terminal.dart';
 
 const BotDetector _kBotDetector = BotDetector();
 
@@ -300,3 +302,229 @@
 Future<List<T>> waitGroup<T>(Iterable<Future<T>> futures) {
   return Future.wait<T>(futures.where((Future<T> future) => future != null));
 }
+/// The terminal width used by the [wrapText] function if there is no terminal
+/// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified.
+const int kDefaultTerminalColumns = 100;
+
+/// Smallest column that will be used for text wrapping. If the requested column
+/// width is smaller than this, then this is what will be used.
+const int kMinColumnWidth = 10;
+
+/// Wraps a block of text into lines no longer than [columnWidth].
+///
+/// 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 that same amount, adding
+/// [indent] spaces in addition to any existing indent.
+///
+/// If [hangingIndent] is supplied, then that many additional spaces will be
+/// added to each line, except for the first line. The [hangingIndent] is added
+/// to the specified [indent], if any. This is useful for wrapping
+/// text with a heading prefix (e.g. "Usage: "):
+///
+/// ```dart
+/// String prefix = "Usage: ";
+/// print(prefix + wrapText(invocation, indent: 2, hangingIndent: prefix.length, columnWidth: 40));
+/// ```
+///
+/// yields:
+/// ```
+///   Usage: app main_command <subcommand>
+///          [arguments]
+/// ```
+///
+/// If [columnWidth] is not specified, then the column width will be the
+/// [outputPreferences.wrapColumn], which is set with the --wrap-column option.
+///
+/// If [outputPreferences.wrapText] is false, then the text will be returned
+/// unchanged.
+///
+/// The [indent] and [hangingIndent] must be smaller than [columnWidth] when
+/// added together.
+String wrapText(String text, {int columnWidth, int hangingIndent, int indent}) {
+  if (text == null || text.isEmpty) {
+    return '';
+  }
+  indent ??= 0;
+  columnWidth ??= outputPreferences.wrapColumn;
+  columnWidth -= indent;
+  assert(columnWidth >= 0);
+
+  hangingIndent ??= 0;
+  final List<String> splitText = text.split('\n');
+  final List<String> result = <String>[];
+  for (String line in splitText) {
+    String trimmedText = line.trimLeft();
+    final String leadingWhitespace = line.substring(0, line.length - trimmedText.length);
+    List<String> 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 List<String> firstLineWrap = _wrapTextAsLines(
+        trimmedText,
+        columnWidth: columnWidth - leadingWhitespace.length,
+      );
+      notIndented = <String>[firstLineWrap.removeAt(0)];
+      trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
+      if (firstLineWrap.isNotEmpty) {
+        notIndented.addAll(_wrapTextAsLines(
+          trimmedText,
+          columnWidth: columnWidth - leadingWhitespace.length - hangingIndent,
+        ));
+      }
+    } else {
+      notIndented = _wrapTextAsLines(
+        trimmedText,
+        columnWidth: columnWidth - leadingWhitespace.length,
+      );
+    }
+    String hangingIndentString;
+    final String indentString = ' ' * indent;
+    result.addAll(notIndented.map(
+      (String line) {
+        // Don't return any lines with just whitespace on them.
+        if (line.isEmpty) {
+          return '';
+        }
+        final String result = '$indentString${hangingIndentString ?? ''}$leadingWhitespace$line';
+        hangingIndentString ??= ' ' * hangingIndent;
+        return result;
+      },
+    ));
+  }
+  return result.join('\n');
+}
+
+// Used to represent a run of ANSI control sequences next to a visible
+// character.
+class _AnsiRun {
+  _AnsiRun(this.original, this.character);
+
+  String original;
+  String character;
+}
+
+/// Wraps a block of text into lines no longer than [columnWidth], starting at the
+/// [start] column, and returning 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 [columnWidth] is not specified, then the column width will be the width of the
+/// terminal window by default. If the stdout is not a terminal window, then the
+/// default will be [outputPreferences.wrapColumn].
+///
+/// If [outputPreferences.wrapText] is false, then the text will be returned
+/// simply split at the newlines, but not wrapped.
+List<String> _wrapTextAsLines(String text, {int start = 0, int columnWidth}) {
+  if (text == null || text.isEmpty) {
+    return <String>[''];
+  }
+  assert(columnWidth != null);
+  assert(columnWidth >= 0);
+  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(_AnsiRun run) {
+    final int rune = run.character.isNotEmpty ? run.character.codeUnitAt(0) : 0x0;
+    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;
+  }
+
+  // Splits a string so that the resulting list has the same number of elements
+  // as there are visible characters in the string, but elements may include one
+  // or more adjacent ANSI sequences. Joining the list elements again will
+  // reconstitute the original string. This is useful for manipulating "visible"
+  // characters in the presence of ANSI control codes.
+  List<_AnsiRun> splitWithCodes(String input) {
+    final RegExp characterOrCode = RegExp('(\u001b\[[0-9;]*m|.)', multiLine: true);
+    List<_AnsiRun> result = <_AnsiRun>[];
+    final StringBuffer current = StringBuffer();
+    for (Match match in characterOrCode.allMatches(input)) {
+      current.write(match[0]);
+      if (match[0].length < 4) {
+        // This is a regular character, write it out.
+        result.add(_AnsiRun(current.toString(), match[0]));
+        current.clear();
+      }
+    }
+    // If there's something accumulated, then it must be an ANSI sequence, so
+    // add it to the end of the last entry so that we don't lose it.
+    if (current.isNotEmpty) {
+      if (result.isNotEmpty) {
+        result.last.original += current.toString();
+      } else {
+        // If there is nothing in the string besides control codes, then just
+        // return them as the only entry.
+        result = <_AnsiRun>[_AnsiRun(current.toString(), '')];
+      }
+    }
+    return result;
+  }
+
+  String joinRun(List<_AnsiRun> list, int start, [int end]) {
+    return list.sublist(start, end).map<String>((_AnsiRun run) => run.original).join().trim();
+  }
+
+  final List<String> result = <String>[];
+  final int effectiveLength = max(columnWidth - start, kMinColumnWidth);
+  for (String line in text.split('\n')) {
+    // If the line is short enough, even with ANSI codes, then we can just add
+    // add it and move on.
+    if (line.length <= effectiveLength || !outputPreferences.wrapText) {
+      result.add(line);
+      continue;
+    }
+    final List<_AnsiRun> splitLine = splitWithCodes(line);
+    if (splitLine.length <= effectiveLength) {
+      result.add(line);
+      continue;
+    }
+
+    int currentLineStart = 0;
+    int lastWhitespace;
+    // Find the start of the current line.
+    for (int index = 0; index < splitLine.length; ++index) {
+      if (splitLine[index].character.isNotEmpty && isWhitespace(splitLine[index])) {
+        lastWhitespace = index;
+      }
+
+      if (index - 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) {
+          index = lastWhitespace;
+        }
+
+        result.add(joinRun(splitLine, currentLineStart, index));
+
+        // Skip any intervening whitespace.
+        while (isWhitespace(splitLine[index]) && index < splitLine.length) {
+          index++;
+        }
+
+        currentLineStart = index;
+        lastWhitespace = null;
+      }
+    }
+    result.add(joinRun(splitLine, currentLineStart));
+  }
+  return result;
+}
diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart
index ddb18d3..5cbc68b 100644
--- a/packages/flutter_tools/lib/src/commands/analyze.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze.dart
@@ -20,7 +20,7 @@
         help: 'Analyze the current project, if applicable.', defaultsTo: true);
     argParser.addFlag('dartdocs',
         negatable: false,
-        help: 'List every public member that is lacking documentation.\n'
+        help: 'List every public member that is lacking documentation. '
               '(The public_member_api_docs lint must be enabled in analysis_options.yaml)',
         hide: !verboseHelp);
     argParser.addFlag('watch',
@@ -45,7 +45,7 @@
 
     // Not used by analyze --watch
     argParser.addFlag('congratulate',
-        help: 'Show output even when there are no errors, warnings, hints, or lints.\n'
+        help: 'Show output even when there are no errors, warnings, hints, or lints. '
               'Ignored if --watch is specified.',
         defaultsTo: true);
     argParser.addFlag('preamble',
diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart
index d7a13d0..faadddc 100644
--- a/packages/flutter_tools/lib/src/commands/analyze_once.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart
@@ -132,7 +132,7 @@
       printStatus('');
     errors.sort();
     for (AnalysisError error in errors)
-      printStatus(error.toString());
+      printStatus(error.toString(), hangingIndent: 7);
 
     final String seconds = (timer.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
 
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 35e44ac..c22cc0f 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -50,7 +50,7 @@
       )..addFlag('machine',
           hide: !verboseHelp,
           negatable: false,
-          help: 'Handle machine structured JSON command input and provide output\n'
+          help: 'Handle machine structured JSON command input and provide output '
                 'and progress in machine friendly format.',
       );
     hotRunnerFactory ??= HotRunnerFactory();
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index cc2b4b6..15474ce 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -34,8 +34,8 @@
 
   @override
   final String description = 'Build an Android APK file from your app.\n\n'
-    'This command can build debug and release versions of your application. \'debug\' builds support\n'
-    'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are\n'
+    'This command can build debug and release versions of your application. \'debug\' builds support '
+    'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are '
     'suitable for deploying to app stores.';
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index c747fd3..da21e2f 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -34,19 +34,19 @@
       )
       ..addOption('precompile',
         hide: !verboseHelp,
-        help: 'Precompile functions specified in input file. This flag is only\n'
-              'allowed when using --dynamic. It takes a Dart compilation trace\n'
-              'file produced by the training run of the application. With this\n'
-              'flag, instead of using default Dart VM snapshot provided by the\n'
-              'engine, the application will use its own snapshot that includes\n'
+        help: 'Precompile functions specified in input file. This flag is only '
+              'allowed when using --dynamic. It takes a Dart compilation trace '
+              'file produced by the training run of the application. With this '
+              'flag, instead of using default Dart VM snapshot provided by the '
+              'engine, the application will use its own snapshot that includes '
               'additional compiled functions.'
       )
       ..addFlag('hotupdate',
         hide: !verboseHelp,
-        help: 'Build differential snapshot based on the last state of the build\n'
-              'tree and any changes to the application source code since then.\n'
-              'This flag is only allowed when using --dynamic. With this flag,\n'
-              'a partial VM snapshot is generated that is loaded on top of the\n'
+        help: 'Build differential snapshot based on the last state of the build '
+              'tree and any changes to the application source code since then. '
+              'This flag is only allowed when using --dynamic. With this flag, '
+              'a partial VM snapshot is generated that is loaded on top of the '
               'original VM snapshot that contains precompiled code.'
       )
       ..addMultiOption(FlutterOptions.kExtraFrontEndOptions,
diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart
index 8109f8d..b1e0c2d 100644
--- a/packages/flutter_tools/lib/src/commands/config.dart
+++ b/packages/flutter_tools/lib/src/commands/config.dart
@@ -35,7 +35,7 @@
   final String description =
     'Configure Flutter settings.\n\n'
     'To remove a setting, configure it to an empty string.\n\n'
-    'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve\n'
+    'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve '
     'Flutter tools over time. See Google\'s privacy policy: https://www.google.com/intl/en/policies/privacy/';
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index a99b75c..2711780 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -92,7 +92,7 @@
     argParser.addOption(
       'org',
       defaultsTo: 'com.example',
-      help: 'The organization responsible for your new Flutter project, in reverse domain name notation.\n'
+      help: 'The organization responsible for your new Flutter project, in reverse domain name notation. '
             'This string is used in Java package names and as prefix in the iOS bundle identifier.'
     );
     argParser.addOption(
@@ -174,7 +174,7 @@
     }
 
     if (Cache.flutterRoot == null)
-      throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment\n'
+      throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment '
         'variable was specified. Unable to find package:flutter.', exitCode: 2);
 
     await Cache.instance.updateAll();
@@ -230,7 +230,7 @@
         organization = existingOrganizations.first;
       } else if (1 < existingOrganizations.length) {
         throwToolExit(
-          'Ambiguous organization in existing files: $existingOrganizations.\n'
+          'Ambiguous organization in existing files: $existingOrganizations. '
           'The --org command line argument must be specified to recreate project.'
         );
       }
@@ -554,7 +554,7 @@
 /// if we should disallow the directory name.
 String _validateProjectDir(String dirPath, { String flutterRoot }) {
   if (fs.path.isWithin(flutterRoot, dirPath)) {
-    return 'Cannot create a project within the Flutter SDK.\n'
+    return 'Cannot create a project within the Flutter SDK. '
       "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index f8bf5a7..ae5da50 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -749,18 +749,26 @@
   Stream<LogMessage> get onMessage => _messageController.stream;
 
   @override
-  void printError(String message, { StackTrace stackTrace, bool emphasis = false, TerminalColor color }) {
+  void printError(
+      String message, {
+      StackTrace stackTrace,
+      bool emphasis = false,
+      TerminalColor color,
+      int indent,
+      int hangingIndent,
+    }) {
     _messageController.add(LogMessage('error', message, stackTrace));
   }
 
   @override
   void printStatus(
       String message, {
-        bool emphasis = false,
-        TerminalColor color,
-        bool newline = true,
-        int indent,
-      }) {
+      bool emphasis = false,
+      TerminalColor color,
+      bool newline = true,
+      int indent,
+      int hangingIndent,
+    }) {
     _messageController.add(LogMessage('status', message));
   }
 
@@ -875,9 +883,22 @@
   int _nextProgressId = 0;
 
   @override
-  void printError(String message, { StackTrace stackTrace, bool emphasis, TerminalColor color}) {
+  void printError(
+      String message, {
+      StackTrace stackTrace,
+      bool emphasis,
+      TerminalColor color,
+      int indent,
+      int hangingIndent,
+    }) {
     if (parent != null) {
-      parent.printError(message, stackTrace: stackTrace, emphasis: emphasis);
+      parent.printError(
+        message,
+        stackTrace: stackTrace,
+        emphasis: emphasis,
+        indent: indent,
+        hangingIndent: hangingIndent,
+      );
     } else {
       if (stackTrace != null) {
         _sendLogEvent(<String, dynamic>{
@@ -897,11 +918,12 @@
   @override
   void printStatus(
       String message, {
-        bool emphasis = false,
-        TerminalColor color,
-        bool newline = true,
-        int indent,
-      }) {
+      bool emphasis = false,
+      TerminalColor color,
+      bool newline = true,
+      int indent,
+      int hangingIndent,
+    }) {
     if (parent != null) {
       parent.printStatus(
         message,
@@ -909,6 +931,7 @@
         color: color,
         newline: newline,
         indent: indent,
+        hangingIndent: hangingIndent,
       );
     } else {
       _sendLogEvent(<String, dynamic>{'log': message});
diff --git a/packages/flutter_tools/lib/src/commands/devices.dart b/packages/flutter_tools/lib/src/commands/devices.dart
index 0613a98..78effffd 100644
--- a/packages/flutter_tools/lib/src/commands/devices.dart
+++ b/packages/flutter_tools/lib/src/commands/devices.dart
@@ -33,13 +33,13 @@
       printStatus(
         'No devices detected.\n\n'
         "Run 'flutter emulators' to list and start any available device emulators.\n\n"
-        'Or, if you expected your device to be detected, please run "flutter doctor" to diagnose\n'
+        'Or, if you expected your device to be detected, please run "flutter doctor" to diagnose '
         'potential issues, or visit https://flutter.io/setup/ for troubleshooting tips.');
       final List<String> diagnostics = await deviceManager.getDeviceDiagnostics();
       if (diagnostics.isNotEmpty) {
         printStatus('');
         for (String diagnostic in diagnostics) {
-          printStatus('• ${diagnostic.replaceAll('\n', '\n  ')}');
+          printStatus('• $diagnostic', hangingIndent: 2);
         }
       }
     } else {
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index 88daf78..2179381 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -45,23 +45,24 @@
       ..addFlag('keep-app-running',
         defaultsTo: null,
         help: 'Will keep the Flutter application running when done testing.\n'
-              'By default, "flutter drive" stops the application after tests are finished,\n'
-              'and --keep-app-running overrides this. On the other hand, if --use-existing-app\n'
-              'is specified, then "flutter drive" instead defaults to leaving the application\n'
+              'By default, "flutter drive" stops the application after tests are finished, '
+              'and --keep-app-running overrides this. On the other hand, if --use-existing-app '
+              'is specified, then "flutter drive" instead defaults to leaving the application '
               'running, and --no-keep-app-running overrides it.',
       )
       ..addOption('use-existing-app',
-        help: 'Connect to an already running instance via the given observatory URL.\n'
-              'If this option is given, the application will not be automatically started,\n'
+        help: 'Connect to an already running instance via the given observatory URL. '
+              'If this option is given, the application will not be automatically started, '
               'and it will only be stopped if --no-keep-app-running is explicitly set.',
         valueHelp: 'url',
       )
       ..addOption('driver',
-        help: 'The test file to run on the host (as opposed to the target file to run on\n'
-              'the device). By default, this file has the same base name as the target\n'
-              'file, but in the "test_driver/" directory instead, and with "_test" inserted\n'
-              'just before the extension, so e.g. if the target is "lib/main.dart", the\n'
-              'driver will be "test_driver/main_test.dart".',
+        help: 'The test file to run on the host (as opposed to the target file to run on '
+              'the device).\n'
+              'By default, this file has the same base name as the target file, but in the '
+              '"test_driver/" directory instead, and with "_test" inserted just before the '
+              'extension, so e.g. if the target is "lib/main.dart", the driver will be '
+              '"test_driver/main_test.dart".',
         valueHelp: 'path',
       );
   }
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart
index b7b748a..ab4a247 100644
--- a/packages/flutter_tools/lib/src/commands/packages.dart
+++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -105,10 +105,10 @@
   @override
   String get description {
     return 'Run the "test" package.\n'
-           'This is similar to "flutter test", but instead of hosting the tests in the\n'
-           'flutter environment it hosts the tests in a pure Dart environment. The main\n'
-           'differences are that the "dart:ui" library is not available and that tests\n'
-           'run faster. This is helpful for testing libraries that do not depend on any\n'
+           'This is similar to "flutter test", but instead of hosting the tests in the '
+           'flutter environment it hosts the tests in a pure Dart environment. The main '
+           'differences are that the "dart:ui" library is not available and that tests '
+           'run faster. This is helpful for testing libraries that do not depend on any '
            'packages from the Flutter SDK. It is equivalent to "pub run test".';
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 160cf29..6656619 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -32,7 +32,7 @@
       ..addFlag('ipv6',
         hide: true,
         negatable: false,
-        help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool\n'
+        help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool '
               'forwards the host port to a device port.',
       )
       ..addOption('route',
@@ -83,27 +83,27 @@
       )
       ..addFlag('enable-software-rendering',
         negatable: false,
-        help: 'Enable rendering using the Skia software backend. This is useful\n'
-              'when testing Flutter on emulators. By default, Flutter will\n'
-              'attempt to either use OpenGL or Vulkan and fall back to software\n'
-              'when neither is available.',
+        help: 'Enable rendering using the Skia software backend. '
+              'This is useful when testing Flutter on emulators. By default, '
+              'Flutter will attempt to either use OpenGL or Vulkan and fall back '
+              'to software when neither is available.',
       )
       ..addFlag('skia-deterministic-rendering',
         negatable: false,
-        help: 'When combined with --enable-software-rendering, provides 100%\n'
+        help: 'When combined with --enable-software-rendering, provides 100% '
               'deterministic Skia rendering.',
       )
       ..addFlag('trace-skia',
         negatable: false,
-        help: 'Enable tracing of Skia code. This is useful when debugging\n'
+        help: 'Enable tracing of Skia code. This is useful when debugging '
               'the GPU thread. By default, Flutter will not log skia code.',
       )
       ..addFlag('use-test-fonts',
         negatable: true,
-        help: 'Enable (and default to) the "Ahem" font. This is a special font\n'
-              'used in tests to remove any dependencies on the font metrics. It\n'
-              'is enabled when you use "flutter test". Set this flag when running\n'
-              'a test using "flutter run" for debugging purposes. This flag is\n'
+        help: 'Enable (and default to) the "Ahem" font. This is a special font '
+              'used in tests to remove any dependencies on the font metrics. It '
+              'is enabled when you use "flutter test". Set this flag when running '
+              'a test using "flutter run" for debugging purposes. This flag is '
               'only available when running in debug mode.',
       )
       ..addFlag('build',
@@ -116,19 +116,19 @@
       )
       ..addOption('precompile',
         hide: !verboseHelp,
-        help: 'Precompile functions specified in input file. This flag is only\n'
-              'allowed when using --dynamic. It takes a Dart compilation trace\n'
-              'file produced by the training run of the application. With this\n'
-              'flag, instead of using default Dart VM snapshot provided by the\n'
-              'engine, the application will use its own snapshot that includes\n'
+        help: 'Precompile functions specified in input file. This flag is only '
+              'allowed when using --dynamic. It takes a Dart compilation trace '
+              'file produced by the training run of the application. With this '
+              'flag, instead of using default Dart VM snapshot provided by the '
+              'engine, the application will use its own snapshot that includes '
               'additional functions.'
       )
       ..addFlag('hotupdate',
         hide: !verboseHelp,
-        help: 'Build differential snapshot based on the last state of the build\n'
-              'tree and any changes to the application source code since then.\n'
-              'This flag is only allowed when using --dynamic. With this flag,\n'
-              'a partial VM snapshot is generated that is loaded on top of the\n'
+        help: 'Build differential snapshot based on the last state of the build '
+              'tree and any changes to the application source code since then. '
+              'This flag is only allowed when using --dynamic. With this flag, '
+              'a partial VM snapshot is generated that is loaded on top of the '
               'original VM snapshot that contains precompiled code.'
       )
       ..addFlag('track-widget-creation',
@@ -142,7 +142,7 @@
       ..addFlag('machine',
         hide: !verboseHelp,
         negatable: false,
-        help: 'Handle machine structured JSON command input and provide output\n'
+        help: 'Handle machine structured JSON command input and provide output '
               'and progress in machine friendly format.',
       )
       ..addFlag('hot',
@@ -151,8 +151,8 @@
         help: 'Run with support for hot reloading.',
       )
       ..addOption('pid-file',
-        help: 'Specify a file to write the process id to.\n'
-              'You can send SIGUSR1 to trigger a hot reload\n'
+        help: 'Specify a file to write the process id to. '
+              'You can send SIGUSR1 to trigger a hot reload '
               'and SIGUSR2 to trigger a hot restart.',
       )
       ..addFlag('resident',
@@ -164,9 +164,9 @@
       ..addFlag('benchmark',
         negatable: false,
         hide: !verboseHelp,
-        help: 'Enable a benchmarking mode. This will run the given application,\n'
-              'measure the startup time and the app restart time, write the\n'
-              'results out to "refresh_benchmark.json", and exit. This flag is\n'
+        help: 'Enable a benchmarking mode. This will run the given application, '
+              'measure the startup time and the app restart time, write the '
+              'results out to "refresh_benchmark.json", and exit. This flag is '
               'intended for use in generating automated flutter benchmarks.',
       )
       ..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true)
diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart
index e6ae9ba..c2dc3d1 100644
--- a/packages/flutter_tools/lib/src/commands/screenshot.dart
+++ b/packages/flutter_tools/lib/src/commands/screenshot.dart
@@ -33,7 +33,7 @@
       valueHelp: 'port',
       help: 'The observatory port to connect to.\n'
           'This is required when --$_kType is "$_kSkiaType" or "$_kRasterizerType".\n'
-          'To find the observatory port number, use "flutter run --verbose"\n'
+          'To find the observatory port number, use "flutter run --verbose" '
           'and look for "Forwarded host port ... for Observatory" in the output.',
     );
     argParser.addOption(
@@ -42,8 +42,8 @@
       help: 'The type of screenshot to retrieve.',
       allowed: const <String>[_kDeviceType, _kSkiaType, _kRasterizerType],
       allowedHelp: const <String, String>{
-        _kDeviceType: 'Delegate to the device\'s native screenshot capabilities. This\n'
-            'screenshots the entire screen currently being displayed (including content\n'
+        _kDeviceType: 'Delegate to the device\'s native screenshot capabilities. This '
+            'screenshots the entire screen currently being displayed (including content '
             'not rendered by Flutter, like the device status bar).',
         _kSkiaType: 'Render the Flutter app as a Skia picture. Requires --$_kObservatoryPort',
         _kRasterizerType: 'Render the Flutter app using the rasterizer. Requires --$_kObservatoryPort',
diff --git a/packages/flutter_tools/lib/src/commands/shell_completion.dart b/packages/flutter_tools/lib/src/commands/shell_completion.dart
index 0ef8ca5..27a65dc 100644
--- a/packages/flutter_tools/lib/src/commands/shell_completion.dart
+++ b/packages/flutter_tools/lib/src/commands/shell_completion.dart
@@ -26,9 +26,9 @@
 
   @override
   final String description = 'Output command line shell completion setup scripts.\n\n'
-      'This command prints the flutter command line completion setup script for Bash and Zsh. To\n'
-      'use it, specify an output file and follow the instructions in the generated output file to\n'
-      'install it in your shell environment. Once it is sourced, your shell will be able to\n'
+      'This command prints the flutter command line completion setup script for Bash and Zsh. To '
+      'use it, specify an output file and follow the instructions in the generated output file to '
+      'install it in your shell environment. Once it is sourced, your shell will be able to '
       'complete flutter commands and options.';
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 978b589..b39a221 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -35,7 +35,7 @@
         negatable: false,
         help: 'Start in a paused mode and wait for a debugger to connect.\n'
               'You must specify a single test file to run, explicitly.\n'
-              'Instructions for connecting with a debugger and printed to the\n'
+              'Instructions for connecting with a debugger and printed to the '
               'console once the test has started.',
       )
       ..addFlag('coverage',
@@ -72,7 +72,7 @@
       )
       ..addFlag('update-goldens',
         negatable: false,
-        help: 'Whether matchesGoldenFile() calls within your test methods should\n'
+        help: 'Whether matchesGoldenFile() calls within your test methods should '
               'update the golden files rather than test for an existing match.',
       )
       ..addOption('concurrency',
@@ -94,8 +94,8 @@
     if (!fs.isFileSync('pubspec.yaml')) {
       throwToolExit(
         'Error: No pubspec.yaml file found in the current working directory.\n'
-        'Run this command from the root of your project. Test files must be\n'
-        'called *_test.dart and must reside in the package\'s \'test\'\n'
+        'Run this command from the root of your project. Test files must be '
+        'called *_test.dart and must reside in the package\'s \'test\' '
         'directory (or one of its subdirectories).');
     }
   }
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart
index a714199..7956964 100644
--- a/packages/flutter_tools/lib/src/commands/trace.dart
+++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -23,7 +23,7 @@
     argParser.addFlag('stop', negatable: false, help: 'Stop tracing. Implied if --start is also omitted.');
     argParser.addOption('duration',
       abbr: 'd',
-      help: 'Time to wait after starting (if --start is specified or implied) and before\n'
+      help: 'Time to wait after starting (if --start is specified or implied) and before '
             'stopping (if --stop is specified or implied).\n'
             'Defaults to ten seconds if --stop is specified or implied, zero otherwise.',
     );
@@ -38,8 +38,8 @@
 
   @override
   final String usageFooter =
-    '\`trace\` called without the --start or --stop flags will automatically start tracing,\n'
-    'delay a set amount of time (controlled by --duration), and stop tracing. To explicitly\n'
+    '\`trace\` called without the --start or --stop flags will automatically start tracing, '
+    'delay a set amount of time (controlled by --duration), and stop tracing. To explicitly '
     'control tracing, call trace with --start and later with --stop.\n'
     'The --debug-port argument is required.';
 
diff --git a/packages/flutter_tools/lib/src/dart/analysis.dart b/packages/flutter_tools/lib/src/dart/analysis.dart
index 189c174..7b38d13 100644
--- a/packages/flutter_tools/lib/src/dart/analysis.dart
+++ b/packages/flutter_tools/lib/src/dart/analysis.dart
@@ -4,12 +4,14 @@
 
 import 'dart:async';
 import 'dart:convert';
+import 'dart:math' as math;
 
 import '../base/file_system.dart' hide IOSink;
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/platform.dart';
 import '../base/process_manager.dart';
+import '../base/terminal.dart';
 import '../base/utils.dart';
 import '../globals.dart';
 
@@ -145,13 +147,20 @@
   }
 }
 
+enum _AnalysisSeverity {
+  error,
+  warning,
+  info,
+  none,
+}
+
 class AnalysisError implements Comparable<AnalysisError> {
   AnalysisError(this.json);
 
-  static final Map<String, int> _severityMap = <String, int>{
-    'ERROR': 3,
-    'WARNING': 2,
-    'INFO': 1
+  static final Map<String, _AnalysisSeverity> _severityMap = <String, _AnalysisSeverity>{
+    'INFO': _AnalysisSeverity.info,
+    'WARNING': _AnalysisSeverity.warning,
+    'ERROR': _AnalysisSeverity.error,
   };
 
   static final String _separator = platform.isWindows ? '-' : '•';
@@ -162,7 +171,19 @@
   Map<String, dynamic> json;
 
   String get severity => json['severity'];
-  int get severityLevel => _severityMap[severity] ?? 0;
+  String get colorSeverity {
+    switch(_severityLevel) {
+      case _AnalysisSeverity.error:
+        return terminal.color(severity, TerminalColor.red);
+      case _AnalysisSeverity.warning:
+        return terminal.color(severity, TerminalColor.yellow);
+      case _AnalysisSeverity.info:
+      case _AnalysisSeverity.none:
+        return severity;
+    }
+    return null;
+  }
+  _AnalysisSeverity get _severityLevel => _severityMap[severity] ?? _AnalysisSeverity.none;
   String get type => json['type'];
   String get message => json['message'];
   String get code => json['code'];
@@ -189,7 +210,7 @@
     if (offset != other.offset)
       return offset - other.offset;
 
-    final int diff = other.severityLevel - severityLevel;
+    final int diff = other._severityLevel.index - _severityLevel.index;
     if (diff != 0)
       return diff;
 
@@ -198,7 +219,10 @@
 
   @override
   String toString() {
-    return '${severity.toLowerCase().padLeft(7)} $_separator '
+    // Can't use "padLeft" because of ANSI color sequences in the colorized
+    // severity.
+    final String padding = ' ' * math.max(0, 7 - severity.length);
+    return '$padding${colorSeverity.toLowerCase()} $_separator '
         '$messageSentenceFragment $_separator '
         '${fs.path.relative(file)}:$startLine:$startColumn $_separator '
         '$code';
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index cca0673..f712fad 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -14,6 +14,8 @@
 import 'base/os.dart';
 import 'base/platform.dart';
 import 'base/process_manager.dart';
+import 'base/terminal.dart';
+import 'base/utils.dart';
 import 'base/version.dart';
 import 'cache.dart';
 import 'device.dart';
@@ -122,26 +124,28 @@
     bool allGood = true;
 
     for (DoctorValidator validator in validators) {
+      final StringBuffer lineBuffer = StringBuffer();
       final ValidationResult result = await validator.validate();
-      buffer.write('${result.leadingBox} ${validator.title} is ');
+      lineBuffer.write('${result.coloredLeadingBox} ${validator.title} is ');
       switch (result.type) {
         case ValidationType.missing:
-          buffer.write('not installed.');
+          lineBuffer.write('not installed.');
           break;
         case ValidationType.partial:
-          buffer.write('partially installed; more components are available.');
+          lineBuffer.write('partially installed; more components are available.');
           break;
         case ValidationType.notAvailable:
-          buffer.write('not available.');
+          lineBuffer.write('not available.');
           break;
         case ValidationType.installed:
-          buffer.write('fully installed.');
+          lineBuffer.write('fully installed.');
           break;
       }
 
       if (result.statusInfo != null)
-        buffer.write(' (${result.statusInfo})');
+        lineBuffer.write(' (${result.statusInfo})');
 
+      buffer.write(wrapText(lineBuffer.toString(), hangingIndent: result.leadingBox.length + 1));
       buffer.writeln();
 
       if (result.type != ValidationType.installed)
@@ -192,20 +196,23 @@
           break;
       }
 
-      if (result.statusInfo != null)
-        printStatus('${result.leadingBox} ${validator.title} (${result.statusInfo})');
-      else
-        printStatus('${result.leadingBox} ${validator.title}');
+      if (result.statusInfo != null) {
+        printStatus('${result.coloredLeadingBox} ${validator.title} (${result.statusInfo})',
+            hangingIndent: result.leadingBox.length + 1);
+      } else {
+        printStatus('${result.coloredLeadingBox} ${validator.title}',
+            hangingIndent: result.leadingBox.length + 1);
+      }
 
       for (ValidationMessage message in result.messages) {
-        if (message.isError || message.isHint || verbose == true) {
-          final String text = message.message.replaceAll('\n', '\n      ');
-          if (message.isError) {
-            printStatus('    ✗ $text', emphasis: true);
-          } else if (message.isHint) {
-            printStatus('    ! $text');
-          } else {
-            printStatus('    • $text');
+        if (message.type != ValidationMessageType.information || verbose == true) {
+          int hangingIndent = 2;
+          int indent = 4;
+          for (String line in '${message.coloredIndicator} ${message.message}'.split('\n')) {
+            printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
+            // Only do hanging indent for the first line.
+            hangingIndent = 0;
+            indent = 6;
           }
         }
       }
@@ -216,10 +223,11 @@
     // Make sure there's always one line before the summary even when not verbose.
     if (!verbose)
       printStatus('');
+
     if (issues > 0) {
-      printStatus('! Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.');
+      printStatus('${terminal.color('!', TerminalColor.yellow)} Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
     } else {
-      printStatus('• No issues found!');
+      printStatus('${terminal.color('•', TerminalColor.green)} No issues found!', hangingIndent: 2);
     }
 
     return doctorResult;
@@ -256,6 +264,12 @@
   installed,
 }
 
+enum ValidationMessageType {
+  error,
+  hint,
+  information,
+}
+
 abstract class DoctorValidator {
   const DoctorValidator(this.title);
 
@@ -344,17 +358,56 @@
     }
     return null;
   }
+
+  String get coloredLeadingBox {
+    assert(type != null);
+    switch (type) {
+      case ValidationType.missing:
+        return terminal.color(leadingBox, TerminalColor.red);
+      case ValidationType.installed:
+        return terminal.color(leadingBox, TerminalColor.green);
+      case ValidationType.notAvailable:
+      case ValidationType.partial:
+       return terminal.color(leadingBox, TerminalColor.yellow);
+    }
+    return null;
+  }
 }
 
 class ValidationMessage {
-  ValidationMessage(this.message) : isError = false, isHint = false;
-  ValidationMessage.error(this.message) : isError = true, isHint = false;
-  ValidationMessage.hint(this.message) : isError = false, isHint = true;
+  ValidationMessage(this.message) : type = ValidationMessageType.information;
+  ValidationMessage.error(this.message) : type = ValidationMessageType.error;
+  ValidationMessage.hint(this.message) : type = ValidationMessageType.hint;
 
-  final bool isError;
-  final bool isHint;
+  final ValidationMessageType type;
+  bool get isError => type == ValidationMessageType.error;
+  bool get isHint => type == ValidationMessageType.hint;
   final String message;
 
+  String get indicator {
+    switch (type) {
+      case ValidationMessageType.error:
+        return '✗';
+      case ValidationMessageType.hint:
+        return '!';
+      case ValidationMessageType.information:
+        return '•';
+    }
+    return null;
+  }
+
+  String get coloredIndicator {
+    switch (type) {
+      case ValidationMessageType.error:
+        return terminal.color(indicator, TerminalColor.red);
+      case ValidationMessageType.hint:
+        return terminal.color(indicator, TerminalColor.yellow);
+      case ValidationMessageType.information:
+        return terminal.color(indicator, TerminalColor.green);
+    }
+    return null;
+  }
+
   @override
   String toString() => message;
 }
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index ca8f334..d032326 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -25,12 +25,16 @@
   StackTrace stackTrace,
   bool emphasis,
   TerminalColor color,
+  int indent,
+  int hangingIndent,
 }) {
   logger.printError(
     message,
     stackTrace: stackTrace,
     emphasis: emphasis ?? false,
     color: color,
+    indent: indent,
+    hangingIndent: hangingIndent,
   );
 }
 
@@ -49,6 +53,7 @@
   bool newline,
   TerminalColor color,
   int indent,
+  int hangingIndent,
 }) {
   logger.printStatus(
     message,
@@ -56,6 +61,7 @@
     color: color,
     newline: newline ?? true,
     indent: indent,
+    hangingIndent: hangingIndent,
   );
 }
 
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index cef4908..c097507 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -100,7 +100,7 @@
       abbr: 't',
       defaultsTo: bundle.defaultMainPath,
       help: 'The main entry-point file of the application, as run on the device.\n'
-            'If the --target option is omitted, but a file name is provided on\n'
+            'If the --target option is omitted, but a file name is provided on '
             'the command line, then that is used instead.',
       valueHelp: 'path');
     _usesTargetOption = true;
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index dc9c2fda..0c46579 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -17,11 +17,13 @@
 import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/flags.dart';
+import '../base/io.dart' as io;
 import '../base/logger.dart';
 import '../base/os.dart';
 import '../base/platform.dart';
 import '../base/process.dart';
 import '../base/process_manager.dart';
+import '../base/terminal.dart';
 import '../base/utils.dart';
 import '../cache.dart';
 import '../dart/package_map.dart';
@@ -60,6 +62,16 @@
         negatable: false,
         hide: !verboseHelp,
         help: 'Reduce the amount of output from some commands.');
+    argParser.addFlag('wrap',
+        negatable: true,
+        hide: !verboseHelp,
+        help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.',
+        defaultsTo: true);
+    argParser.addOption('wrap-column',
+        hide: !verboseHelp,
+        help: 'Sets the output wrap column. If not set, uses the width of the terminal, or 100 if '
+            'the output is not a terminal. Use --no-wrap to turn off wrapping entirely.',
+        defaultsTo: null);
     argParser.addOption('device-id',
         abbr: 'd',
         help: 'Target device id or name (prefixes allowed).');
@@ -73,7 +85,8 @@
     argParser.addFlag('color',
         negatable: true,
         hide: !verboseHelp,
-        help: 'Whether to use terminal colors (requires support for ANSI escape sequences).');
+        help: 'Whether to use terminal colors (requires support for ANSI escape sequences).',
+        defaultsTo: true);
     argParser.addFlag('version-check',
         negatable: true,
         defaultsTo: true,
@@ -103,8 +116,8 @@
     argParser.addOption('flutter-root',
         hide: !verboseHelp,
         help: 'The root directory of the Flutter repository.\n'
-              'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent of the\n'
-              'directory that the "flutter" script itself is in.');
+              'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent '
+              'of the directory that the "flutter" script itself is in.');
 
     if (verboseHelp)
       argParser.addSeparator('Local build selection options (not normally required):');
@@ -112,9 +125,10 @@
     argParser.addOption('local-engine-src-path',
         hide: !verboseHelp,
         help: 'Path to your engine src directory, if you are building Flutter locally.\n'
-              'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to the path given in your pubspec.yaml\n'
-              'dependency_overrides for $kFlutterEnginePackageName, if any, or, failing that, tries to guess at the location\n'
-              'based on the value of the --flutter-root option.');
+              'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to '
+              'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, '
+              'if any, or, failing that, tries to guess at the location based on the value of the '
+              '--flutter-root option.');
 
     argParser.addOption('local-engine',
         hide: !verboseHelp,
@@ -127,15 +141,15 @@
 
     argParser.addOption('record-to',
         hide: !verboseHelp,
-        help: 'Enables recording of process invocations (including stdout and stderr of all such invocations),\n'
+        help: 'Enables recording of process invocations (including stdout and stderr of all such invocations), '
               'and file system access (reads and writes).\n'
-              'Serializes that recording to a directory with the path specified in this flag. If the\n'
+              'Serializes that recording to a directory with the path specified in this flag. If the '
               'directory does not already exist, it will be created.');
     argParser.addOption('replay-from',
         hide: !verboseHelp,
-        help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from\n'
-              'the specified recording (obtained via --record-to). The path specified in this flag must refer\n'
-              'to a directory that holds serialized process invocations structured according to the output of\n'
+        help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from '
+              'the specified recording (obtained via --record-to). The path specified in this flag must refer '
+              'to a directory that holds serialized process invocations structured according to the output of '
               '--record-to.');
     argParser.addFlag('show-test-device',
         negatable: false,
@@ -146,11 +160,20 @@
 
   @override
   ArgParser get argParser => _argParser;
-  final ArgParser _argParser = ArgParser(allowTrailingOptions: false);
+  final ArgParser _argParser = ArgParser(
+    allowTrailingOptions: false,
+    usageLineLength: outputPreferences.wrapText ? outputPreferences.wrapColumn : null,
+  );
 
   @override
   String get usageFooter {
-    return 'Run "flutter help -v" for verbose help output, including less commonly used options.';
+    return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.');
+  }
+
+  @override
+  String get usage {
+    final String usageWithoutDescription = super.usage.substring(description.length + 2);
+    return  '${wrapText(description)}\n\n$usageWithoutDescription';
   }
 
   static String get _defaultFlutterRoot {
@@ -223,6 +246,32 @@
       contextOverrides[Logger] = VerboseLogger(logger);
     }
 
+    final int terminalColumns = io.stdio.terminalColumns;
+    int wrapColumn = terminalColumns ?? kDefaultTerminalColumns;
+    if (topLevelResults['wrap-column'] != null) {
+      try {
+        wrapColumn = int.parse(topLevelResults['wrap-column']);
+        if (wrapColumn < 0) {
+          throwToolExit('Argument to --wrap-column must be a positive integer. '
+              'You supplied ${topLevelResults['wrap-column']}.');
+        }
+      } on FormatException {
+        throwToolExit('Unable to parse argument '
+            '--wrap-column=${topLevelResults['wrap-column']}. Must be a positive integer.');
+      }
+    }
+
+    // If we're not writing to a terminal with a defined width, then don't wrap
+    // anything, unless the user explicitly said to.
+    final bool useWrapping = topLevelResults.wasParsed('wrap')
+        ? topLevelResults['wrap']
+        : terminalColumns == null ? false : topLevelResults['wrap'];
+    contextOverrides[OutputPreferences] = OutputPreferences(
+      wrapText: useWrapping,
+      showColor: topLevelResults['color'],
+      wrapColumn: wrapColumn,
+    );
+
     if (topLevelResults['show-test-device'] ||
         topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
       FlutterTesterDevices.showFlutterTesterDevice = true;
@@ -307,9 +356,6 @@
       body: () async {
         logger.quiet = topLevelResults['quiet'];
 
-        if (topLevelResults.wasParsed('color'))
-          logger.supportsColor = topLevelResults['color'];
-
         if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
           await Cache.lock();
 
@@ -343,7 +389,6 @@
         if (topLevelResults['machine']) {
           throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
         }
-
         await super.runCommand(topLevelResults);
       },
     );
@@ -377,8 +422,8 @@
 
       if (engineSourcePath == null) {
         throwToolExit('Unable to detect local Flutter engine build directory.\n'
-          'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and\n'
-          'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or\n'
+          'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and '
+          'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or '
           'use --local-engine-src-path to specify the path to the root of your flutter/engine repository.',
           exitCode: 2);
       }
@@ -386,7 +431,7 @@
 
     if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) {
       throwToolExit('Unable to detect a Flutter engine build directory in $engineSourcePath.\n'
-        'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that\n'
+        'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that '
         'you have compiled the engine in that directory, which should produce an \'out\' directory',
         exitCode: 2);
     }
@@ -469,7 +514,7 @@
             'Warning: the \'flutter\' tool you are currently running is not the one from the current directory:\n'
             '  running Flutter  : ${Cache.flutterRoot}\n'
             '  current directory: $directory\n'
-            'This can happen when you have multiple copies of flutter installed. Please check your system path to verify\n'
+            'This can happen when you have multiple copies of flutter installed. Please check your system path to verify '
             'that you\'re running the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
           );
         }
@@ -495,25 +540,25 @@
 
         if (!fs.isDirectorySync(flutterPath)) {
           printError(
-            'Warning! This package referenced a Flutter repository via the .packages file that is\n'
-            'no longer available. The repository from which the \'flutter\' tool is currently\n'
+            'Warning! This package referenced a Flutter repository via the .packages file that is '
+            'no longer available. The repository from which the \'flutter\' tool is currently '
             'executing will be used instead.\n'
             '  running Flutter tool: ${Cache.flutterRoot}\n'
             '  previous reference  : $flutterPath\n'
-            'This can happen if you deleted or moved your copy of the Flutter repository, or\n'
-            'if it was on a volume that is no longer mounted or has been mounted at a\n'
-            'different location. Please check your system path to verify that you are running\n'
+            'This can happen if you deleted or moved your copy of the Flutter repository, or '
+            'if it was on a volume that is no longer mounted or has been mounted at a '
+            'different location. Please check your system path to verify that you are running '
             'the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
           );
         } else if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) {
           printError(
-            'Warning! The \'flutter\' tool you are currently running is from a different Flutter\n'
-            'repository than the one last used by this package. The repository from which the\n'
+            'Warning! The \'flutter\' tool you are currently running is from a different Flutter '
+            'repository than the one last used by this package. The repository from which the '
             '\'flutter\' tool is currently executing will be used instead.\n'
             '  running Flutter tool: ${Cache.flutterRoot}\n'
             '  previous reference  : $flutterPath\n'
-            'This can happen when you have multiple copies of flutter installed. Please check\n'
-            'your system path to verify that you are running the expected version (run\n'
+            'This can happen when you have multiple copies of flutter installed. Please check '
+            'your system path to verify that you are running the expected version (run '
             '\'flutter --version\' to see which flutter is on your path).\n'
           );
         }