Merge pull request #27 from dart-lang/highlight-crlf

Produce better highlights for Windows newlines
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 74ab342..489f01a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@
 * Produce better source span highlights for multi-line spans that cover the
   entire last line of the span, including the newline.
 
+* Produce better source span highlights for spans that contain Windows-style
+  newlines.
+
 # 1.5.0
 
 * Improve the output of `SourceSpan.highlight()` and `SourceSpan.message()`:
diff --git a/lib/src/highlighter.dart b/lib/src/highlighter.dart
index 97d696d..3ed1749 100644
--- a/lib/src/highlighter.dart
+++ b/lib/src/highlighter.dart
@@ -58,66 +58,99 @@
     if (color == true) color = colors.RED;
     if (color == false) color = null;
 
-    // Normalize [span] to ensure that it's a [SourceSpanWithContext] whose
-    // context actually contains its text at the expected column. If it's not,
-    // adjust the start and end locations' line and column fields so that the
-    // highlighter can assume they match up with the context.
-    SourceSpanWithContext newSpan;
-    if (span is SourceSpanWithContext &&
-        findLineStart(span.context, span.text, span.start.column) != null) {
-      newSpan = span;
-    } else {
-      newSpan = new SourceSpanWithContext(
-          new SourceLocation(span.start.offset,
-              sourceUrl: span.sourceUrl, line: 0, column: 0),
-          new SourceLocation(span.end.offset,
-              sourceUrl: span.sourceUrl,
-              line: countCodeUnits(span.text, $lf),
-              column: _lastColumn(span.text)),
-          span.text,
-          span.text);
-    }
-
-    // Normalize [span] to remove a trailing newline from `span.context`. If
-    // necessary, also adjust `span.end` so that it doesn't point past where the
-    // trailing newline used to be.
-    if (newSpan.context.endsWith("\n")) {
-      var context = newSpan.context.substring(0, newSpan.context.length - 1);
-
-      var text = newSpan.text;
-      var start = newSpan.start;
-      var end = newSpan.end;
-      if (newSpan.text.endsWith("\n") && _isTextAtEndOfContext(newSpan)) {
-        text = newSpan.text.substring(0, newSpan.text.length - 1);
-        end = new SourceLocation(newSpan.end.offset - 1,
-            sourceUrl: newSpan.sourceUrl,
-            line: newSpan.end.line - 1,
-            column: _lastColumn(text));
-        start =
-            newSpan.start.offset == newSpan.end.offset ? end : newSpan.start;
-      }
-      newSpan = new SourceSpanWithContext(start, end, text, context);
-    }
-
-    // Normalize [span] so that the end location is at the end of a line, rather
-    // than on the beginning of the next line.
-    if (newSpan.end.column == 0 && newSpan.end.line != newSpan.start.line) {
-      assert(newSpan.text.endsWith("\n"));
-
-      var text = newSpan.text.substring(0, newSpan.text.length - 1);
-      newSpan = new SourceSpanWithContext(
-          newSpan.start,
-          new SourceLocation(span.end.offset - 1,
-              sourceUrl: span.sourceUrl,
-              line: span.end.line - 1,
-              column: _lastColumn(text)),
-          text,
-          newSpan.context);
-    }
+    var newSpan = _normalizeContext(span);
+    newSpan = _normalizeNewlines(newSpan);
+    newSpan = _normalizeTrailingNewline(newSpan);
+    newSpan = _normalizeEndOfLine(newSpan);
 
     return new Highlighter._(newSpan, color);
   }
 
+  /// Normalizes [span] to ensure that it's a [SourceSpanWithContext] whose
+  /// context actually contains its text at the expected column.
+  ///
+  /// If it's not already a [SourceSpanWithContext], adjust the start and end
+  /// locations' line and column fields so that the highlighter can assume they
+  /// match up with the context.
+  static SourceSpanWithContext _normalizeContext(SourceSpan span) =>
+      span is SourceSpanWithContext &&
+              findLineStart(span.context, span.text, span.start.column) != null
+          ? span
+          : new SourceSpanWithContext(
+              new SourceLocation(span.start.offset,
+                  sourceUrl: span.sourceUrl, line: 0, column: 0),
+              new SourceLocation(span.end.offset,
+                  sourceUrl: span.sourceUrl,
+                  line: countCodeUnits(span.text, $lf),
+                  column: _lastColumn(span.text)),
+              span.text,
+              span.text);
+
+  /// Normalizes [span] to replace Windows-style newlines with Unix-style
+  /// newlines.
+  static SourceSpanWithContext _normalizeNewlines(SourceSpanWithContext span) {
+    var text = span.text;
+    if (!text.contains("\r\n")) return span;
+
+    var endOffset = span.end.offset;
+    for (var i = 0; i < text.length - 1; i++) {
+      if (text.codeUnitAt(i) == $cr && text.codeUnitAt(i + 1) == $lf) {
+        endOffset--;
+      }
+    }
+
+    return new SourceSpanWithContext(
+        span.start,
+        new SourceLocation(endOffset,
+            sourceUrl: span.sourceUrl,
+            line: span.end.line,
+            column: span.end.column),
+        text.replaceAll("\r\n", "\n"),
+        span.context.replaceAll("\r\n", "\n"));
+  }
+
+  /// Normalizes [span] to remove a trailing newline from `span.context`.
+  ///
+  /// If necessary, also adjust `span.end` so that it doesn't point past where
+  /// the trailing newline used to be.
+  static SourceSpanWithContext _normalizeTrailingNewline(
+      SourceSpanWithContext span) {
+    if (!span.context.endsWith("\n")) return span;
+
+    var context = span.context.substring(0, span.context.length - 1);
+    var text = span.text;
+    var start = span.start;
+    var end = span.end;
+    if (span.text.endsWith("\n") && _isTextAtEndOfContext(span)) {
+      text = span.text.substring(0, span.text.length - 1);
+      end = new SourceLocation(span.end.offset - 1,
+          sourceUrl: span.sourceUrl,
+          line: span.end.line - 1,
+          column: _lastColumn(text));
+      start = span.start.offset == span.end.offset ? end : span.start;
+    }
+    return new SourceSpanWithContext(start, end, text, context);
+  }
+
+  /// Normalizes [span] so that the end location is at the end of a line, rather
+  /// than on the beginning of the next line.
+  static SourceSpanWithContext _normalizeEndOfLine(SourceSpanWithContext span) {
+    if (span.end.column != 0) return span;
+    if (span.end.line == span.start.line) return span;
+
+    assert(span.text.endsWith("\n"));
+
+    var text = span.text.substring(0, span.text.length - 1);
+    return new SourceSpanWithContext(
+        span.start,
+        new SourceLocation(span.end.offset - 1,
+            sourceUrl: span.sourceUrl,
+            line: span.end.line - 1,
+            column: _lastColumn(text)),
+        text,
+        span.context);
+  }
+
   /// Returns the (0-based) column number of the last column of the last line in [text].
   static int _lastColumn(String text) =>
       text.length - text.lastIndexOf("\n") + 1;
@@ -147,7 +180,7 @@
     // on, write those first.
     var lineStart =
         findLineStart(_span.context, _span.text, _span.start.column);
-    assert(lineStart != null); // enforced by [new Highlighter]
+    assert(lineStart != null); // enforced by [_normalizeContext]
 
     var context = _span.context;
     if (lineStart > 0) {
diff --git a/pubspec.yaml b/pubspec.yaml
index 13fa648..fc5ba2b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_span
-version: 1.5.1-dev
+version: 1.5.1
 
 description: A library for identifying source spans and locations.
 author: Dart Team <misc@dartlang.org>
diff --git a/test/highlight_test.dart b/test/highlight_test.dart
index e2536ba..b9e7ded 100644
--- a/test/highlight_test.dart
+++ b/test/highlight_test.dart
@@ -175,6 +175,21 @@
   '"""));
     });
 
+    test("highlights the full last line with a trailing Windows newline", () {
+      var file = new SourceFile.fromString("""
+foo bar baz\r
+whiz bang boom\r
+zip zap zop\r
+""");
+
+      expect(file.span(4, 29).highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | \\ whiz bang boom
+  '"""));
+    });
+
     test("highlights the full last line at the end of the file", () {
       expect(file.span(4, 39).highlight(), equals("""
   ,
@@ -186,8 +201,8 @@
     });
 
     test(
-        "highlights the full last line at the end of the file with no trailing"
-        " newline", () {
+        "highlights the full last line at the end of the file with no trailing "
+        "newline", () {
       var file = new SourceFile.fromString("""
 foo bar baz
 whiz bang boom