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.source_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 'source_span.dart'; | |
14 import 'source_span_mixin.dart'; | |
15 import 'source_location.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)); | |
Siggi Cherem (dart-lang)
2014/07/16 21:26:09
we use to take the max between start and 0, is you
nweiz
2014/07/17 20:22:08
Sublist will throw appropriate errors here. This i
| |
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 if (sourceUrl != other.sourceUrl) { | |
205 throw new ArgumentError("Source URLs \"${sourceUrl}\" and " | |
206 " \"${other.sourceUrl}\" don't match."); | |
207 } | |
208 | |
209 var start = min(this.start, other.start); | |
210 var end = max(this.end, other.end); | |
211 var beginSpan = start == this.start ? this : other; | |
212 var endSpan = end == this.end ? this : other; | |
Siggi Cherem (dart-lang)
2014/07/16 21:26:09
could we reuse more of the code from the mixin? fo
nweiz
2014/07/17 20:22:08
I think we really want to avoid accessing the span
| |
213 | |
214 if (beginSpan.end.compareTo(endSpan.start) < 0) { | |
215 throw new ArgumentError("Spans $this and $other are disjoint."); | |
216 } | |
217 | |
218 return new FileSpan._(file, start, end); | |
219 } | |
220 | |
221 String message(String message, {color}) { | |
222 if (color == true) color = colors.RED; | |
223 if (color == false) color = null; | |
224 | |
225 var line = start.line; | |
226 var column = start.column; | |
227 | |
228 var buffer = new StringBuffer(); | |
229 buffer.write('line ${start.line + 1}, column ${start.column + 1}'); | |
230 if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}'); | |
231 buffer.write(': $message\n'); | |
232 | |
233 var textLine = file.getText(file.getOffset(line), | |
234 line == file.lines - 1 ? null : file.getOffset(line + 1)); | |
235 | |
236 column = math.min(column, textLine.length - 1); | |
237 var toColumn = | |
238 math.min(column + end.offset - start.offset, textLine.length); | |
239 | |
240 if (color != null) { | |
241 buffer.write(textLine.substring(0, column)); | |
242 buffer.write(color); | |
243 buffer.write(textLine.substring(column, toColumn)); | |
244 buffer.write(colors.NONE); | |
245 buffer.write(textLine.substring(toColumn)); | |
246 } else { | |
247 buffer.write(textLine); | |
248 } | |
249 if (!textLine.endsWith('\n')) buffer.write('\n'); | |
250 | |
251 buffer.write(' ' * column); | |
252 if (color != null) buffer.write(color); | |
253 buffer.write('^' * math.max(toColumn - column, 1)); | |
254 if (color != null) buffer.write(colors.NONE); | |
255 return buffer.toString(); | |
256 } | |
257 } | |
OLD | NEW |