[cfe] Actually have both ansi and plain text formatted messages

The CFEs FormattedMessage always had two getters to get the text inside
one that would supposedly give an ansi formated version of the message
and one that would supposedly give a plaintext formated version of the
message. They both returned the same string, though, which would either
be with ansi escape codes or plain text depending on the environment at
compile time.

This CL fixes that by having both messages, and letting the reporting
(i.e. whenever the message is read) decide which to use. That way we
can - for instance - report errors with color if the terminal supports
it correctly when reusing a dill (and reissuing problems, but where the
terminal support changes) and if printing the problem to an html <pre>
field (like observatory does (1)).

It also cleans up two different implementations of whether we think
the terminal supports colors or not, by deleting one of them.

(1) At least sometimes. It works - I think - only for 'evaluateInFrame',
    but that's another story (and will be fixed in a follow-up CL).

TEST=Existing test suites.

Change-Id: Iedaedd9a5c41458d40c23ed4b706324c004ae943
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/186291
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes.dart
index acc5edc..bea98e1 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/codes.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes.dart
@@ -131,11 +131,11 @@
     return message.compareTo(message);
   }
 
-  FormattedMessage withFormatting(String formatted, int line, int column,
-      Severity severity, List<FormattedMessage> relatedInformation,
+  FormattedMessage withFormatting(PlainAndColorizedString formatted, int line,
+      int column, Severity severity, List<FormattedMessage> relatedInformation,
       {List<Uri>? involvedFiles}) {
-    return new FormattedMessage(
-        this, formatted, line, column, severity, relatedInformation,
+    return new FormattedMessage(this, formatted.plain, formatted.colorized,
+        line, column, severity, relatedInformation,
         involvedFiles: involvedFiles);
   }
 
@@ -162,10 +162,20 @@
       'messageObject=$messageObject)';
 }
 
+class PlainAndColorizedString {
+  final String plain;
+  final String colorized;
+
+  const PlainAndColorizedString(this.plain, this.colorized);
+  const PlainAndColorizedString.plainOnly(this.plain) : this.colorized = plain;
+}
+
 class FormattedMessage implements DiagnosticMessage {
   final LocatedMessage locatedMessage;
 
-  final String formatted;
+  final String formattedPlain;
+
+  final String formattedColorized;
 
   final int line;
 
@@ -178,8 +188,14 @@
 
   final List<Uri>? involvedFiles;
 
-  const FormattedMessage(this.locatedMessage, this.formatted, this.line,
-      this.column, this.severity, this.relatedInformation,
+  const FormattedMessage(
+      this.locatedMessage,
+      this.formattedPlain,
+      this.formattedColorized,
+      this.line,
+      this.column,
+      this.severity,
+      this.relatedInformation,
       {this.involvedFiles});
 
   Code<dynamic> get code => locatedMessage.code;
@@ -200,18 +216,22 @@
 
   @override
   Iterable<String> get ansiFormatted sync* {
-    yield formatted;
+    yield formattedColorized;
     if (relatedInformation != null) {
       for (FormattedMessage m in relatedInformation!) {
-        yield m.formatted;
+        yield m.formattedColorized;
       }
     }
   }
 
   @override
-  Iterable<String> get plainTextFormatted {
-    // TODO(ahe): Implement this correctly.
-    return ansiFormatted;
+  Iterable<String> get plainTextFormatted sync* {
+    yield formattedPlain;
+    if (relatedInformation != null) {
+      for (FormattedMessage m in relatedInformation!) {
+        yield m.formattedPlain;
+      }
+    }
   }
 
   Map<String, Object?> toJson() {
diff --git a/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart b/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
index 58af2ac..a4c69bd 100644
--- a/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
@@ -9,7 +9,8 @@
 import 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart'
     show DiagnosticMessage, DiagnosticMessageHandler;
 
-import 'package:_fe_analyzer_shared/src/messages/codes.dart' show Message, Code;
+import 'package:_fe_analyzer_shared/src/messages/codes.dart'
+    show Code, Message, PlainAndColorizedString;
 
 import 'package:dev_compiler/dev_compiler.dart';
 import 'package:dev_compiler/src/js_ast/js_ast.dart' as js_ast;
@@ -44,8 +45,8 @@
   return Message(Code<String>('Expression Compiler Internal error'),
           message: msg)
       .withLocation(uri, 0, 0)
-      .withFormatting(
-          'Internal error: $msg', line, col, Severity.internalProblem, []);
+      .withFormatting(PlainAndColorizedString.plainOnly('Internal error: $msg'),
+          line, col, Severity.internalProblem, []);
 }
 
 /// Dart scope
diff --git a/pkg/front_end/lib/src/api_prototype/terminal_color_support.dart b/pkg/front_end/lib/src/api_prototype/terminal_color_support.dart
index ab69e10..124a899 100644
--- a/pkg/front_end/lib/src/api_prototype/terminal_color_support.dart
+++ b/pkg/front_end/lib/src/api_prototype/terminal_color_support.dart
@@ -4,110 +4,18 @@
 
 library front_end.terminal_color_support;
 
-import 'dart:convert' show jsonEncode;
-
-import 'dart:io' show Platform, Process, ProcessResult, stderr, stdout;
-
 import 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart'
     show DiagnosticMessage;
 
-import 'package:_fe_analyzer_shared/src/util/colors.dart'
-    show ALL_CODES, TERMINAL_CAPABILITIES;
+import 'package:_fe_analyzer_shared/src/util/colors.dart' show enableColors;
 
-/// True if we should enable colors in output.
-///
-/// We enable colors only when both [stdout] and [stderr] support ANSI escapes.
-final bool enableTerminalColors = _computeEnableColors();
+export 'package:_fe_analyzer_shared/src/util/colors.dart' show enableColors;
 
 void printDiagnosticMessage(
     DiagnosticMessage message, void Function(String) println) {
-  if (enableTerminalColors) {
+  if (enableColors) {
     message.ansiFormatted.forEach(println);
   } else {
     message.plainTextFormatted.forEach(println);
   }
 }
-
-/// On Windows, colors are enabled if both stdout and stderr supports ANSI
-/// escapes.  On other platforms, we rely on the external programs `tty` and
-/// `tput` to compute if ANSI colors are supported.
-bool _computeEnableColors() {
-  const bool debug =
-      const bool.fromEnvironment("front_end.debug_compute_enable_colors");
-
-  if (Platform.isWindows) {
-    if (!stdout.supportsAnsiEscapes || !stderr.supportsAnsiEscapes) {
-      // In this case, either [stdout] or [stderr] did not support the property
-      // `supportsAnsiEscapes`. Since we do not have another way to determine
-      // support for colors, we disable them.
-      if (debug) {
-        print("Not enabling colors as ANSI is not supported.");
-      }
-      return false;
-    }
-    if (debug) {
-      print("Enabling colors as OS is Windows.");
-    }
-    return true;
-  }
-
-  // We have to check if the terminal actually supports colors. Currently, to
-  // avoid linking the Dart VM with ncurses, ANSI escape support is reduced to
-  // `Platform.environment['TERM'].contains("xterm")`.
-
-  // Check if stdin is a terminal (TTY).
-  ProcessResult result =
-      Process.runSync("/bin/sh", ["-c", "tty > /dev/null 2> /dev/null"]);
-
-  if (result.exitCode != 0) {
-    if (debug) {
-      print("Not enabling colors, stdin isn't a terminal.");
-    }
-    return false;
-  }
-
-  // The `-S` option of `tput` allows us to query multiple capabilities at
-  // once.
-  result = Process.runSync(
-      "/bin/sh", ["-c", "printf '%s' '$TERMINAL_CAPABILITIES' | tput -S"]);
-
-  if (result.exitCode != 0) {
-    if (debug) {
-      print("Not enabling colors, running tput failed.");
-    }
-    return false;
-  }
-
-  List<String> lines = result.stdout.split("\n");
-
-  if (lines.length != 2) {
-    if (debug) {
-      print("Not enabling colors, unexpected output from tput: "
-          "${jsonEncode(result.stdout)}.");
-    }
-    return false;
-  }
-
-  String numberOfColors = lines[0];
-  if ((int.tryParse(numberOfColors) ?? -1) < 8) {
-    if (debug) {
-      print("Not enabling colors, less than 8 colors supported: "
-          "${jsonEncode(numberOfColors)}.");
-    }
-    return false;
-  }
-
-  String allCodes = lines[1].trim();
-  if (ALL_CODES != allCodes) {
-    if (debug) {
-      print("Not enabling colors, color codes don't match: "
-          "${jsonEncode(ALL_CODES)} != ${jsonEncode(allCodes)}.");
-    }
-    return false;
-  }
-
-  if (debug) {
-    print("Enabling colors.");
-  }
-  return true;
-}
diff --git a/pkg/front_end/lib/src/api_unstable/vm.dart b/pkg/front_end/lib/src/api_unstable/vm.dart
index 1b23da1..a3dcde5 100644
--- a/pkg/front_end/lib/src/api_unstable/vm.dart
+++ b/pkg/front_end/lib/src/api_unstable/vm.dart
@@ -36,7 +36,7 @@
 export '../api_prototype/standard_file_system.dart' show StandardFileSystem;
 
 export '../api_prototype/terminal_color_support.dart'
-    show printDiagnosticMessage;
+    show printDiagnosticMessage, enableColors;
 
 export '../base/nnbd_mode.dart' show NnbdMode;
 
diff --git a/pkg/front_end/lib/src/base/processed_options.dart b/pkg/front_end/lib/src/base/processed_options.dart
index 94a7d0d..1ea8dc4 100644
--- a/pkg/front_end/lib/src/base/processed_options.dart
+++ b/pkg/front_end/lib/src/base/processed_options.dart
@@ -45,6 +45,7 @@
         FormattedMessage,
         LocatedMessage,
         Message,
+        PlainAndColorizedString,
         messageCantInferPackagesFromManyInputs,
         messageCantInferPackagesFromPackageUri,
         messageCompilingWithSoundNullSafety,
@@ -216,7 +217,7 @@
     int offset = message.charOffset;
     Uri uri = message.uri;
     Location location = offset == -1 ? null : getLocation(uri, offset);
-    String formatted =
+    PlainAndColorizedString formatted =
         command_line_reporting.format(message, severity, location: location);
     List<FormattedMessage> formattedContext;
     if (context != null && context.isNotEmpty) {
diff --git a/pkg/front_end/lib/src/fasta/command_line_reporting.dart b/pkg/front_end/lib/src/fasta/command_line_reporting.dart
index 2b05cc6..9ff9cf4 100644
--- a/pkg/front_end/lib/src/fasta/command_line_reporting.dart
+++ b/pkg/front_end/lib/src/fasta/command_line_reporting.dart
@@ -21,7 +21,7 @@
     show $CARET, $SPACE, $TAB;
 
 import 'package:_fe_analyzer_shared/src/util/colors.dart'
-    show enableColors, green, magenta, red, yellow;
+    show green, magenta, red, yellow;
 
 import 'package:_fe_analyzer_shared/src/util/relativize.dart'
     show isWindows, relativizeUri;
@@ -34,7 +34,7 @@
 
 import 'crash.dart' show Crash, safeToString;
 
-import 'fasta_codes.dart' show LocatedMessage;
+import 'fasta_codes.dart' show LocatedMessage, PlainAndColorizedString;
 
 import 'messages.dart' show getLocation, getSourceLine;
 
@@ -42,10 +42,10 @@
 
 const bool hideWarnings = false;
 
-/// Formats [message] as a string that is suitable for output from a
-/// command-line tool. This includes source snippets and different colors based
-/// on [severity].
-String format(LocatedMessage message, Severity severity,
+/// Formats [message] as two strings that is suitable for output from a
+/// command-line tool. This includes source snippets and - in the colorized
+/// version - different colors based on [severity].
+PlainAndColorizedString format(LocatedMessage message, Severity severity,
     {Location location, Map<Uri, Source> uriToSource}) {
   try {
     int length = message.length;
@@ -55,34 +55,34 @@
       length = 1;
     }
     String prefix = severityPrefixes[severity];
-    String messageText =
+    String messageTextTmp =
         prefix == null ? message.message : "$prefix: ${message.message}";
     if (message.tip != null) {
-      messageText += "\n${message.tip}";
+      messageTextTmp += "\n${message.tip}";
     }
-    if (enableColors) {
-      switch (severity) {
-        case Severity.error:
-        case Severity.internalProblem:
-          messageText = red(messageText);
-          break;
+    final String messageTextPlain = messageTextTmp;
+    String messageTextColorized;
+    switch (severity) {
+      case Severity.error:
+      case Severity.internalProblem:
+        messageTextColorized = red(messageTextPlain);
+        break;
 
-        case Severity.warning:
-          messageText = magenta(messageText);
-          break;
+      case Severity.warning:
+        messageTextColorized = magenta(messageTextPlain);
+        break;
 
-        case Severity.context:
-          messageText = green(messageText);
-          break;
+      case Severity.context:
+        messageTextColorized = green(messageTextPlain);
+        break;
 
-        case Severity.info:
-          messageText = yellow(messageText);
-          break;
+      case Severity.info:
+        messageTextColorized = yellow(messageTextPlain);
+        break;
 
-        case Severity.ignored:
-        default:
-          return unhandled("$severity", "format", -1, null);
-      }
+      case Severity.ignored:
+      default:
+        return unhandled("$severity", "format", -1, null);
     }
 
     if (message.uri != null) {
@@ -94,10 +94,17 @@
         location = null;
       }
       String sourceLine = getSourceLine(location, uriToSource);
-      return formatErrorMessage(
-          sourceLine, location, length, path, messageText);
+      return new PlainAndColorizedString(
+        formatErrorMessage(
+            sourceLine, location, length, path, messageTextPlain),
+        formatErrorMessage(
+            sourceLine, location, length, path, messageTextColorized),
+      );
     } else {
-      return messageText;
+      return new PlainAndColorizedString(
+        messageTextPlain,
+        messageTextColorized,
+      );
     }
   } catch (error, trace) {
     print("Crash when formatting: "
diff --git a/pkg/front_end/lib/src/fasta/compiler_context.dart b/pkg/front_end/lib/src/fasta/compiler_context.dart
index a1d2005..913a4e1 100644
--- a/pkg/front_end/lib/src/fasta/compiler_context.dart
+++ b/pkg/front_end/lib/src/fasta/compiler_context.dart
@@ -8,6 +8,7 @@
 
 import 'dart:async' show Zone, runZoned;
 
+import 'package:_fe_analyzer_shared/src/messages/codes.dart';
 import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity;
 
 import 'package:_fe_analyzer_shared/src/util/colors.dart' as colors;
@@ -79,16 +80,10 @@
   }
 
   /// Format [message] as a text string that can be included in generated code.
-  String format(LocatedMessage message, Severity severity) {
+  PlainAndColorizedString format(LocatedMessage message, Severity severity) {
     return command_line_reporting.format(message, severity);
   }
 
-  /// Format [message] as a text string that can be included in generated code.
-  // TODO(askesc): Remove this and direct callers directly to format.
-  String formatWithoutLocation(Message message, Severity severity) {
-    return command_line_reporting.format(message.withoutLocation(), severity);
-  }
-
   // TODO(ahe): Remove this.
   void logError(Object message, Severity severity) {
     errors.add(message);
diff --git a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
index edcd72b..e9cd910 100644
--- a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
@@ -5976,7 +5976,8 @@
           wasHandled: true, context: context);
     }
     String text = libraryBuilder.loader.target.context
-        .format(message.withLocation(uri, charOffset, length), Severity.error);
+        .format(message.withLocation(uri, charOffset, length), Severity.error)
+        .plain;
     InvalidExpression expression = new InvalidExpression(text)
       ..fileOffset = charOffset;
     return expression;
@@ -6451,7 +6452,8 @@
     addProblemErrorIfConst(message, charOffset, length,
         wasHandled: wasHandled, context: context);
     String text = libraryBuilder.loader.target.context
-        .format(message.withLocation(uri, charOffset, length), Severity.error);
+        .format(message.withLocation(uri, charOffset, length), Severity.error)
+        .plain;
     InvalidExpression expression = new InvalidExpression(text)
       ..fileOffset = charOffset;
     return expression;
diff --git a/pkg/front_end/test/lint_test.status b/pkg/front_end/test/lint_test.status
index 2728954..691adfa 100644
--- a/pkg/front_end/test/lint_test.status
+++ b/pkg/front_end/test/lint_test.status
@@ -14,6 +14,7 @@
 front_end/lib/src/api_prototype/front_end/Exports: Fail
 front_end/lib/src/api_prototype/incremental_kernel_generator/Exports: Fail
 front_end/lib/src/api_prototype/language_version/Exports: Fail
+front_end/lib/src/api_prototype/terminal_color_support/Exports: Fail
 front_end/lib/src/api_unstable/bazel_worker/ImportsTwice: Fail
 front_end/lib/src/fasta/fasta_codes/Exports: Fail
 front_end/lib/src/fasta/incremental_compiler/ImportsTwice: Fail
diff --git a/pkg/front_end/test/messages_json_test.dart b/pkg/front_end/test/messages_json_test.dart
index e8fe28a..7f87ca4 100644
--- a/pkg/front_end/test/messages_json_test.dart
+++ b/pkg/front_end/test/messages_json_test.dart
@@ -27,18 +27,34 @@
     LocatedMessage locatedMessage1 =
         new LocatedMessage(Uri.parse("what:ever/fun_1.dart"), 117, 2, message);
     FormattedMessage formattedMessage2 = new FormattedMessage(
-        null, "Formatted string #2", 13, 2, Severity.error, []);
+        null,
+        "Formatted string Plain #2",
+        "Formatted string Colorized #2",
+        13,
+        2,
+        Severity.error, []);
     FormattedMessage formattedMessage3 = new FormattedMessage(
-        null, "Formatted string #3", 313, 32, Severity.error, []);
+        null,
+        "Formatted string Plain #3",
+        "Formatted string Colorized #3",
+        313,
+        32,
+        Severity.error, []);
 
     FormattedMessage formattedMessage1 = new FormattedMessage(
-        locatedMessage1, "Formatted string", 42, 86, severity, [
+        locatedMessage1,
+        "Formatted string Plain",
+        "Formatted string Colorized",
+        42,
+        86,
+        severity, [
       formattedMessage2,
       formattedMessage3
-    ], involvedFiles: [
-      Uri.parse("what:ever/foo.dart"),
-      Uri.parse("what:ever/bar.dart")
-    ]);
+    ],
+        involvedFiles: [
+          Uri.parse("what:ever/foo.dart"),
+          Uri.parse("what:ever/bar.dart")
+        ]);
     expect(formattedMessage1.codeName, "MyCodeName");
 
     DiagnosticMessageFromJson diagnosticMessageFromJson =
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 626be69..e262498 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -210,6 +210,7 @@
 codec
 codes
 collision
+colorized
 com
 combinations
 combinator
diff --git a/pkg/front_end/test/static_types/cfe_allowed.json b/pkg/front_end/test/static_types/cfe_allowed.json
index 63088f0..ca079c1 100644
--- a/pkg/front_end/test/static_types/cfe_allowed.json
+++ b/pkg/front_end/test/static_types/cfe_allowed.json
@@ -1,7 +1,4 @@
 {
-  "pkg/front_end/lib/src/api_prototype/terminal_color_support.dart": {
-    "Dynamic invocation of 'split'.": 1
-  },
   "pkg/front_end/lib/src/base/libraries_specification.dart": {
     "Dynamic invocation of 'toList'.": 1,
     "Dynamic invocation of 'map'.": 1,
diff --git a/pkg/front_end/test/utils/validating_instrumentation.dart b/pkg/front_end/test/utils/validating_instrumentation.dart
index 184e7cb..1c1af83 100644
--- a/pkg/front_end/test/utils/validating_instrumentation.dart
+++ b/pkg/front_end/test/utils/validating_instrumentation.dart
@@ -228,11 +228,14 @@
 
   String _formatProblem(
       Uri uri, int offset, String desc, StackTrace stackTrace) {
-    return CompilerContext.current.format(
-        templateUnspecified
-            .withArguments('$desc${stackTrace == null ? '' : '\n$stackTrace'}')
-            .withLocation(uri, offset, noLength),
-        Severity.internalProblem);
+    return CompilerContext.current
+        .format(
+            templateUnspecified
+                .withArguments(
+                    '$desc${stackTrace == null ? '' : '\n$stackTrace'}')
+                .withLocation(uri, offset, noLength),
+            Severity.internalProblem)
+        .plain;
   }
 
   String _makeExpectationComment(String property, InstrumentationValue value) {
diff --git a/pkg/front_end/tool/_fasta/command_line.dart b/pkg/front_end/tool/_fasta/command_line.dart
index 754f7f2..e38ab78 100644
--- a/pkg/front_end/tool/_fasta/command_line.dart
+++ b/pkg/front_end/tool/_fasta/command_line.dart
@@ -24,6 +24,7 @@
 
 import 'package:front_end/src/api_prototype/standard_file_system.dart'
     show StandardFileSystem;
+import 'package:front_end/src/api_prototype/terminal_color_support.dart';
 import 'package:front_end/src/base/nnbd_mode.dart';
 
 import 'package:front_end/src/base/processed_options.dart'
@@ -39,9 +40,10 @@
 import 'package:front_end/src/fasta/fasta_codes.dart'
     show
         Message,
-        templateFastaCLIArgumentRequired,
+        PlainAndColorizedString,
         messageFastaUsageLong,
         messageFastaUsageShort,
+        templateFastaCLIArgumentRequired,
         templateUnspecified;
 
 import 'package:front_end/src/fasta/problems.dart' show DebugAbort;
@@ -440,10 +442,18 @@
     problem = e;
   }
 
-  return CompilerContext.runWithOptions<T>(options, (c) {
+  return CompilerContext.runWithOptions<T>(options, (CompilerContext c) {
     if (problem != null) {
       print(computeUsage(programName, options.verbose).message);
-      print(c.formatWithoutLocation(problem.message, Severity.error));
+      PlainAndColorizedString formatted =
+          c.format(problem.message.withoutLocation(), Severity.error);
+      String formattedText;
+      if (enableColors) {
+        formattedText = formatted.colorized;
+      } else {
+        formattedText = formatted.plain;
+      }
+      print(formattedText);
       exit(1);
     }
 
diff --git a/pkg/vm/bin/kernel_service.dart b/pkg/vm/bin/kernel_service.dart
index 0fcaf18..e740e1a 100644
--- a/pkg/vm/bin/kernel_service.dart
+++ b/pkg/vm/bin/kernel_service.dart
@@ -93,7 +93,8 @@
     int nullSafety,
     List<String> experimentalFlags,
     Uri packagesUri,
-    List<String> errors,
+    List<String> errorsPlain,
+    List<String> errorsColorized,
     String invocationModes,
     String verbosityLevel) {
   final expFlags = <String>[];
@@ -113,8 +114,10 @@
     ..verbose = verbose
     ..omitPlatform = true
     ..explicitExperimentalFlags = parseExperimentalFlags(
-        parseExperimentalArguments(expFlags),
-        onError: (msg) => errors.add(msg))
+        parseExperimentalArguments(expFlags), onError: (msg) {
+      errorsPlain.add(msg);
+      errorsColorized.add(msg);
+    })
     ..environmentDefines = new EnvironmentMap()
     ..nnbdMode = (nullSafety == kNullSafetyOptionStrong)
         ? NnbdMode.Strong
@@ -128,7 +131,8 @@
           // TODO(sigmund): support emitting code with errors as long as they
           // are handled in the generated code.
           printToStdErr = false; // errors are printed by VM
-          errors.addAll(message.plainTextFormatted);
+          errorsPlain.addAll(message.plainTextFormatted);
+          errorsColorized.addAll(message.ansiFormatted);
           break;
         case Severity.warning:
           printToStdErr = true;
@@ -168,7 +172,8 @@
   final bool supportCodeCoverage;
   final bool supportHotReload;
 
-  final List<String> errors = <String>[];
+  final List<String> errorsPlain = <String>[];
+  final List<String> errorsColorized = <String>[];
 
   CompilerOptions options;
 
@@ -202,7 +207,8 @@
         nullSafety,
         experimentalFlags,
         packagesUri,
-        errors,
+        errorsPlain,
+        errorsColorized,
         invocationModes,
         verbosityLevel);
   }
@@ -212,7 +218,7 @@
       final CompilerResult compilerResult = await compileInternal(script);
       final Component component = compilerResult.component;
 
-      if (errors.isEmpty) {
+      if (errorsPlain.isEmpty) {
         // Record dependencies only if compilation was error free.
         _recordDependencies(isolateGroupId, component, options.packagesFileUri);
       }
@@ -333,7 +339,8 @@
     if (generator == null) {
       generator = new IncrementalCompiler(options, script);
     }
-    errors.clear();
+    errorsPlain.clear();
+    errorsColorized.clear();
     final component = await generator.compile(entryPoint: script);
     return new CompilerResult(component, const {},
         generator.getClassHierarchy(), generator.getCoreTypes());
@@ -609,7 +616,8 @@
     return;
   }
 
-  compiler.errors.clear();
+  compiler.errorsPlain.clear();
+  compiler.errorsColorized.clear();
 
   CompilationResult result;
   try {
@@ -622,10 +630,13 @@
       return;
     }
 
-    if (compiler.errors.isNotEmpty) {
+    assert(compiler.errorsPlain.length == compiler.errorsColorized.length);
+    // Any error will be printed verbatim in observatory, so we always use the
+    // plain version (i.e. the one without ANSI escape codes in it).
+    if (compiler.errorsPlain.isNotEmpty) {
       // TODO(sigmund): the compiler prints errors to the console, so we
       // shouldn't print those messages again here.
-      result = new CompilationResult.errors(compiler.errors, null);
+      result = new CompilationResult.errors(compiler.errorsPlain, null);
     } else {
       Component component = createExpressionEvaluationComponent(procedure);
       result = new CompilationResult.ok(serializeComponent(component));
@@ -811,7 +822,8 @@
       // resolve it against the working directory.
       packagesUri = Uri.directory(workingDirectory).resolveUri(packagesUri);
     }
-    final List<String> errors = <String>[];
+    final List<String> errorsPlain = <String>[];
+    final List<String> errorsColorized = <String>[];
     var options = setupCompilerOptions(
         fileSystem,
         platformKernelPath,
@@ -819,7 +831,8 @@
         nullSafety,
         experimentalFlags,
         packagesUri,
-        errors,
+        errorsPlain,
+        errorsColorized,
         invocationModes,
         verbosityLevel);
 
@@ -872,14 +885,17 @@
     CompilerResult compilerResult = await compiler.compile(script);
     Set<Library> loadedLibraries = compilerResult.loadedLibraries;
 
-    if (compiler.errors.isNotEmpty) {
+    assert(compiler.errorsPlain.length == compiler.errorsColorized.length);
+    final List<String> errors =
+        enableColors ? compiler.errorsColorized : compiler.errorsPlain;
+    if (errors.isNotEmpty) {
       if (compilerResult.component != null) {
         result = new CompilationResult.errors(
-            compiler.errors,
+            errors,
             serializeComponent(compilerResult.component,
                 filter: (lib) => !loadedLibraries.contains(lib)));
       } else {
-        result = new CompilationResult.errors(compiler.errors, null);
+        result = new CompilationResult.errors(errors, null);
       }
     } else {
       // We serialize the component excluding vm_platform.dill because the VM has