OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library source_span.file; |
| 6 |
| 7 import 'dart:math' as math; |
| 8 import 'dart:typed_data'; |
| 9 |
| 10 import 'package:path/path.dart' as p; |
| 11 |
| 12 import 'colors.dart' as colors; |
| 13 import 'location.dart'; |
| 14 import 'span.dart'; |
| 15 import 'span_mixin.dart'; |
| 16 import 'utils.dart'; |
| 17 |
| 18 // Constants to determine end-of-lines. |
| 19 const int _LF = 10; |
| 20 const int _CR = 13; |
| 21 |
| 22 /// A class representing a source file. |
| 23 /// |
| 24 /// This doesn't necessarily have to correspond to a file on disk, just a chunk |
| 25 /// of text usually with a URL associated with it. |
| 26 class SourceFile { |
| 27 /// The URL where the source file is located. |
| 28 /// |
| 29 /// This may be null, indicating that the URL is unknown or unavailable. |
| 30 final Uri url; |
| 31 |
| 32 /// An array of offsets for each line beginning in the file. |
| 33 /// |
| 34 /// Each offset refers to the first character *after* the newline. If the |
| 35 /// source file has a trailing newline, the final offset won't actually be in |
| 36 /// the file. |
| 37 final _lineStarts = <int>[0]; |
| 38 |
| 39 /// The code points of the characters in the file. |
| 40 final Uint32List _decodedChars; |
| 41 |
| 42 /// The length of the file in characters. |
| 43 int get length => _decodedChars.length; |
| 44 |
| 45 /// The number of lines in the file. |
| 46 int get lines => _lineStarts.length; |
| 47 |
| 48 /// Creates a new source file from [text]. |
| 49 /// |
| 50 /// [url] may be either a [String], a [Uri], or `null`. |
| 51 SourceFile(String text, {url}) |
| 52 : this.decoded(text.runes, url: url); |
| 53 |
| 54 /// Creates a new source file from a list of decoded characters. |
| 55 /// |
| 56 /// [url] may be either a [String], a [Uri], or `null`. |
| 57 SourceFile.decoded(Iterable<int> decodedChars, {url}) |
| 58 : url = url is String ? Uri.parse(url) : url, |
| 59 _decodedChars = new Uint32List.fromList(decodedChars.toList()) { |
| 60 for (var i = 0; i < _decodedChars.length; i++) { |
| 61 var c = _decodedChars[i]; |
| 62 if (c == _CR) { |
| 63 // Return not followed by newline is treated as a newline |
| 64 var j = i + 1; |
| 65 if (j >= _decodedChars.length || _decodedChars[j] != _LF) c = _LF; |
| 66 } |
| 67 if (c == _LF) _lineStarts.add(i + 1); |
| 68 } |
| 69 } |
| 70 |
| 71 /// Returns a span in [this] from [start] to [end] (exclusive). |
| 72 /// |
| 73 /// If [end] isn't passed, it defaults to the end of the file. |
| 74 FileSpan span(int start, [int end]) { |
| 75 if (end == null) end = length - 1; |
| 76 return new FileSpan._(this, location(start), location(end)); |
| 77 } |
| 78 |
| 79 /// Returns a location in [this] at [offset]. |
| 80 FileLocation location(int offset) => new FileLocation._(this, offset); |
| 81 |
| 82 /// Gets the 0-based line corresponding to [offset]. |
| 83 int getLine(int offset) { |
| 84 if (offset < 0) { |
| 85 throw new RangeError("Offset may not be negative, was $offset."); |
| 86 } else if (offset > length) { |
| 87 throw new RangeError("Offset $offset must not be greater than the number " |
| 88 "of characters in the file, $length."); |
| 89 } |
| 90 return binarySearch(_lineStarts, (o) => o > offset) - 1; |
| 91 } |
| 92 |
| 93 /// Gets the 0-based column corresponding to [offset]. |
| 94 /// |
| 95 /// If [line] is passed, it's assumed to be the line containing [offset] and |
| 96 /// is used to more efficiently compute the column. |
| 97 int getColumn(int offset, {int line}) { |
| 98 if (offset < 0) { |
| 99 throw new RangeError("Offset may not be negative, was $offset."); |
| 100 } else if (offset > length) { |
| 101 throw new RangeError("Offset $offset must be not be greater than the " |
| 102 "number of characters in the file, $length."); |
| 103 } |
| 104 |
| 105 if (line == null) { |
| 106 line = getLine(offset); |
| 107 } else if (line < 0) { |
| 108 throw new RangeError("Line may not be negative, was $line."); |
| 109 } else if (line >= lines) { |
| 110 throw new RangeError("Line $line must be less than the number of " |
| 111 "lines in the file, $lines."); |
| 112 } |
| 113 |
| 114 var lineStart = _lineStarts[line]; |
| 115 if (lineStart > offset) { |
| 116 throw new RangeError("Line $line comes after offset $offset."); |
| 117 } |
| 118 |
| 119 return offset - lineStart; |
| 120 } |
| 121 |
| 122 /// Gets the offset for a [line] and [column]. |
| 123 /// |
| 124 /// [column] defaults to 0. |
| 125 int getOffset(int line, [int column]) { |
| 126 if (column == null) column = 0; |
| 127 |
| 128 if (line < 0) { |
| 129 throw new RangeError("Line may not be negative, was $line."); |
| 130 } else if (line >= lines) { |
| 131 throw new RangeError("Line $line must be less than the number of " |
| 132 "lines in the file, $lines."); |
| 133 } else if (column < 0) { |
| 134 throw new RangeError("Column may not be negative, was $column."); |
| 135 } |
| 136 |
| 137 var result = _lineStarts[line] + column; |
| 138 if (result > length || |
| 139 (line + 1 < lines && result >= _lineStarts[line + 1])) { |
| 140 throw new RangeError("Line $line doesn't have $column columns."); |
| 141 } |
| 142 |
| 143 return result; |
| 144 } |
| 145 |
| 146 /// Returns the text of the file from [start] to [end] (exclusive). |
| 147 /// |
| 148 /// If [end] isn't passed, it defaults to the end of the file. |
| 149 String getText(int start, [int end]) => |
| 150 new String.fromCharCodes(_decodedChars.sublist(start, end)); |
| 151 } |
| 152 |
| 153 /// A [SourceLocation] within a [SourceFile]. |
| 154 /// |
| 155 /// Unlike the base [SourceLocation], [FileLocation] lazily computes its line |
| 156 /// and column values based on its offset and the contents of [file]. |
| 157 /// |
| 158 /// A [FileLocation] can be created using [SourceFile.location]. |
| 159 class FileLocation extends SourceLocation { |
| 160 /// The [file] that [this] belongs to. |
| 161 final SourceFile file; |
| 162 |
| 163 Uri get sourceUrl => file.url; |
| 164 int get line => file.getLine(offset); |
| 165 int get column => file.getColumn(offset); |
| 166 |
| 167 FileLocation._(this.file, int offset) |
| 168 : super(offset) { |
| 169 if (offset > file.length) { |
| 170 throw new RangeError("Offset $offset must not be greater than the number " |
| 171 "of characters in the file, ${file.length}."); |
| 172 } |
| 173 } |
| 174 |
| 175 FileSpan pointSpan() => new FileSpan._(file, this, this); |
| 176 } |
| 177 |
| 178 /// A [SourceSpan] within a [SourceFile]. |
| 179 /// |
| 180 /// Unlike the base [SourceSpan], [FileSpan] lazily computes its line and column |
| 181 /// values based on its offset and the contents of [file]. [FileSpan.message] is |
| 182 /// also able to provide more context then [SourceSpan.message], and |
| 183 /// [FileSpan.union] will return a [FileSpan] if possible. |
| 184 /// |
| 185 /// A [FileSpan] can be created using [SourceFile.span]. |
| 186 class FileSpan extends SourceSpanMixin { |
| 187 /// The [file] that [this] belongs to. |
| 188 final SourceFile file; |
| 189 |
| 190 final FileLocation start; |
| 191 final FileLocation end; |
| 192 |
| 193 String get text => file.getText(start.offset, end.offset); |
| 194 |
| 195 FileSpan._(this.file, this.start, this.end) { |
| 196 if (end.offset < start.offset) { |
| 197 throw new ArgumentError('End $end must come after start $start.'); |
| 198 } |
| 199 } |
| 200 |
| 201 SourceSpan union(SourceSpan other) { |
| 202 if (other is! FileSpan) return super.union(other); |
| 203 |
| 204 var span = expand(other); |
| 205 var beginSpan = span.start == this.start ? this : other; |
| 206 var endSpan = span.end == this.end ? this : other; |
| 207 |
| 208 if (beginSpan.end.compareTo(endSpan.start) < 0) { |
| 209 throw new ArgumentError("Spans $this and $other are disjoint."); |
| 210 } |
| 211 |
| 212 return span; |
| 213 } |
| 214 |
| 215 /// Returns a new span that covers both [this] and [other]. |
| 216 /// |
| 217 /// Unlike [union], [other] may be disjoint from [this]. If it is, the text |
| 218 /// between the two will be covered by the returned span. |
| 219 FileSpan expand(FileSpan other) { |
| 220 if (sourceUrl != other.sourceUrl) { |
| 221 throw new ArgumentError("Source URLs \"${sourceUrl}\" and " |
| 222 " \"${other.sourceUrl}\" don't match."); |
| 223 } |
| 224 |
| 225 var start = min(this.start, other.start); |
| 226 var end = max(this.end, other.end); |
| 227 return new FileSpan._(file, start, end); |
| 228 } |
| 229 |
| 230 String message(String message, {color}) { |
| 231 if (color == true) color = colors.RED; |
| 232 if (color == false) color = null; |
| 233 |
| 234 var line = start.line; |
| 235 var column = start.column; |
| 236 |
| 237 var buffer = new StringBuffer(); |
| 238 buffer.write('line ${start.line + 1}, column ${start.column + 1}'); |
| 239 if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}'); |
| 240 buffer.write(': $message\n'); |
| 241 |
| 242 var textLine = file.getText(file.getOffset(line), |
| 243 line == file.lines - 1 ? null : file.getOffset(line + 1)); |
| 244 |
| 245 column = math.min(column, textLine.length - 1); |
| 246 var toColumn = |
| 247 math.min(column + end.offset - start.offset, textLine.length); |
| 248 |
| 249 if (color != null) { |
| 250 buffer.write(textLine.substring(0, column)); |
| 251 buffer.write(color); |
| 252 buffer.write(textLine.substring(column, toColumn)); |
| 253 buffer.write(colors.NONE); |
| 254 buffer.write(textLine.substring(toColumn)); |
| 255 } else { |
| 256 buffer.write(textLine); |
| 257 } |
| 258 if (!textLine.endsWith('\n')) buffer.write('\n'); |
| 259 |
| 260 buffer.write(' ' * column); |
| 261 if (color != null) buffer.write(color); |
| 262 buffer.write('^' * math.max(toColumn - column, 1)); |
| 263 if (color != null) buffer.write(colors.NONE); |
| 264 return buffer.toString(); |
| 265 } |
| 266 } |
OLD | NEW |