Merge pull request #20 from bcko/patch-1
Update .gitignore to new `dart_tool` pub cache
diff --git a/.analysis_options b/.analysis_options
deleted file mode 100644
index a10d4c5..0000000
--- a/.analysis_options
+++ /dev/null
@@ -1,2 +0,0 @@
-analyzer:
- strong-mode: true
diff --git a/.travis.yml b/.travis.yml
index 44a0542..18267fc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@
dart:
- dev
- - stable
+ - 2.0.0
dart_task:
- test: --platform vm,chrome
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68eafaa..ddb4ff0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,54 @@
+# 1.5.5
+
+* Fix a bug where `FileSpan.highlight()` would crash for spans that covered a
+ trailing newline and a single additional empty line.
+
+# 1.5.4
+
+* `FileSpan.highlight()` now properly highlights point spans at the beginning of
+ lines.
+
+# 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
+ ending one character before the end of the file. This matches the documented
+ behavior.
+
+* `FileSpan.context` now includes the full line on which the span appears for
+ empty spans at the beginning and end of lines.
+
+* Fix an edge case where `FileSpan.highlight()` could crash when highlighting a
+ span that ended with an empty line.
+
+# 1.5.1
+
+* 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()`:
+
+ * 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.
+
# 1.4.0
* The `new SourceFile()` constructor is deprecated. This constructed a source
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/file.dart b/lib/src/file.dart
index 6154e13..27dae5d 100644
--- a/lib/src/file.dart
+++ b/lib/src/file.dart
@@ -88,7 +88,7 @@
///
/// If [end] isn't passed, it defaults to the end of the file.
FileSpan span(int start, [int end]) {
- if (end == null) end = length - 1;
+ if (end == null) end = length;
return new _FileSpan(this, start, end);
}
@@ -291,8 +291,39 @@
FileLocation get start => new FileLocation._(file, _start);
FileLocation get end => new FileLocation._(file, _end);
String get text => file.getText(_start, _end);
- String get context => file.getText(file.getOffset(start.line),
- end.line == file.lines - 1 ? null : file.getOffset(end.line + 1));
+
+ String get context {
+ var endLine = file.getLine(_end);
+ var endColumn = file.getColumn(_end);
+
+ int endOffset;
+ if (endColumn == 0 && endLine != 0) {
+ // If [end] is at the very beginning of the line, the span covers the
+ // previous newline, so we only want to include the previous line in the
+ // context...
+
+ if (length == 0) {
+ // ...unless this is a point span, in which case we want to include the
+ // next line (or the empty string if this is the end of the file).
+ return endLine == file.lines - 1
+ ? ""
+ : file.getText(
+ file.getOffset(endLine), file.getOffset(endLine + 1));
+ }
+
+ endOffset = _end;
+ } else if (endLine == file.lines - 1) {
+ // If the span covers the last line of the file, the context should go all
+ // the way to the end of the file.
+ endOffset = file.length;
+ } else {
+ // Otherwise, the context should cover the full line on which [end]
+ // appears.
+ endOffset = file.getOffset(endLine + 1);
+ }
+
+ return file.getText(file.getOffset(file.getLine(_start)), endOffset);
+ }
_FileSpan(this.file, this._start, this._end) {
if (_end < _start) {
diff --git a/lib/src/highlighter.dart b/lib/src/highlighter.dart
new file mode 100644
index 0000000..17a47bc
--- /dev/null
+++ b/lib/src/highlighter.dart
@@ -0,0 +1,427 @@
+// 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;
+
+ 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: _lastLineLength(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;
+
+ // If there's a full blank line on the end of [span.context], it's probably
+ // significant, so we shouldn't trim it.
+ if (span.text.endsWith("\n\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: _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 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;
+
+ 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 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.
+ if (text.codeUnitAt(text.length - 1) == $lf) {
+ return text.length == 1
+ ? 0
+ : text.length - text.lastIndexOf("\n", text.length - 2) - 1;
+ } else {
+ return 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 [_normalizeContext]
+
+ 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");
+
+ var lastLineIndex = _span.end.line - _span.start.line;
+ if (lines.last.isEmpty && lines.length > lastLineIndex + 1) {
+ // Trim a trailing newline so we don't add an empty line to the end of the
+ // highlight.
+ lines.removeLast();
+ }
+
+ _writeFirstLine(lines.first);
+ 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 599d668..57ffe79 100644
--- a/lib/src/span.dart
+++ b/lib/src/span.dart
@@ -2,28 +2,30 @@
// 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';
/// A class that describes a segment of source text.
abstract class SourceSpan implements Comparable<SourceSpan> {
/// The start location of this span.
- final SourceLocation start;
+ SourceLocation get start;
/// The end location of this span, exclusive.
- final SourceLocation end;
+ SourceLocation get end;
/// The source text for this span.
- final String text;
+ String get text;
/// The URL of the source (typically a file) of this span.
///
/// This may be null, indicating that the source URL is unknown or
/// unavailable.
- final Uri sourceUrl;
+ Uri get sourceUrl;
/// The length of this span, in characters.
- final int length;
+ int get length;
/// Creates a new span from [start] to [end] (exclusive) containing [text].
///
@@ -48,10 +50,16 @@
/// Formats [message] in a human-friendly way associated with this span.
///
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
- /// it indicates an ANSII terminal color escape that should be used to
- /// highlight the span's text. 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.
+ /// 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.
+ ///
+ /// 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.
@@ -61,10 +69,16 @@
/// isn't a [SourceSpanWithContext], returns an empty string.
///
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
- /// it indicates an ANSII terminal color escape that should be used to
- /// highlight the span's text. 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.
+ /// 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.
+ ///
+ /// 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 46e3d52..813dd3b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,12 +1,17 @@
name: source_span
-version: 1.4.0
-author: Dart Team <misc@dartlang.org>
+version: 1.5.6-dev
+
description: A library for identifying source spans and locations.
+author: Dart Team <misc@dartlang.org>
homepage: https://github.com/dart-lang/source_span
-dependencies:
- charcode: '^1.0.0'
- path: '>=1.2.0 <2.0.0'
+
environment:
- sdk: '>=1.8.0 <2.0.0'
+ sdk: '>=2.0.0 <3.0.0'
+
+dependencies:
+ charcode: ^1.0.0
+ path: '>=1.2.0 <2.0.0'
+ term_glyph: ^1.0.0
+
dev_dependencies:
- test: '>=0.12.0 <0.13.0'
+ test: ^1.0.0
diff --git a/test/file_test.dart b/test/file_test.dart
index 3f02a8b..e043ac3 100644
--- a/test/file_test.dart
+++ b/test/file_test.dart
@@ -8,7 +8,7 @@
main() {
var file;
setUp(() {
- file = new SourceFile("""
+ file = new SourceFile.fromString("""
foo bar baz
whiz bang boom
zip zap zop""", url: "foo.dart");
@@ -122,7 +122,7 @@
});
test("for span().expand() source URLs must match", () {
- var other = new SourceFile("""
+ var other = new SourceFile.fromString("""
foo bar baz
whiz bang boom
zip zap zop""", url: "bar.dart").span(10, 11);
@@ -139,11 +139,11 @@
group("new SourceFile()", () {
test("handles CRLF correctly", () {
- expect(new SourceFile("foo\r\nbar").getLine(6), equals(1));
+ expect(new SourceFile.fromString("foo\r\nbar").getLine(6), equals(1));
});
test("handles a lone CR correctly", () {
- expect(new SourceFile("foo\rbar").getLine(5), equals(1));
+ expect(new SourceFile.fromString("foo\rbar").getLine(5), equals(1));
});
});
@@ -157,7 +157,7 @@
test("end defaults to the end of the file", () {
var span = file.span(5);
expect(span.start, equals(file.location(5)));
- expect(span.end, equals(file.location(file.length - 1)));
+ expect(span.end, equals(file.location(file.length)));
});
});
@@ -253,10 +253,46 @@
expect(file.span(8, 15).text, equals("baz\nwhi"));
});
- test("context contains the span's text", () {
- var span = file.span(8, 15);
- expect(span.context.contains(span.text), isTrue);
- expect(span.context, equals('foo bar baz\nwhiz bang boom\n'));
+ test("text includes the last char when end is defaulted to EOF", () {
+ expect(file.span(29).text, equals("p zap zop"));
+ });
+
+ group("context", () {
+ test("contains the span's text", () {
+ var span = file.span(8, 15);
+ expect(span.context.contains(span.text), isTrue);
+ expect(span.context, equals('foo bar baz\nwhiz bang boom\n'));
+ });
+
+ test("contains the previous line for a point span at the end of a line",
+ () {
+ var span = file.span(25, 25);
+ expect(span.context, equals('whiz bang boom\n'));
+ });
+
+ test("contains the next line for a point span at the beginning of a line",
+ () {
+ var span = file.span(12, 12);
+ expect(span.context, equals('whiz bang boom\n'));
+ });
+
+ group("for a point span at the end of a file", () {
+ test("without a newline, contains the last line", () {
+ var span = file.span(file.length, file.length);
+ expect(span.context, equals('zip zap zop'));
+ });
+
+ test("with a newline, contains an empty line", () {
+ file = new SourceFile.fromString("""
+foo bar baz
+whiz bang boom
+zip zap zop
+""", url: "foo.dart");
+
+ var span = file.span(file.length, file.length);
+ expect(span.context, isEmpty);
+ });
+ });
});
group("union()", () {
diff --git a/test/highlight_test.dart b/test/highlight_test.dart
index 08d2e17..5fdf622 100644
--- a/test/highlight_test.dart
+++ b/test/highlight_test.dart
@@ -2,14 +2,26 @@
// 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("""
+ file = new SourceFile.fromString("""
foo bar baz
whiz bang boom
zip zap zop
@@ -18,98 +30,504 @@
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("foo bar baz").span(4, 7);
+ var span = new SourceFile.fromString("foo bar baz").span(4, 7);
expect(span.highlight(), equals("""
-foo bar baz
- ^^^"""));
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
});
- test("highlights the first line of a multiline span", () {
- expect(file.span(4, 20).highlight(), equals("""
-foo bar baz
- ^^^^^^^^"""));
+ group("highlights a point span", () {
+ test("in the middle of a line", () {
+ expect(file.location(4).pointSpan().highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ test("at the beginning of the file", () {
+ expect(file.location(0).pointSpan().highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ test("at the beginning of a line", () {
+ expect(file.location(12).pointSpan().highlight(), equals("""
+ ,
+2 | whiz bang boom
+ | ^
+ '"""));
+ });
+
+ test("at the end of a line", () {
+ expect(file.location(11).pointSpan().highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ test("at the end of the file", () {
+ expect(file.location(38).pointSpan().highlight(), equals("""
+ ,
+3 | zip zap zop
+ | ^
+ '"""));
+ });
+
+ test("after the end of the file", () {
+ expect(file.location(39).pointSpan().highlight(), equals("""
+ ,
+4 |
+ | ^
+ '"""));
+ });
+
+ test("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("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
+ | ^
+ '"""));
+ });
+
+ test("in an empty file", () {
+ expect(new SourceFile.fromString("").location(0).pointSpan().highlight(),
+ equals("""
+ ,
+1 |
+ | ^
+ '"""));
+ });
+
+ test("on an empty line", () {
+ var file = new SourceFile.fromString("foo\n\nbar");
+ expect(file.location(4).pointSpan().highlight(), equals("""
+ ,
+2 |
+ | ^
+ '"""));
+ });
});
- test("works for a point span", () {
- expect(file.location(4).pointSpan().highlight(), equals("""
-foo bar baz
- ^"""));
+ test("highlights a single-line file without a newline", () {
+ expect(
+ new SourceFile.fromString("foo bar").span(0, 7).highlight(), equals("""
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
});
- test("works for a point span at the end of a line", () {
- expect(file.location(11).pointSpan().highlight(), equals("""
-foo bar baz
- ^"""));
+ test("highlights a single empty line", () {
+ expect(new SourceFile.fromString("foo\n\nbar").span(4, 5).highlight(),
+ equals("""
+ ,
+2 |
+ | ^
+ '"""));
});
- test("works for a point span at the end of the file", () {
- expect(file.location(38).pointSpan().highlight(), equals("""
+ 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 first line if it's empty", () {
+ var file = new SourceFile.fromString("""
+foo
+
+bar
+""");
+
+ expect(file.span(4, 9).highlight(), equals("""
+ ,
+2 | /
+3 | \\ bar
+ '"""));
+ });
+
+ test("highlights the full last line", () {
+ expect(file.span(4, 27).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | \\ whiz bang boom
+ '"""));
+ });
+
+ test("highlights the full last line with no trailing newline", () {
+ expect(file.span(4, 26).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | \\ whiz bang boom
+ '"""));
+ });
+
+ 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("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | \\ zip zap zop
+ '"""));
+ });
+
+ test(
+ "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
+zip zap zop""");
+
+ expect(file.span(4, 38).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | \\ zip zap zop
+ '"""));
+ });
+
+ test("highlights the full last line if it's empty", () {
+ var file = new SourceFile.fromString("""
+foo
+
+bar
+""");
+
+ expect(file.span(0, 5).highlight(), equals("""
+ ,
+1 | / foo
+2 | \\
+ '"""));
+ });
+
+ test("highlights multiple empty lines", () {
+ var file = new SourceFile.fromString("foo\n\n\n\nbar");
+ expect(file.span(4, 7).highlight(), equals("""
+ ,
+2 | /
+3 | |
+4 | \\
+ '"""));
+ });
+
+ // Regression test for #32
+ test("highlights the end of a line and an empty line", () {
+ var file = new SourceFile.fromString("foo\n\n");
+ expect(file.span(3, 5).highlight(), equals("""
+ ,
+1 | foo
+ | ,----^
+2 | \\
+ '"""));
+ });
+ });
+
+ 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("works for a point span at the end of the file with no trailing newline",
- () {
- file = new SourceFile("zip zap zop");
- expect(file.location(11).pointSpan().highlight(), equals("""
-zip zap zop
- ^"""));
- });
+ group("supports lines of preceding and following context for a span", () {
+ test("within a single line", () {
+ var span = new SourceSpanWithContext(
+ new SourceLocation(20, line: 2, column: 5, sourceUrl: "foo.dart"),
+ new SourceLocation(27, line: 2, column: 12, sourceUrl: "foo.dart"),
+ "foo bar",
+ "previous\nlines\n-----foo bar-----\nfollowing line\n");
- test("works for a point span in an empty file", () {
- expect(new SourceFile("").location(0).pointSpan().highlight(), equals("""
+ expect(span.highlight(), equals("""
+ ,
+1 | previous
+2 | lines
+3 | -----foo bar-----
+ | ^^^^^^^
+4 | following line
+ '"""));
+ });
-^"""));
- });
+ test("covering a full line", () {
+ var span = new SourceSpanWithContext(
+ new SourceLocation(15, line: 2, column: 0, sourceUrl: "foo.dart"),
+ new SourceLocation(33, line: 3, column: 0, sourceUrl: "foo.dart"),
+ "-----foo bar-----\n",
+ "previous\nlines\n-----foo bar-----\nfollowing line\n");
- test("works for a single-line file without a newline", () {
- expect(new SourceFile("foo bar").span(0, 7).highlight(), equals("""
-foo bar
-^^^^^^^"""));
- });
+ expect(span.highlight(), equals("""
+ ,
+1 | previous
+2 | lines
+3 | -----foo bar-----
+ | ^^^^^^^^^^^^^^^^^
+4 | following line
+ '"""));
+ });
- test("emits tabs for tabs", () {
- expect(new SourceFile(" \t \t\tfoo bar").span(5, 8).highlight(), equals("""
- \t \t\tfoo bar
- \t \t\t^^^"""));
- });
+ test("covering multiple full lines", () {
+ var span = new SourceSpanWithContext(
+ new SourceLocation(15, line: 2, column: 0, sourceUrl: "foo.dart"),
+ new SourceLocation(23, line: 4, column: 0, sourceUrl: "foo.dart"),
+ "foo\nbar\n",
+ "previous\nlines\nfoo\nbar\nfollowing line\n");
- test("supports lines of preceding 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"),
- "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}"""));
+ expect(span.highlight(), equals("""
+ ,
+1 | previous
+2 | lines
+3 | / foo
+4 | \\ bar
+5 | following line
+ '"""));
+ });
});
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);