Line data Source code
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 : import 'package:path/path.dart' as p;
6 : import 'package:term_glyph/term_glyph.dart' as glyph;
7 :
8 : import 'charcode.dart';
9 : import 'file.dart';
10 : import 'highlighter.dart';
11 : import 'location.dart';
12 : import 'span_mixin.dart';
13 : import 'span_with_context.dart';
14 :
15 : /// A class that describes a segment of source text.
16 : abstract class SourceSpan implements Comparable<SourceSpan> {
17 : /// The start location of this span.
18 : SourceLocation get start;
19 :
20 : /// The end location of this span, exclusive.
21 : SourceLocation get end;
22 :
23 : /// The source text for this span.
24 : String get text;
25 :
26 : /// The URL of the source (typically a file) of this span.
27 : ///
28 : /// This may be null, indicating that the source URL is unknown or
29 : /// unavailable.
30 : Uri? get sourceUrl;
31 :
32 : /// The length of this span, in characters.
33 : int get length;
34 :
35 : /// Creates a new span from [start] to [end] (exclusive) containing [text].
36 : ///
37 : /// [start] and [end] must have the same source URL and [start] must come
38 : /// before [end]. [text] must have a number of characters equal to the
39 : /// distance between [start] and [end].
40 0 : factory SourceSpan(SourceLocation start, SourceLocation end, String text) =>
41 0 : SourceSpanBase(start, end, text);
42 :
43 : /// Creates a new span that's the union of `this` and [other].
44 : ///
45 : /// The two spans must have the same source URL and may not be disjoint.
46 : /// [text] is computed by combining `this.text` and `other.text`.
47 : SourceSpan union(SourceSpan other);
48 :
49 : /// Compares two spans.
50 : ///
51 : /// [other] must have the same source URL as `this`. This orders spans by
52 : /// [start] then [length].
53 : @override
54 : int compareTo(SourceSpan other);
55 :
56 : /// Formats [message] in a human-friendly way associated with this span.
57 : ///
58 : /// [color] may either be a [String], a [bool], or `null`. If it's a string,
59 : /// it indicates an [ANSI terminal color escape][] that should
60 : /// be used to highlight the span's text (for example, `"\u001b[31m"` will
61 : /// color red). If it's `true`, it indicates that the text should be
62 : /// highlighted using the default color. If it's `false` or `null`, it
63 : /// indicates that the text shouldn't be highlighted.
64 : ///
65 : /// This uses the full range of Unicode characters to highlight the source
66 : /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
67 : /// characters if it's `true`.
68 : ///
69 : /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
70 : String message(String message, {color});
71 :
72 : /// Prints the text associated with this span in a user-friendly way.
73 : ///
74 : /// This is identical to [message], except that it doesn't print the file
75 : /// name, line number, column number, or message. If [length] is 0 and this
76 : /// isn't a [SourceSpanWithContext], returns an empty string.
77 : ///
78 : /// [color] may either be a [String], a [bool], or `null`. If it's a string,
79 : /// it indicates an [ANSI terminal color escape][] that should
80 : /// be used to highlight the span's text (for example, `"\u001b[31m"` will
81 : /// color red). If it's `true`, it indicates that the text should be
82 : /// highlighted using the default color. If it's `false` or `null`, it
83 : /// indicates that the text shouldn't be highlighted.
84 : ///
85 : /// This uses the full range of Unicode characters to highlight the source
86 : /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
87 : /// characters if it's `true`.
88 : ///
89 : /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
90 : String highlight({color});
91 : }
92 :
93 : /// A base class for source spans with [start], [end], and [text] known at
94 : /// construction time.
95 : class SourceSpanBase extends SourceSpanMixin {
96 : @override
97 : final SourceLocation start;
98 : @override
99 : final SourceLocation end;
100 : @override
101 : final String text;
102 :
103 0 : SourceSpanBase(this.start, this.end, this.text) {
104 0 : if (end.sourceUrl != start.sourceUrl) {
105 0 : throw ArgumentError('Source URLs \"${start.sourceUrl}\" and '
106 0 : " \"${end.sourceUrl}\" don't match.");
107 0 : } else if (end.offset < start.offset) {
108 0 : throw ArgumentError('End $end must come after start $start.');
109 0 : } else if (text.length != start.distance(end)) {
110 0 : throw ArgumentError('Text "$text" must be ${start.distance(end)} '
111 : 'characters long.');
112 : }
113 : }
114 : }
115 :
116 : // TODO(#52): Move these to instance methods in the next breaking release.
117 : /// Extension methods on the base [SourceSpan] API.
118 : extension SourceSpanExtension on SourceSpan {
119 : /// Like [SourceSpan.message], but also highlights [secondarySpans] to provide
120 : /// the user with additional context.
121 : ///
122 : /// Each span takes a label ([label] for this span, and the values of the
123 : /// [secondarySpans] map for the secondary spans) that's used to indicate to
124 : /// the user what that particular span represents.
125 : ///
126 : /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
127 : /// the resulting string. By default this span is colored red and the
128 : /// secondary spans are colored blue, but that can be customized by passing
129 : /// ANSI escape strings to [primaryColor] or [secondaryColor].
130 : ///
131 : /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
132 : ///
133 : /// Each span in [secondarySpans] must refer to the same document as this
134 : /// span. Throws an [ArgumentError] if any secondary span has a different
135 : /// source URL than this span.
136 : ///
137 : /// Note that while this will work with plain [SourceSpan]s, it will produce
138 : /// much more useful output with [SourceSpanWithContext]s (including
139 : /// [FileSpan]s).
140 0 : String messageMultiple(
141 : String message, String label, Map<SourceSpan, String> secondarySpans,
142 : {bool color = false, String? primaryColor, String? secondaryColor}) {
143 0 : final buffer = StringBuffer()
144 0 : ..write('line ${start.line + 1}, column ${start.column + 1}');
145 0 : if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
146 : buffer
147 0 : ..writeln(': $message')
148 0 : ..write(highlightMultiple(label, secondarySpans,
149 : color: color,
150 : primaryColor: primaryColor,
151 : secondaryColor: secondaryColor));
152 0 : return buffer.toString();
153 : }
154 :
155 : /// Like [SourceSpan.highlight], but also highlights [secondarySpans] to
156 : /// provide the user with additional context.
157 : ///
158 : /// Each span takes a label ([label] for this span, and the values of the
159 : /// [secondarySpans] map for the secondary spans) that's used to indicate to
160 : /// the user what that particular span represents.
161 : ///
162 : /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
163 : /// the resulting string. By default this span is colored red and the
164 : /// secondary spans are colored blue, but that can be customized by passing
165 : /// ANSI escape strings to [primaryColor] or [secondaryColor].
166 : ///
167 : /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
168 : ///
169 : /// Each span in [secondarySpans] must refer to the same document as this
170 : /// span. Throws an [ArgumentError] if any secondary span has a different
171 : /// source URL than this span.
172 : ///
173 : /// Note that while this will work with plain [SourceSpan]s, it will produce
174 : /// much more useful output with [SourceSpanWithContext]s (including
175 : /// [FileSpan]s).
176 0 : String highlightMultiple(String label, Map<SourceSpan, String> secondarySpans,
177 : {bool color = false, String? primaryColor, String? secondaryColor}) =>
178 0 : Highlighter.multiple(this, label, secondarySpans,
179 : color: color,
180 : primaryColor: primaryColor,
181 : secondaryColor: secondaryColor)
182 0 : .highlight();
183 :
184 : /// Returns a span from [start] code units (inclusive) to [end] code units
185 : /// (exclusive) after the beginning of this span.
186 0 : SourceSpan subspan(int start, [int? end]) {
187 0 : RangeError.checkValidRange(start, end, length);
188 0 : if (start == 0 && (end == null || end == length)) return this;
189 :
190 0 : final text = this.text;
191 0 : final startLocation = this.start;
192 0 : var line = startLocation.line;
193 0 : var column = startLocation.column;
194 :
195 : // Adjust [line] and [column] as necessary if the character at [i] in [text]
196 : // is a newline.
197 0 : void consumeCodePoint(int i) {
198 0 : final codeUnit = text.codeUnitAt(i);
199 0 : if (codeUnit == $lf ||
200 : // A carriage return counts as a newline, but only if it's not
201 : // followed by a line feed.
202 0 : (codeUnit == $cr &&
203 0 : (i + 1 == text.length || text.codeUnitAt(i + 1) != $lf))) {
204 0 : line += 1;
205 : column = 0;
206 : } else {
207 0 : column += 1;
208 : }
209 : }
210 :
211 0 : for (var i = 0; i < start; i++) {
212 : consumeCodePoint(i);
213 : }
214 :
215 0 : final newStartLocation = SourceLocation(startLocation.offset + start,
216 0 : sourceUrl: sourceUrl, line: line, column: column);
217 :
218 : SourceLocation newEndLocation;
219 0 : if (end == null || end == length) {
220 0 : newEndLocation = this.end;
221 0 : } else if (end == start) {
222 : newEndLocation = newStartLocation;
223 : } else {
224 0 : for (var i = start; i < end; i++) {
225 : consumeCodePoint(i);
226 : }
227 0 : newEndLocation = SourceLocation(startLocation.offset + end,
228 0 : sourceUrl: sourceUrl, line: line, column: column);
229 : }
230 :
231 0 : return SourceSpan(
232 0 : newStartLocation, newEndLocation, text.substring(start, end));
233 : }
234 : }
|