Fix a bug when highlighting an end-of-file point span (#30)


diff --git a/CHANGELOG.md b/CHANGELOG.md
index afba8b7..6e79fbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 1.5.3
+
+* Fix an edge case where `FileSpan.highlight()` would put the highlight
+  indicator in the wrong position when highlighting a point span after the end
+  of a file.
+
 # 1.5.2
 
 * `SourceFile.span()` now goes to the end of the file by default, rather than
diff --git a/lib/src/highlighter.dart b/lib/src/highlighter.dart
index f29a89d..8100f18 100644
--- a/lib/src/highlighter.dart
+++ b/lib/src/highlighter.dart
@@ -82,7 +82,7 @@
               new SourceLocation(span.end.offset,
                   sourceUrl: span.sourceUrl,
                   line: countCodeUnits(span.text, $lf),
-                  column: _lastColumn(span.text)),
+                  column: _lastLineLength(span.text)),
               span.text,
               span.text);
 
@@ -126,34 +126,55 @@
       end = new SourceLocation(span.end.offset - 1,
           sourceUrl: span.sourceUrl,
           line: span.end.line - 1,
-          column: _lastColumn(text));
+          column: _lastLineLength(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.
+  /// Normalizes [span] so that the end location is at the end of a line rather
+  /// than at 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"));
+    if (span.length == 0) {
+      if (span.end.offset == 0) return span;
 
-    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);
+      // If [span] is a point span with an empty context, there's no useful
+      // adjustment we can do.
+      if (span.context.isEmpty) return span;
+
+      var location = new SourceLocation(span.end.offset - 1,
+          sourceUrl: span.sourceUrl,
+          line: span.end.line - 1,
+          column: _lastLineLength(span.context));
+      return new SourceSpanWithContext(location, location, "", span.context);
+    } else {
+      if (span.end.line == span.start.line) return span;
+
+      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: _lastLineLength(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;
+  /// Returns the length of the last line in [text], whether or not it ends in a
+  /// newline.
+  static int _lastLineLength(String text) {
+    if (text.isEmpty) return 0;
+
+    // The "- 1" here avoids counting the newline itself.
+    return text.codeUnitAt(text.length - 1) == $lf
+        ? text.length - text.lastIndexOf("\n", text.length - 2) - 1
+        : text.length - text.lastIndexOf("\n") - 1;
+  }
 
   /// Returns whether [span]'s text runs all the way to the end of its context.
   static bool _isTextAtEndOfContext(SourceSpanWithContext span) =>
diff --git a/pubspec.yaml b/pubspec.yaml
index fd80bc8..c4b4ccf 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_span
-version: 1.5.2
+version: 1.5.3
 
 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 8418367..7832e51 100644
--- a/test/highlight_test.dart
+++ b/test/highlight_test.dart
@@ -53,6 +53,14 @@
   '"""));
   });
 
+  test("works for a point span at the beginning of the file", () {
+    expect(file.location(0).pointSpan().highlight(), equals("""
+  ,
+1 | foo bar baz
+  | ^
+  '"""));
+  });
+
   test("works for a point span at the end of a line", () {
     expect(file.location(11).pointSpan().highlight(), equals("""
   ,
@@ -69,9 +77,28 @@
   '"""));
   });
 
+  test("works for a point span after the end of the file", () {
+    expect(file.location(39).pointSpan().highlight(), equals("""
+  ,
+3 | zip zap zop
+  |            ^
+  '"""));
+  });
+
   test("works for a point span at the end of the file with no trailing newline",
       () {
     file = new SourceFile.fromString("zip zap zop");
+    expect(file.location(10).pointSpan().highlight(), equals("""
+  ,
+1 | zip zap zop
+  |           ^
+  '"""));
+  });
+
+  test(
+      "works for a point span after the end of the file with no trailing newline",
+      () {
+    file = new SourceFile.fromString("zip zap zop");
     expect(file.location(11).pointSpan().highlight(), equals("""
   ,
 1 | zip zap zop
@@ -157,7 +184,7 @@
   '"""));
     });
 
-    test("highlights the full last line if it's empty", () {
+    test("highlights the full first line if it's empty", () {
       var file = new SourceFile.fromString("""
 foo