| // Copyright (c) 2014, 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. | |
| // ignore_for_file: prefer_interpolation_to_compose_strings | |
| import 'package:source_span/source_span.dart'; | |
| import 'package:source_span/src/colors.dart' as colors; | |
| import 'package:term_glyph/term_glyph.dart' as glyph; | |
| import 'package:test/test.dart'; | |
| void main() { | |
| late bool oldAscii; | |
| setUpAll(() { | |
| oldAscii = glyph.ascii; | |
| glyph.ascii = true; | |
| }); | |
| tearDownAll(() { | |
| glyph.ascii = oldAscii; | |
| }); | |
| late SourceFile file; | |
| setUp(() { | |
| file = SourceFile.fromString(''' | |
| foo bar baz | |
| whiz bang boom | |
| zip zap zop | |
| '''); | |
| }); | |
| test('points to the span in the source', () { | |
| expect(file.span(4, 7).highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ^^^ | |
| '""")); | |
| }); | |
| test('gracefully handles a missing source URL', () { | |
| final span = SourceFile.fromString('foo bar baz').span(4, 7); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | 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 = 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 = SourceFile.fromString('zip zap zop'); | |
| expect(file.location(11).pointSpan().highlight(), equals(""" | |
| , | |
| 1 | zip zap zop | |
| | ^ | |
| '""")); | |
| }); | |
| test('in an empty file', () { | |
| expect(SourceFile.fromString('').location(0).pointSpan().highlight(), | |
| equals(""" | |
| , | |
| 1 | | |
| | ^ | |
| '""")); | |
| }); | |
| test('on an empty line', () { | |
| final file = SourceFile.fromString('foo\n\nbar'); | |
| expect(file.location(4).pointSpan().highlight(), equals(""" | |
| , | |
| 2 | | |
| | ^ | |
| '""")); | |
| }); | |
| }); | |
| test('highlights a single-line file without a newline', () { | |
| expect(SourceFile.fromString('foo bar').span(0, 7).highlight(), equals(""" | |
| , | |
| 1 | foo bar | |
| | ^^^^^^^ | |
| '""")); | |
| }); | |
| test('highlights text including a trailing newline', () { | |
| expect(file.span(8, 12).highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ^^^ | |
| '""")); | |
| }); | |
| test('highlights a single empty line', () { | |
| expect( | |
| SourceFile.fromString('foo\n\nbar').span(4, 5).highlight(), equals(""" | |
| , | |
| 2 | | |
| | ^ | |
| '""")); | |
| }); | |
| test('highlights a trailing newline', () { | |
| expect(file.span(11, 12).highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ^ | |
| '""")); | |
| }); | |
| 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", () { | |
| final file = 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", () { | |
| final file = SourceFile.fromString(''' | |
| foo | |
| bar | |
| '''); | |
| expect(file.span(4, 9).highlight(), equals(r""" | |
| , | |
| 2 | / | |
| 3 | \ bar | |
| '""")); | |
| }); | |
| test('highlights the full last line', () { | |
| expect(file.span(4, 27).highlight(), equals(r""" | |
| , | |
| 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(r""" | |
| , | |
| 1 | foo bar baz | |
| | ,-----^ | |
| 2 | \ whiz bang boom | |
| '""")); | |
| }); | |
| test('highlights the full last line with a trailing Windows newline', () { | |
| final file = SourceFile.fromString(''' | |
| foo bar baz\r | |
| whiz bang boom\r | |
| zip zap zop\r | |
| '''); | |
| expect(file.span(4, 29).highlight(), equals(r""" | |
| , | |
| 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(r""" | |
| , | |
| 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', () { | |
| final file = SourceFile.fromString(''' | |
| foo bar baz | |
| whiz bang boom | |
| zip zap zop'''); | |
| expect(file.span(4, 38).highlight(), equals(r""" | |
| , | |
| 1 | foo bar baz | |
| | ,-----^ | |
| 2 | | whiz bang boom | |
| 3 | \ zip zap zop | |
| '""")); | |
| }); | |
| test("highlights the full last line if it's empty", () { | |
| final file = SourceFile.fromString(''' | |
| foo | |
| bar | |
| '''); | |
| expect(file.span(0, 5).highlight(), equals(r""" | |
| , | |
| 1 | / foo | |
| 2 | \ | |
| '""")); | |
| }); | |
| test('highlights multiple empty lines', () { | |
| final file = SourceFile.fromString('foo\n\n\n\nbar'); | |
| expect(file.span(4, 7).highlight(), equals(r""" | |
| , | |
| 2 | / | |
| 3 | | | |
| 4 | \ | |
| '""")); | |
| }); | |
| // Regression test for #32 | |
| test('highlights the end of a line and an empty line', () { | |
| final file = SourceFile.fromString('foo\n\n'); | |
| expect(file.span(3, 5).highlight(), equals(r""" | |
| , | |
| 1 | foo | |
| | ,----^ | |
| 2 | \ | |
| '""")); | |
| }); | |
| }); | |
| group('prints tabs as spaces', () { | |
| group('in a single-line span', () { | |
| test('before the highlighted section', () { | |
| final span = SourceFile.fromString('foo\tbar baz').span(4, 7); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ^^^ | |
| '""")); | |
| }); | |
| test('within the highlighted section', () { | |
| final span = SourceFile.fromString('foo bar\tbaz bang').span(4, 11); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz bang | |
| | ^^^^^^^^^^ | |
| '""")); | |
| }); | |
| test('after the highlighted section', () { | |
| final span = 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', () { | |
| final span = 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', () { | |
| final span = SourceFile.fromString(''' | |
| foo bar\tbaz | |
| whiz bang boom | |
| ''').span(4, 21); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ,-----^ | |
| 2 | | whiz bang boom | |
| | '---------^ | |
| '""")); | |
| }); | |
| test('at the beginning of the first highlighted line', () { | |
| final span = SourceFile.fromString(''' | |
| foo bar\tbaz | |
| whiz bang boom | |
| ''').span(7, 21); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ,--------^ | |
| 2 | | whiz bang boom | |
| | '---------^ | |
| '""")); | |
| }); | |
| test('within a middle highlighted line', () { | |
| final span = 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', () { | |
| final span = SourceFile.fromString(''' | |
| foo bar baz | |
| whiz\tbang boom | |
| ''').span(4, 21); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ,-----^ | |
| 2 | | whiz bang boom | |
| | '------------^ | |
| '""")); | |
| }); | |
| test('at the end of the last highlighted line', () { | |
| final span = SourceFile.fromString(''' | |
| foo bar baz | |
| whiz\tbang boom | |
| ''').span(4, 17); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ,-----^ | |
| 2 | | whiz bang boom | |
| | '--------^ | |
| '""")); | |
| }); | |
| test('after the highlighted section', () { | |
| final span = SourceFile.fromString(''' | |
| foo bar baz | |
| whiz bang\tboom | |
| ''').span(4, 21); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar baz | |
| | ,-----^ | |
| 2 | | whiz bang boom | |
| | '---------^ | |
| '""")); | |
| }); | |
| }); | |
| }); | |
| group('supports lines of preceding and following context for a span', () { | |
| test('within a single line', () { | |
| final span = SourceSpanWithContext( | |
| SourceLocation(20, line: 2, column: 5, sourceUrl: 'foo.dart'), | |
| SourceLocation(27, line: 2, column: 12, sourceUrl: 'foo.dart'), | |
| 'foo bar', | |
| 'previous\nlines\n-----foo bar-----\nfollowing line\n'); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | previous | |
| 2 | lines | |
| 3 | -----foo bar----- | |
| | ^^^^^^^ | |
| 4 | following line | |
| '""")); | |
| }); | |
| test('covering a full line', () { | |
| final span = SourceSpanWithContext( | |
| SourceLocation(15, line: 2, column: 0, sourceUrl: 'foo.dart'), | |
| SourceLocation(33, line: 3, column: 0, sourceUrl: 'foo.dart'), | |
| '-----foo bar-----\n', | |
| 'previous\nlines\n-----foo bar-----\nfollowing line\n'); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | previous | |
| 2 | lines | |
| 3 | -----foo bar----- | |
| | ^^^^^^^^^^^^^^^^^ | |
| 4 | following line | |
| '""")); | |
| }); | |
| test('covering multiple full lines', () { | |
| final span = SourceSpanWithContext( | |
| SourceLocation(15, line: 2, column: 0, sourceUrl: 'foo.dart'), | |
| SourceLocation(23, line: 4, column: 0, sourceUrl: 'foo.dart'), | |
| 'foo\nbar\n', | |
| 'previous\nlines\nfoo\nbar\nfollowing line\n'); | |
| expect(span.highlight(), equals(r""" | |
| , | |
| 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(""" | |
| , | |
| 1 | foo bar baz | |
| | ^^^ | |
| '""")); | |
| }); | |
| test('colorizes if color is true', () { | |
| expect(file.span(4, 7).highlight(color: true), equals(''' | |
| ${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(''' | |
| ${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.red}-----^${colors.none} | |
| ${colors.blue}2 |${colors.none} ${colors.red}|${colors.none} ${colors.red}whiz bang boom${colors.none} | |
| ${colors.blue}3 |${colors.none} ${colors.red}|${colors.none} ${colors.red}zip zap${colors.none} zop | |
| ${colors.blue} |${colors.none} ${colors.red}'${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}/${colors.none} ${colors.red}foo bar baz${colors.none} | |
| ${colors.blue}2 |${colors.none} ${colors.red}|${colors.none} ${colors.red}whiz bang boom${colors.none} | |
| ${colors.blue}3 |${colors.none} ${colors.red}\\${colors.none} ${colors.red}zip zap zop${colors.none} | |
| ${colors.blue} '${colors.none}''')); | |
| }); | |
| }); | |
| group('line numbers have appropriate padding', () { | |
| test('with line number 9', () { | |
| expect( | |
| SourceFile.fromString('\n' * 8 + 'foo bar baz\n') | |
| .span(8, 11) | |
| .highlight(), | |
| equals(""" | |
| , | |
| 9 | foo bar baz | |
| | ^^^ | |
| '""")); | |
| }); | |
| test('with line number 10', () { | |
| expect( | |
| SourceFile.fromString('\n' * 9 + 'foo bar baz\n') | |
| .span(9, 12) | |
| .highlight(), | |
| equals(""" | |
| , | |
| 10 | foo bar baz | |
| | ^^^ | |
| '""")); | |
| }); | |
| }); | |
| group('bare CR and CRLF line endings', () { | |
| test('highlights a trailing bare CR', () { | |
| final file = SourceFile.fromString('ab\r'); | |
| expect(file.span(2, 3).highlight(), equals(""" | |
| , | |
| 1 | ab | |
| | ^ | |
| '""")); | |
| }); | |
| test('highlights spans across bare CR lines', () { | |
| final file = SourceFile.fromString('foo bar\rbaz bang\r'); | |
| expect(file.span(4, 11).highlight(), equals(""" | |
| , | |
| 1 | foo bar | |
| | ,-----^ | |
| 2 | | baz bang | |
| | '---^ | |
| '""")); | |
| }); | |
| test('strips CR from single-line span context in CRLF file', () { | |
| final file = SourceFile.fromString('foo bar\r\nbaz'); | |
| expect(file.span(4, 7).highlight(), equals(""" | |
| , | |
| 1 | foo bar | |
| | ^^^ | |
| '""")); | |
| }); | |
| test('handles spans starting or ending between CR and LF', () { | |
| final file = SourceFile.fromString('ab\r\ncd'); | |
| expect(file.span(1, 3).highlight(), equals(""" | |
| , | |
| 1 | ab | |
| | ^ | |
| '""")); | |
| expect(file.location(3).pointSpan().highlight(), equals(""" | |
| , | |
| 1 | ab | |
| | ^ | |
| '""")); | |
| expect(file.span(3, 5).highlight(), equals(""" | |
| , | |
| 1 | ab | |
| | ,---^ | |
| 2 | | cd | |
| | '-^ | |
| '""")); | |
| }); | |
| test('disambiguates SourceSpanWithContext subspans across lines', () { | |
| return; | |
| SourceSpanWithContext contextSpan(String source) { | |
| final span = SourceFile.fromString(source).span(0, source.length); | |
| return SourceSpanWithContext( | |
| span.start, span.end, span.text, span.context); | |
| } | |
| expect(contextSpan('ab\r\nabba').subspan(3, 3).highlight(), equals(""" | |
| , | |
| 1 | ab | |
| | ^ | |
| 2 | abba | |
| '""")); | |
| expect(contextSpan('ab\r\nabba').subspan(7, 7).highlight(), equals(""" | |
| , | |
| 1 | ab | |
| 2 | abba | |
| | ^ | |
| '""")); | |
| expect(contextSpan('a\rb\r\n').subspan(3, 4).highlight(), equals(""" | |
| , | |
| 1 | a | |
| 2 | b | |
| | ^ | |
| '""")); | |
| expect(contextSpan('b\r\nb\r').subspan(4, 5).highlight(), equals(""" | |
| , | |
| 1 | b | |
| 2 | b | |
| | ^ | |
| '""")); | |
| }); | |
| test('highlights all 4-char CR/LF SourceSpanWithContext subspans', () { | |
| return; | |
| const chars = ['a', 'b', '\r', '\n']; | |
| final strings = <String>['']; | |
| for (var length = 1; length <= 4; length++) { | |
| final previous = strings.where((s) => s.length == length - 1).toList(); | |
| for (var prefix in previous) { | |
| for (var char in chars) { | |
| strings.add(prefix + char); | |
| } | |
| } | |
| } | |
| for (var source in strings) { | |
| final fileSpan = SourceFile.fromString(source).span(0, source.length); | |
| final full = SourceSpanWithContext( | |
| fileSpan.start, fileSpan.end, fileSpan.text, fileSpan.context); | |
| for (var i = 0; i <= source.length; i++) { | |
| for (var j = i; j <= source.length; j++) { | |
| final highlighted = full.subspan(i, j).highlight(); | |
| if (!source.contains('\r\n')) { | |
| final lfSource = source.replaceAll('\r', '\n'); | |
| final lfFileSpan = | |
| SourceFile.fromString(lfSource).span(0, lfSource.length); | |
| final lfFull = SourceSpanWithContext(lfFileSpan.start, | |
| lfFileSpan.end, lfFileSpan.text, lfFileSpan.context); | |
| expect(highlighted, equals(lfFull.subspan(i, j).highlight()), | |
| reason: 'source=${source.codeUnits} [$i, $j]'); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| test('normalizes plain SourceSpan ending with double newline', () { | |
| final span = SourceSpan( | |
| SourceLocation(0), SourceLocation(5, line: 2, column: 0), 'abc\n\n'); | |
| expect(span.highlight(), equals(r""" | |
| , | |
| 1 | / abc | |
| 2 | \ | |
| '""")); | |
| }); | |
| }); | |
| test('highlights a point span at start of last line without trailing newline', | |
| () { | |
| final file = SourceFile.fromString('\n\n~'); | |
| expect(file.location(2).pointSpan().highlight(), equals(""" | |
| , | |
| 3 | ~ | |
| | ^ | |
| '""")); | |
| }); | |
| test( | |
| 'highlights a span starting after column 0 and ending at newline with ' | |
| 'following context', () { | |
| final span = SourceSpanWithContext(SourceLocation(4, line: 0, column: 4), | |
| SourceLocation(8, line: 1, column: 0), 'bar\n', 'foo bar\nbaz\n'); | |
| expect(span.highlight(), equals(""" | |
| , | |
| 1 | foo bar | |
| | ^^^ | |
| 2 | baz | |
| '""")); | |
| }); | |
| } |