Index: pkg/source_maps/lib/span.dart |
diff --git a/pkg/source_maps/lib/span.dart b/pkg/source_maps/lib/span.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..19829941f3b07c962f1a159979df29c6dd80181e |
--- /dev/null |
+++ b/pkg/source_maps/lib/span.dart |
@@ -0,0 +1,330 @@ |
+// Copyright (c) 2013, 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. |
+ |
+/// Dart classes representing the souce spans and source files. |
+library source_maps.span; |
+ |
+import 'dart:utf' show stringToCodepoints, codepointsToString; |
+import 'dart:math' show min; |
+ |
+import 'src/utils.dart'; |
+ |
+/// A simple class that describe a segment of source text. |
+abstract class Span implements Comparable { |
+ /// The start location of this span. |
+ final Location start; |
+ |
+ /// The end location of this span, exclusive. |
+ final Location end; |
+ |
+ /// Url of the source (typically a file) containing this span. |
+ String get sourceUrl => start.sourceUrl; |
+ |
+ /// The length of this span, in characters. |
+ int get length => end.offset - start.offset; |
+ |
+ /// The source text for this span, if available. |
+ String get text; |
+ |
+ /// Whether [text] corresponds to an identifier symbol. |
+ final bool isIdentifier; |
+ |
+ Span(this.start, this.end, bool isIdentifier) |
+ : isIdentifier = isIdentifier != null ? isIdentifier : false { |
+ _checkRange(); |
+ } |
+ |
+ /// Creates a new span that is the union of two existing spans [start] and |
+ /// [end]. Note that the resulting span might contain some positions that were |
+ /// not in either of the original spans if [start] and [end] are disjoint. |
+ Span.union(Span start, Span end) |
+ : start = start.start, end = end.end, isIdentifier = false { |
+ _checkRange(); |
+ } |
+ |
+ void _checkRange() { |
+ if (start.offset < 0) throw new ArgumentError('start $start must be >= 0'); |
+ if (end.offset < start.offset) { |
+ throw new ArgumentError('end $end must be >= start $start'); |
+ } |
+ } |
+ |
+ /// Compares two spans. If the spans are not in the same source, this method |
+ /// generates an error. |
+ int compareTo(Span other) { |
+ int d = start.compareTo(other.start); |
+ return d == 0 ? end.compareTo(other.end) : d; |
+ } |
+ |
+ /// Gets the location in standard printed form `filename:line:column`, where |
+ /// line and column are adjusted by 1 to match the convention in editors. |
+ String get formatLocation => start.formatString; |
+ |
+ String getLocationMessage(String message, |
+ {bool useColors: false, String color}) { |
+ return '$formatLocation: $message'; |
+ } |
+ |
+ bool operator ==(Span other) => |
+ sourceUrl == other.sourceUrl && start == other.start && end == other.end; |
+ |
+ String toString() => '<$runtimeType: $start $end $formatLocation $text>'; |
+} |
+ |
+/// A location in the source text |
+abstract class Location implements Comparable { |
+ /// Url of the source containing this span. |
+ String get sourceUrl; |
+ |
+ /// The offset of this location, 0-based. |
+ final int offset; |
+ |
+ /// The 0-based line in the source of this location. |
+ int get line; |
+ |
+ /// The 0-based column in the source of this location. |
+ int get column; |
+ |
+ Location(this.offset); |
+ |
+ /// Compares two locations. If the locations are not in the same source, this |
+ /// method generates an error. |
+ int compareTo(Location other) { |
+ if (sourceUrl != other.sourceUrl) { |
+ throw new ArgumentError('can only compare locations of the same source'); |
+ } |
+ return offset - other.offset; |
+ } |
+ |
+ String toString() => '(Location $offset)'; |
+ String get formatString => '$sourceUrl:${line + 1}:${column + 1}'; |
+} |
+ |
+/// Implementation of [Location] with fixed values given at allocation time. |
+class FixedLocation extends Location { |
+ final String sourceUrl; |
+ final int line; |
+ final int column; |
+ |
+ FixedLocation(int offset, this.sourceUrl, this.line, this.column) |
+ : super(offset); |
+} |
+ |
+/// Implementation of [Span] where all the values are given at allocation time. |
+class FixedSpan extends Span { |
+ /// The source text for this span, if available. |
+ final String text; |
+ |
+ /// Creates a span which starts and end in the same line. |
+ FixedSpan(String sourceUrl, int start, int line, int column, |
+ {String text: '', bool isIdentifier: false}) |
+ : text = text, super(new FixedLocation(start, sourceUrl, line, column), |
+ new FixedLocation(start + text.length, sourceUrl, line, |
+ column + text.length), |
+ isIdentifier); |
+} |
+ |
+/// [Location] with values computed from an underling [SourceFile]. |
+class FileLocation extends Location { |
+ /// The source file containing this location. |
+ final SourceFile file; |
+ |
+ String get sourceUrl => file.url; |
+ int get line => file.getLine(offset); |
+ int get column => file.getColumn(line, offset); |
+ |
+ FileLocation(this.file, int offset): super(offset); |
+} |
+ |
+/// [Span] where values are computed from an underling [SourceFile]. |
+class FileSpan extends Span { |
+ /// The source file containing this span. |
+ final SourceFile file; |
+ |
+ /// The source text for this span, if available. |
+ String get text => file.getText(start.offset, end.offset); |
+ |
+ factory FileSpan(SourceFile file, int start, |
+ [int end, bool isIdentifier = false]) { |
+ var startLoc = new FileLocation(file, start); |
+ var endLoc = end == null ? startLoc : new FileLocation(file, end); |
+ return new FileSpan.locations(startLoc, endLoc, isIdentifier); |
+ } |
+ |
+ FileSpan.locations(FileLocation start, FileLocation end, |
+ bool isIdentifier) |
+ : file = start.file, super(start, end, isIdentifier); |
+ |
+ /// Creates a new span that is the union of two existing spans [start] and |
+ /// [end]. Note that the resulting span might contain some positions that were |
+ /// not in either of the original spans if [start] and [end] are disjoint. |
+ FileSpan.union(FileSpan start, FileSpan end) |
+ : file = start.file, super.union(start, end) { |
+ if (start.file != end.file) { |
+ throw new ArgumentError('start and end must be from the same file'); |
+ } |
+ } |
+ |
+ String getLocationMessage(String message, |
+ {bool useColors: false, String color}) { |
+ return file.getLocationMessage(message, start.offset, end.offset, |
+ useColors: useColors, color: color); |
+ } |
+} |
+ |
+// Constants to determine end-of-lines. |
+const int _LF = 10; |
+const int _CR = 13; |
+ |
+// Color constants used for generating messages. |
+const String _RED_COLOR = '\u001b[31m'; |
+const String _NO_COLOR = '\u001b[0m'; |
+ |
+/// Stores information about a source file, to permit computation of the line |
+/// and column. Also contains a nice default error message highlighting the code |
+/// location. |
+class SourceFile { |
+ /// Url where the source file is located. |
+ final String url; |
+ final List<int> _lineStarts; |
+ final List<int> _decodedChars; |
+ |
+ SourceFile(this.url, this._lineStarts, this._decodedChars); |
+ |
+ SourceFile.text(this.url, String text) |
+ : _lineStarts = <int>[0], |
+ _decodedChars = stringToCodepoints(text) { |
+ for (int i = 0; i < _decodedChars.length; i++) { |
+ var c = _decodedChars[i]; |
+ if (c == _CR) { |
+ // Return not followed by newline is treated as a newline |
+ int j = i + 1; |
+ if (j >= _decodedChars.length || _decodedChars[j] != _LF) { |
+ c = _LF; |
+ } |
+ } |
+ if (c == _LF) _lineStarts.add(i + 1); |
+ } |
+ } |
+ |
+ /// Returns a span in this [SourceFile] with the given offsets. |
+ Span span(int start, [int end, bool isIdentifier = false]) => |
+ new FileSpan(this, start, end, isIdentifier); |
+ |
+ /// Returns a location in this [SourceFile] with the given offset. |
+ Location location(int offset) => new FileLocation(this, offset); |
+ |
+ /// Gets the 0-based line corresponding to an offset. |
+ int getLine(int offset) { |
+ return binarySearch(_lineStarts, (o) => o > offset) - 1; |
+ } |
+ |
+ /// Gets the 0-based column corresponding to an offset. |
+ int getColumn(int line, int offset) { |
+ return offset - _lineStarts[line]; |
+ } |
+ |
+ /// Get the offset for a given line and column |
+ int getOffset(int line, int column) { |
+ return _lineStarts[min(line, _lineStarts.length - 1)] + column; |
+ } |
+ |
+ /// Gets the text at the given offsets. |
+ String getText(int start, [int end]) { |
+ return codepointsToString(_decodedChars.sublist(start, end)); |
+ } |
+ |
+ /// Create a pretty string representation from a span. |
+ String getLocationMessage(String message, int start, int end, |
+ {bool useColors: false, String color}) { |
+ // TODO(jmesserly): it would be more useful to pass in an object that |
+ // controls how the errors are printed. This method is a bit too smart. |
+ var line = getLine(start); |
+ var column = getColumn(line, start); |
+ |
+ var src = url == null ? '' : url; |
+ var msg = '$src:${line + 1}:${column + 1}: $message'; |
+ |
+ if (_decodedChars == null) { |
+ // We don't have any text to include, so exit. |
+ return msg; |
+ } |
+ |
+ var buf = new StringBuffer(msg); |
+ buf.write('\n'); |
+ var textLine; |
+ |
+ // +1 for 0-indexing, +1 again to avoid the last line |
+ if ((line + 2) < _lineStarts.length) { |
+ textLine = getText(_lineStarts[line], _lineStarts[line + 1]); |
+ } else { |
+ textLine = getText(_lineStarts[line]); |
+ textLine = '$textLine\n'; |
+ } |
+ |
+ int toColumn = min(column + end - start, textLine.length); |
+ if (useColors) { |
+ if (color == null) { |
+ color = _RED_COLOR; |
+ } |
+ buf.write(textLine.substring(0, column)); |
+ buf.write(color); |
+ buf.write(textLine.substring(column, toColumn)); |
+ buf.write(_NO_COLOR); |
+ buf.write(textLine.substring(toColumn)); |
+ } else { |
+ buf.write(textLine); |
+ } |
+ |
+ int i = 0; |
+ for (; i < column; i++) { |
+ buf.write(' '); |
+ } |
+ |
+ if (useColors) buf.write(color); |
+ for (; i < toColumn; i++) { |
+ buf.write('^'); |
+ } |
+ if (useColors) buf.write(_NO_COLOR); |
+ return buf.toString(); |
+ } |
+} |
+ |
+/// A convenience type to treat a code segment as if it were a separate |
+/// [SourceFile]. A [SourceFileSegment] shifts all locations by an offset, which |
+/// allows you to set source-map locations based on the locations relative to |
+/// the start of the segment, but that get translated to absolute locations in |
+/// the original source file. |
+class SourceFileSegment extends SourceFile { |
+ final int _baseOffset; |
+ final int _baseLine; |
+ final int _baseColumn; |
+ |
+ SourceFileSegment(String url, String textSegment, Location startOffset) |
+ : _baseOffset = startOffset.offset, |
+ _baseLine = startOffset.line, |
+ _baseColumn = startOffset.column, |
+ super.text(url, textSegment); |
+ |
+ Span span(int start, [int end, bool isIdentifier = false]) => |
+ super.span(start + _baseOffset, |
+ end == null ? null : end + _baseOffset, isIdentifier); |
+ |
+ Location location(int offset) => super.location(offset + _baseOffset); |
+ |
+ int getLine(int offset) => |
+ super.getLine(offset - _baseOffset) + _baseLine; |
+ |
+ int getColumn(int line, int offset) { |
+ var col = super.getColumn(line - _baseLine, offset - _baseOffset); |
+ return line == _baseLine ? col + _baseColumn : col; |
+ } |
+ |
+ int getOffset(int line, int column) => |
+ super.getOffset(line - _baseLine, |
+ line == _baseLine ? column - _baseColumn : column) + _baseOffset; |
+ |
+ String getText(int start, [int end]) => |
+ super.getText(start - _baseOffset, end - _baseOffset); |
+} |