Merge pull request #25 from dart-lang/better-highlight
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53a700c..2248a39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# 1.5.0
+
+* Improve the output of `SourceSpan.highlight()` and `SourceSpan.message()`:
+
+ * They now include line numbers.
+ * They will now print every line of a multiline span.
+ * They will now use Unicode box-drawing characters by default (this can be
+ controlled using [`term_glyph.ascii`][]).
+
+[`term_glyph.ascii`]: https://pub.dartlang.org/documentation/term_glyph/latest/term_glyph/ascii.html
+
# 1.4.1
* Set max SDK version to `<3.0.0`, and adjust other dependencies.
diff --git a/lib/src/colors.dart b/lib/src/colors.dart
index b9afab0..2931eea 100644
--- a/lib/src/colors.dart
+++ b/lib/src/colors.dart
@@ -7,4 +7,6 @@
const String YELLOW = '\u001b[33m';
+const String BLUE = '\u001b[34m';
+
const String NONE = '\u001b[0m';
diff --git a/lib/src/highlighter.dart b/lib/src/highlighter.dart
new file mode 100644
index 0000000..a05412a
--- /dev/null
+++ b/lib/src/highlighter.dart
@@ -0,0 +1,362 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'package:charcode/charcode.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import 'colors.dart' as colors;
+import 'location.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A class for writing a chunk of text with a particular span highlighted.
+class Highlighter {
+ /// The span to highlight.
+ final SourceSpanWithContext _span;
+
+ /// The color to highlight [_span] within its context, or `null` if the span
+ /// should not be colored.
+ final String _color;
+
+ /// Whether [_span] covers multiple lines.
+ final bool _multiline;
+
+ /// The number of characters before the bar in the sidebar.
+ final int _paddingBeforeSidebar;
+
+ // The number of characters between the bar in the sidebar and the text
+ // being highlighted.
+ int get _paddingAfterSidebar =>
+ // This is just a space for a single-line span, but for a multi-line span
+ // needs to accommodate " | ".
+ _multiline ? 3 : 1;
+
+ /// The buffer to which to write the result.
+ final _buffer = new StringBuffer();
+
+ /// The number of spaces to render for hard tabs that appear in `_span.text`.
+ ///
+ /// We don't want to render raw tabs, because they'll mess up our character
+ /// alignment.
+ static const _spacesPerTab = 4;
+
+ /// Creats a [Highlighter] that will return a message associated with [span]
+ /// when [write] is called.
+ ///
+ /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+ /// it indicates an [ANSI terminal color
+ /// escape](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) that should
+ /// be used to highlight the span's text (for example, `"\u001b[31m"` will
+ /// color red). If it's `true`, it indicates that the text should be
+ /// highlighted using the default color. If it's `false` or `null`, it
+ /// indicates that the text shouldn't be highlighted.
+ factory Highlighter(SourceSpan span, {color}) {
+ 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);
+ }
+
+ return new Highlighter._(newSpan, color);
+ }
+
+ /// 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 whether [span]'s text runs all the way to the end of its context.
+ static bool _isTextAtEndOfContext(SourceSpanWithContext span) =>
+ findLineStart(span.context, span.text, span.start.column) +
+ span.start.column +
+ span.length ==
+ span.context.length;
+
+ Highlighter._(this._span, this._color)
+ : _multiline = _span.start.line != _span.end.line,
+ // In a purely mathematical world, floor(log10(n)) would give the number of
+ // digits in n, but floating point errors render that unreliable in
+ // practice.
+ _paddingBeforeSidebar = _span.end.line.toString().length + 1;
+
+ /// Returns the highlighted span text.
+ ///
+ /// This method should only be called once.
+ String highlight() {
+ _writeSidebar(end: glyph.downEnd);
+ _buffer.writeln();
+
+ // If [_span.context] contains lines prior to the one [_span.text] appears
+ // on, write those first.
+ var lineStart =
+ findLineStart(_span.context, _span.text, _span.start.column);
+ assert(lineStart != null); // enforced by [new Highlighter]
+
+ var context = _span.context;
+ if (lineStart > 0) {
+ // Take a substring to one character *before* [lineStart] because
+ // [findLineStart] is guaranteed to return a position immediately after a
+ // newline. Including that newline would add an extra empty line to the
+ // end of [lines].
+ var lines = context.substring(0, lineStart - 1).split("\n");
+ var lineNumber = _span.start.line - lines.length;
+ for (var line in lines) {
+ _writeSidebar(line: lineNumber);
+ _buffer.write(" " * _paddingAfterSidebar);
+ _writeText(line);
+ _buffer.writeln();
+ lineNumber++;
+ }
+ context = context.substring(lineStart);
+ }
+
+ var lines = context.split("\n");
+
+ // Trim a trailing newline so we don't add an empty line to the end of the
+ // highlight.
+ if (lines.last.isEmpty && lines.length > 1) lines.removeLast();
+
+ _writeFirstLine(lines.first);
+ var lastLineIndex = _span.end.line - _span.start.line;
+ if (_multiline) {
+ _writeIntermediateLines(lines.skip(1).take(lastLineIndex - 1));
+ _writeLastLine(lines[lastLineIndex]);
+ }
+ _writeTrailingLines(lines.skip(lastLineIndex + 1));
+
+ _writeSidebar(end: glyph.upEnd);
+
+ return _buffer.toString();
+ }
+
+ // Writes the first (and possibly only) line highlighted by the span.
+ void _writeFirstLine(String line) {
+ _writeSidebar(line: _span.start.line);
+
+ var startColumn = math.min(_span.start.column, line.length);
+ var endColumn = math.min(
+ startColumn + _span.end.offset - _span.start.offset, line.length);
+ var textBefore = line.substring(0, startColumn);
+
+ // If the span covers the entire first line other than initial whitespace,
+ // don't bother pointing out exactly where it begins.
+ if (_multiline && _isOnlyWhitespace(textBefore)) {
+ _buffer.write(" ");
+ _colorize(() {
+ _buffer.write(glyph.glyphOrAscii("┌", "/"));
+ _buffer.write(" ");
+ _writeText(line);
+ });
+ _buffer.writeln();
+ return;
+ }
+
+ _buffer.write(" " * _paddingAfterSidebar);
+ _writeText(textBefore);
+ var textInside = line.substring(startColumn, endColumn);
+ _colorize(() => _writeText(textInside));
+ _writeText(line.substring(endColumn));
+ _buffer.writeln();
+
+ // Adjust the start and end column to account for any tabs that were
+ // converted to spaces.
+ var tabsBefore = _countTabs(textBefore);
+ var tabsInside = _countTabs(textInside);
+ startColumn = startColumn + tabsBefore * (_spacesPerTab - 1);
+ endColumn = endColumn + (tabsBefore + tabsInside) * (_spacesPerTab - 1);
+
+ // Write the highlight for the first line. This is a series of carets for a
+ // single-line span, and a pointer to the beginning of a multi-line span.
+ _writeSidebar();
+ if (_multiline) {
+ _buffer.write(" ");
+ _colorize(() {
+ _buffer.write(glyph.topLeftCorner);
+ _buffer.write(glyph.horizontalLine * (startColumn + 1));
+ _buffer.write("^");
+ });
+ } else {
+ _buffer.write(" " * (startColumn + 1));
+ _colorize(
+ () => _buffer.write("^" * math.max(endColumn - startColumn, 1)));
+ }
+ _buffer.writeln();
+ }
+
+ /// Writes the lines between the first and last lines highlighted by the span.
+ void _writeIntermediateLines(Iterable<String> lines) {
+ assert(_multiline);
+
+ // +1 because the first line was already written.
+ var lineNumber = _span.start.line + 1;
+ for (var line in lines) {
+ _writeSidebar(line: lineNumber);
+
+ _buffer.write(" ");
+ _colorize(() {
+ _buffer.write(glyph.verticalLine);
+ _buffer.write(" ");
+ _writeText(line);
+ });
+ _buffer.writeln();
+
+ lineNumber++;
+ }
+ }
+
+ // Writes the last line highlighted by the span.
+ void _writeLastLine(String line) {
+ assert(_multiline);
+
+ _writeSidebar(line: _span.end.line);
+
+ var endColumn = math.min(_span.end.column, line.length);
+
+ // If the span covers the entire last line, don't bother pointing out
+ // exactly where it ends.
+ if (_multiline && endColumn == line.length) {
+ _buffer.write(" ");
+ _colorize(() {
+ _buffer.write(glyph.glyphOrAscii("└", "\\"));
+ _buffer.write(" ");
+ _writeText(line);
+ });
+ _buffer.writeln();
+ return;
+ }
+
+ _buffer.write(" ");
+ var textInside = line.substring(0, endColumn);
+ _colorize(() {
+ _buffer.write(glyph.verticalLine);
+ _buffer.write(" ");
+ _writeText(textInside);
+ });
+ _writeText(line.substring(endColumn));
+ _buffer.writeln();
+
+ // Adjust the end column to account for any tabs that were converted to
+ // spaces.
+ var tabsInside = _countTabs(textInside);
+ endColumn = endColumn + tabsInside * (_spacesPerTab - 1);
+
+ // Write the highlight for the final line, which is an arrow pointing to the
+ // end of the span.
+ _writeSidebar();
+ _buffer.write(" ");
+ _colorize(() {
+ _buffer.write(glyph.bottomLeftCorner);
+ _buffer.write(glyph.horizontalLine * endColumn);
+ _buffer.write("^");
+ });
+ _buffer.writeln();
+ }
+
+ /// Writes lines that appear in the context string but come after the span.
+ void _writeTrailingLines(Iterable<String> lines) {
+ // +1 because this comes after any lines covered by the span.
+ var lineNumber = _span.end.line + 1;
+ for (var line in lines) {
+ _writeSidebar(line: lineNumber);
+ _buffer.write(" " * _paddingAfterSidebar);
+ _writeText(line);
+ _buffer.writeln();
+ lineNumber++;
+ }
+ }
+
+ /// Writes a snippet from the source text, converting hard tab characters into
+ /// plain indentation.
+ void _writeText(String text) {
+ for (var char in text.codeUnits) {
+ if (char == $tab) {
+ _buffer.write(" " * _spacesPerTab);
+ } else {
+ _buffer.writeCharCode(char);
+ }
+ }
+ }
+
+ // Writes a sidebar to [buffer] that includes [line] as the line number if
+ // given and writes [end] at the end (defaults to [glyphs.verticalLine]).
+ void _writeSidebar({int line, String end}) {
+ _colorize(() {
+ if (line != null) {
+ // Add 1 to line to convert from computer-friendly 0-indexed line
+ // numbers to human-friendly 1-indexed line numbers.
+ _buffer.write((line + 1).toString().padRight(_paddingBeforeSidebar));
+ } else {
+ _buffer.write(" " * _paddingBeforeSidebar);
+ }
+ _buffer.write(end ?? glyph.verticalLine);
+ }, color: colors.BLUE);
+ }
+
+ /// Returns the number of hard tabs in [text].
+ int _countTabs(String text) {
+ var count = 0;
+ for (var char in text.codeUnits) {
+ if (char == $tab) count++;
+ }
+ return count;
+ }
+
+ /// Returns whether [text] contains only space or tab characters.
+ bool _isOnlyWhitespace(String text) {
+ for (var char in text.codeUnits) {
+ if (char != $space && char != $tab) return false;
+ }
+ return true;
+ }
+
+ /// Colors all text written to [_buffer] during [callback], if colorization is
+ /// enabled.
+ ///
+ /// If [color] is passed, it's used as the color; otherwise, [_color] is used.
+ void _colorize(void callback(), {String color}) {
+ if (_color != null) _buffer.write(color ?? _color);
+ callback();
+ if (_color != null) _buffer.write(colors.NONE);
+ }
+}
diff --git a/lib/src/span.dart b/lib/src/span.dart
index 19655a5..57ffe79 100644
--- a/lib/src/span.dart
+++ b/lib/src/span.dart
@@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
import 'location.dart';
import 'span_mixin.dart';
@@ -54,6 +56,10 @@
/// color red). If it's `true`, it indicates that the text should be
/// highlighted using the default color. If it's `false` or `null`, it
/// indicates that the text shouldn't be highlighted.
+ ///
+ /// This uses the full range of Unicode characters to highlight the source
+ /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
+ /// characters if it's `true`.
String message(String message, {color});
/// Prints the text associated with this span in a user-friendly way.
@@ -69,6 +75,10 @@
/// color red). If it's `true`, it indicates that the text should be
/// highlighted using the default color. If it's `false` or `null`, it
/// indicates that the text shouldn't be highlighted.
+ ///
+ /// This uses the full range of Unicode characters to highlight the source
+ /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
+ /// characters if it's `true`.
String highlight({color});
}
diff --git a/lib/src/span_mixin.dart b/lib/src/span_mixin.dart
index 1f7799d..d8ac8f2 100644
--- a/lib/src/span_mixin.dart
+++ b/lib/src/span_mixin.dart
@@ -2,12 +2,9 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-import 'dart:math' as math;
-
-import 'package:charcode/charcode.dart';
import 'package:path/path.dart' as p;
-import 'colors.dart' as colors;
+import 'highlighter.dart';
import 'span.dart';
import 'span_with_context.dart';
import 'utils.dart';
@@ -63,54 +60,8 @@
}
String highlight({color}) {
- if (color == true) color = colors.RED;
- if (color == false) color = null;
-
- var column = start.column;
- var buffer = new StringBuffer();
- String textLine;
- if (this is SourceSpanWithContext) {
- var context = (this as SourceSpanWithContext).context;
- var lineStart = findLineStart(context, text, column);
- if (lineStart != null && lineStart > 0) {
- buffer.write(context.substring(0, lineStart));
- context = context.substring(lineStart);
- }
- var endIndex = context.indexOf('\n');
- textLine = endIndex == -1 ? context : context.substring(0, endIndex + 1);
- column = math.min(column, textLine.length);
- } else if (length == 0) {
- return "";
- } else {
- textLine = text.split("\n").first;
- column = 0;
- }
-
- var toColumn =
- math.min(column + end.offset - start.offset, textLine.length);
- if (color != null) {
- buffer.write(textLine.substring(0, column));
- buffer.write(color);
- buffer.write(textLine.substring(column, toColumn));
- buffer.write(colors.NONE);
- buffer.write(textLine.substring(toColumn));
- } else {
- buffer.write(textLine);
- }
- if (!textLine.endsWith('\n')) buffer.write('\n');
-
- for (var i = 0; i < column; i++) {
- if (textLine.codeUnitAt(i) == $tab) {
- buffer.writeCharCode($tab);
- } else {
- buffer.writeCharCode($space);
- }
- }
-
- if (color != null) buffer.write(color);
- buffer.write('^' * math.max(toColumn - column, 1));
- if (color != null) buffer.write(colors.NONE);
- return buffer.toString();
+ if (this is! SourceSpanWithContext && this.length == 0) return "";
+ return new Highlighter(this, color: color).highlight();
}
bool operator ==(other) =>
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 6938547..228b240 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -12,19 +12,43 @@
Comparable max(Comparable obj1, Comparable obj2) =>
obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+/// Returns the number of instances of [codeUnit] in [string].
+int countCodeUnits(String string, int codeUnit) {
+ var count = 0;
+ for (var codeUnitToCheck in string.codeUnits) {
+ if (codeUnitToCheck == codeUnit) count++;
+ }
+ return count;
+}
+
/// Finds a line in [context] containing [text] at the specified [column].
///
/// Returns the index in [context] where that line begins, or null if none
/// exists.
int findLineStart(String context, String text, int column) {
- var isEmpty = text == '';
+ // If the text is empty, we just want to find the first line that has at least
+ // [column] characters.
+ if (text.isEmpty) {
+ var beginningOfLine = 0;
+ while (true) {
+ var index = context.indexOf("\n", beginningOfLine);
+ if (index == -1) {
+ return context.length - beginningOfLine >= column
+ ? beginningOfLine
+ : null;
+ }
+
+ if (index - beginningOfLine >= column) return beginningOfLine;
+ beginningOfLine = index + 1;
+ }
+ }
+
var index = context.indexOf(text);
while (index != -1) {
- var lineStart = context.lastIndexOf('\n', index) + 1;
+ // Start looking before [index] in case [text] starts with a newline.
+ var lineStart = index == 0 ? 0 : context.lastIndexOf('\n', index - 1) + 1;
var textColumn = index - lineStart;
- if (column == textColumn || (isEmpty && column == textColumn + 1)) {
- return lineStart;
- }
+ if (column == textColumn) return lineStart;
index = context.indexOf(text, index + 1);
}
return null;
diff --git a/pubspec.yaml b/pubspec.yaml
index 1d53210..ebd3a72 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: source_span
-version: 1.4.1
+version: 1.5.0
description: A library for identifying source spans and locations.
author: Dart Team <misc@dartlang.org>
@@ -11,6 +11,7 @@
dependencies:
charcode: ^1.0.0
path: '>=1.2.0 <2.0.0'
+ term_glyph: ^1.0.0
dev_dependencies:
test: '>=0.12.0 <2.0.0'
diff --git a/test/highlight_test.dart b/test/highlight_test.dart
index ac8334b..19f8ca8 100644
--- a/test/highlight_test.dart
+++ b/test/highlight_test.dart
@@ -2,11 +2,23 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'package:term_glyph/term_glyph.dart' as glyph;
import 'package:test/test.dart';
+
import 'package:source_span/source_span.dart';
import 'package:source_span/src/colors.dart' as colors;
main() {
+ bool oldAscii;
+ setUpAll(() {
+ oldAscii = glyph.ascii;
+ glyph.ascii = true;
+ });
+
+ tearDownAll(() {
+ glyph.ascii = oldAscii;
+ });
+
var file;
setUp(() {
file = new SourceFile.fromString("""
@@ -18,101 +30,341 @@
test("points to the span in the source", () {
expect(file.span(4, 7).highlight(), equals("""
-foo bar baz
- ^^^"""));
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
});
test("gracefully handles a missing source URL", () {
var span = new SourceFile.fromString("foo bar baz").span(4, 7);
expect(span.highlight(), equals("""
-foo bar baz
- ^^^"""));
- });
-
- test("highlights the first line of a multiline span", () {
- expect(file.span(4, 20).highlight(), equals("""
-foo bar baz
- ^^^^^^^^"""));
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
});
test("works for a point span", () {
expect(file.location(4).pointSpan().highlight(), equals("""
-foo bar baz
- ^"""));
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
});
test("works for a point span at the end of a line", () {
expect(file.location(11).pointSpan().highlight(), equals("""
-foo bar baz
- ^"""));
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
});
test("works for a point span at the end of the file", () {
expect(file.location(38).pointSpan().highlight(), equals("""
-zip zap zop
- ^"""));
+ ,
+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(11).pointSpan().highlight(), equals("""
-zip zap zop
- ^"""));
+ ,
+1 | zip zap zop
+ | ^
+ '"""));
});
test("works for a point span in an empty file", () {
expect(new SourceFile.fromString("").location(0).pointSpan().highlight(),
equals("""
-
-^"""));
+ ,
+1 |
+ | ^
+ '"""));
});
test("works for a single-line file without a newline", () {
expect(
new SourceFile.fromString("foo bar").span(0, 7).highlight(), equals("""
-foo bar
-^^^^^^^"""));
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
});
- test("emits tabs for tabs", () {
- expect(new SourceFile.fromString(" \t \t\tfoo bar").span(5, 8).highlight(),
- equals("""
- \t \t\tfoo bar
- \t \t\t^^^"""));
+ group("with a multiline span", () {
+ test("highlights the middle of the first and last lines", () {
+ expect(file.span(4, 34).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("works when it begins at the end of a line", () {
+ expect(file.span(11, 34).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,------------^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("works when it ends at the beginning of a line", () {
+ expect(file.span(4, 28).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-^
+ '"""));
+ });
+
+ test("highlights the full first line", () {
+ expect(file.span(0, 34).highlight(), equals("""
+ ,
+1 | / foo bar baz
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("highlights the full first line even if it's indented", () {
+ var file = new SourceFile.fromString("""
+ foo bar baz
+ whiz bang boom
+ zip zap zop
+""");
+
+ expect(file.span(2, 38).highlight(), equals("""
+ ,
+1 | / foo bar baz
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("highlights the full last line", () {
+ expect(file.span(4, 26).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("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | \\ zip zap zop
+ '"""));
+ });
+
+ test("highlights the full last line with no trailing newline", () {
+ var file = new SourceFile.fromString("""
+foo bar baz
+whiz bang boom
+zip zap zop""");
+
+ expect(file.span(4, 38).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | \\ zip zap zop
+ '"""));
+ });
});
- test("supports lines of preceding context", () {
+ group("prints tabs as spaces", () {
+ group("in a single-line span", () {
+ test("before the highlighted section", () {
+ var span = new SourceFile.fromString("foo\tbar baz").span(4, 7);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ test("within the highlighted section", () {
+ var span = new SourceFile.fromString("foo bar\tbaz bang").span(4, 11);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz bang
+ | ^^^^^^^^^^
+ '"""));
+ });
+
+ test("after the highlighted section", () {
+ var span = new SourceFile.fromString("foo bar\tbaz").span(4, 7);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+ });
+
+ group("in a multi-line span", () {
+ test("before the highlighted section", () {
+ var span = new SourceFile.fromString("""
+foo\tbar baz
+whiz bang boom
+""").span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,--------^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+
+ test("within the first highlighted line", () {
+ var span = new SourceFile.fromString("""
+foo bar\tbaz
+whiz bang boom
+""").span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+
+ test("within a middle highlighted line", () {
+ var span = new SourceFile.fromString("""
+foo bar baz
+whiz\tbang boom
+zip zap zop
+""").span(4, 34);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("within the last highlighted line", () {
+ var span = new SourceFile.fromString("""
+foo bar baz
+whiz\tbang boom
+""").span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '------------^
+ '"""));
+ });
+
+ test("after the highlighted section", () {
+ var span = new SourceFile.fromString("""
+foo bar baz
+whiz bang\tboom
+""").span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+ });
+ });
+
+ test("supports lines of preceding and following context", () {
var span = new SourceSpanWithContext(
- new SourceLocation(5, line: 3, column: 5, sourceUrl: "foo.dart"),
- new SourceLocation(12, line: 3, column: 12, sourceUrl: "foo.dart"),
+ new SourceLocation(5, line: 2, column: 5, sourceUrl: "foo.dart"),
+ new SourceLocation(12, line: 2, column: 12, sourceUrl: "foo.dart"),
"foo bar",
"previous\nlines\n-----foo bar-----\nfollowing line\n");
expect(span.highlight(color: colors.YELLOW), equals("""
-previous
-lines
------${colors.YELLOW}foo bar${colors.NONE}-----
- ${colors.YELLOW}^^^^^^^${colors.NONE}"""));
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} previous
+${colors.BLUE}2 |${colors.NONE} lines
+${colors.BLUE}3 |${colors.NONE} -----${colors.YELLOW}foo bar${colors.NONE}-----
+${colors.BLUE} |${colors.NONE} ${colors.YELLOW}^^^^^^^${colors.NONE}
+${colors.BLUE}4 |${colors.NONE} following line
+${colors.BLUE} '${colors.NONE}"""));
});
group("colors", () {
test("doesn't colorize if color is false", () {
expect(file.span(4, 7).highlight(color: false), equals("""
-foo bar baz
- ^^^"""));
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
});
test("colorizes if color is true", () {
expect(file.span(4, 7).highlight(color: true), equals("""
-foo ${colors.RED}bar${colors.NONE} baz
- ${colors.RED}^^^${colors.NONE}"""));
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} foo ${colors.RED}bar${colors.NONE} baz
+${colors.BLUE} |${colors.NONE} ${colors.RED}^^^${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
});
test("uses the given color if it's passed", () {
expect(file.span(4, 7).highlight(color: colors.YELLOW), equals("""
-foo ${colors.YELLOW}bar${colors.NONE} baz
- ${colors.YELLOW}^^^${colors.NONE}"""));
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} foo ${colors.YELLOW}bar${colors.NONE} baz
+${colors.BLUE} |${colors.NONE} ${colors.YELLOW}^^^${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
+ });
+
+ test("colorizes a multiline span", () {
+ expect(file.span(4, 34).highlight(color: true), equals("""
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} foo ${colors.RED}bar baz${colors.NONE}
+${colors.BLUE} |${colors.NONE} ${colors.RED},-----^${colors.NONE}
+${colors.BLUE}2 |${colors.NONE} ${colors.RED}| whiz bang boom${colors.NONE}
+${colors.BLUE}3 |${colors.NONE} ${colors.RED}| zip zap${colors.NONE} zop
+${colors.BLUE} |${colors.NONE} ${colors.RED}'-------^${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
+ });
+
+ test("colorizes a multiline span that highlights full lines", () {
+ expect(file.span(0, 39).highlight(color: true), equals("""
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} ${colors.RED}/ foo bar baz${colors.NONE}
+${colors.BLUE}2 |${colors.NONE} ${colors.RED}| whiz bang boom${colors.NONE}
+${colors.BLUE}3 |${colors.NONE} ${colors.RED}\\ zip zap zop${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
});
});
}
diff --git a/test/span_test.dart b/test/span_test.dart
index b7637cf..9989516 100644
--- a/test/span_test.dart
+++ b/test/span_test.dart
@@ -3,10 +3,22 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:test/test.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
import 'package:source_span/source_span.dart';
import 'package:source_span/src/colors.dart' as colors;
main() {
+ bool oldAscii;
+ setUpAll(() {
+ oldAscii = glyph.ascii;
+ glyph.ascii = true;
+ });
+
+ tearDownAll(() {
+ glyph.ascii = oldAscii;
+ });
+
var span;
setUp(() {
span = new SourceSpan(new SourceLocation(5, sourceUrl: "foo.dart"),
@@ -181,8 +193,10 @@
test("prints the text being described", () {
expect(span.message("oh no"), equals("""
line 1, column 6 of foo.dart: oh no
-foo bar
-^^^^^^^"""));
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
});
test("gracefully handles a missing source URL", () {
@@ -191,8 +205,10 @@
expect(span.message("oh no"), equalsIgnoringWhitespace("""
line 1, column 6: oh no
-foo bar
-^^^^^^^"""));
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
});
test("gracefully handles empty text", () {
@@ -205,22 +221,28 @@
test("doesn't colorize if color is false", () {
expect(span.message("oh no", color: false), equals("""
line 1, column 6 of foo.dart: oh no
-foo bar
-^^^^^^^"""));
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
});
test("colorizes if color is true", () {
expect(span.message("oh no", color: true), equals("""
line 1, column 6 of foo.dart: oh no
-${colors.RED}foo bar${colors.NONE}
-${colors.RED}^^^^^^^${colors.NONE}"""));
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} ${colors.RED}foo bar${colors.NONE}
+${colors.BLUE} |${colors.NONE} ${colors.RED}^^^^^^^${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
});
test("uses the given color if it's passed", () {
expect(span.message("oh no", color: colors.YELLOW), equals("""
line 1, column 6 of foo.dart: oh no
-${colors.YELLOW}foo bar${colors.NONE}
-${colors.YELLOW}^^^^^^^${colors.NONE}"""));
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} ${colors.YELLOW}foo bar${colors.NONE}
+${colors.BLUE} |${colors.NONE} ${colors.YELLOW}^^^^^^^${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
});
test("with context, underlines the right column", () {
@@ -232,8 +254,10 @@
expect(spanWithContext.message("oh no", color: colors.YELLOW), equals("""
line 1, column 6 of foo.dart: oh no
------${colors.YELLOW}foo bar${colors.NONE}-----
- ${colors.YELLOW}^^^^^^^${colors.NONE}"""));
+${colors.BLUE} ,${colors.NONE}
+${colors.BLUE}1 |${colors.NONE} -----${colors.YELLOW}foo bar${colors.NONE}-----
+${colors.BLUE} |${colors.NONE} ${colors.YELLOW}^^^^^^^${colors.NONE}
+${colors.BLUE} '${colors.NONE}"""));
});
});
diff --git a/test/utils_test.dart b/test/utils_test.dart
index 2a86cc0..a8146e3 100644
--- a/test/utils_test.dart
+++ b/test/utils_test.dart
@@ -32,12 +32,23 @@
expect(index, 4);
});
+ test('empty text in empty context', () {
+ var index = findLineStart('', '', 0);
+ expect(index, 0);
+ });
+
test('found on the first line', () {
var context = '0\n2\n45\n';
var index = findLineStart(context, '0', 0);
expect(index, 0);
});
+ test('finds text that starts with a newline', () {
+ var context = '0\n2\n45\n';
+ var index = findLineStart(context, '\n2', 1);
+ expect(index, 0);
+ });
+
test('not found', () {
var context = '0\n2\n45\n';
var index = findLineStart(context, '0', 1);