blob: ba8bec7152edc88ceaaacfefb5bf99fb909ec60f [file] [log] [blame]
// 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.
import 'package:path/path.dart' as p;
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'charcode.dart';
import 'file.dart';
import 'highlighter.dart';
import 'location.dart';
import 'span_mixin.dart';
import 'span_with_context.dart';
/// A class that describes a segment of source text.
abstract class SourceSpan implements Comparable<SourceSpan> {
/// The start location of this span.
SourceLocation get start;
/// The end location of this span, exclusive.
SourceLocation get end;
/// The source text for this span.
String get text;
/// The URL of the source (typically a file) of this span.
///
/// This may be null, indicating that the source URL is unknown or
/// unavailable.
Uri? get sourceUrl;
/// The length of this span, in characters.
int get length;
/// Creates a new span from [start] to [end] (exclusive) containing [text].
///
/// [start] and [end] must have the same source URL and [start] must come
/// before [end]. [text] must have a number of characters equal to the
/// distance between [start] and [end].
factory SourceSpan(SourceLocation start, SourceLocation end, String text) =>
SourceSpanBase(start, end, text);
/// Creates a new span that's the union of `this` and [other].
///
/// The two spans must have the same source URL and may not be disjoint.
/// [text] is computed by combining `this.text` and `other.text`.
SourceSpan union(SourceSpan other);
/// Compares two spans.
///
/// [other] must have the same source URL as `this`. This orders spans by
/// [start] then [length].
@override
int compareTo(SourceSpan other);
/// Formats [message] in a human-friendly way associated with this span.
///
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
/// it indicates an [ANSI terminal color escape][] that should
/// be used to highlight the span's text (for example, `"\u001b[31m"` will
/// color red). If it's `true`, it indicates that the text should be
/// highlighted using the default color. If it's `false` or `null`, it
/// indicates that the text shouldn't be highlighted.
///
/// This uses the full range of Unicode characters to highlight the source
/// span if [glyph.ascii] is `false` (the default), but only uses ASCII
/// characters if it's `true`.
///
/// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
String message(String message, {color});
/// Prints the text associated with this span in a user-friendly way.
///
/// This is identical to [message], except that it doesn't print the file
/// name, line number, column number, or message. If [length] is 0 and this
/// isn't a [SourceSpanWithContext], returns an empty string.
///
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
/// it indicates an [ANSI terminal color escape][] that should
/// be used to highlight the span's text (for example, `"\u001b[31m"` will
/// color red). If it's `true`, it indicates that the text should be
/// highlighted using the default color. If it's `false` or `null`, it
/// indicates that the text shouldn't be highlighted.
///
/// This uses the full range of Unicode characters to highlight the source
/// span if [glyph.ascii] is `false` (the default), but only uses ASCII
/// characters if it's `true`.
///
/// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
String highlight({color});
}
/// A base class for source spans with [start], [end], and [text] known at
/// construction time.
class SourceSpanBase extends SourceSpanMixin {
@override
final SourceLocation start;
@override
final SourceLocation end;
@override
final String text;
SourceSpanBase(this.start, this.end, this.text) {
if (end.sourceUrl != start.sourceUrl) {
throw ArgumentError('Source URLs \"${start.sourceUrl}\" and '
" \"${end.sourceUrl}\" don't match.");
} else if (end.offset < start.offset) {
throw ArgumentError('End $end must come after start $start.');
} else if (text.length != start.distance(end)) {
throw ArgumentError('Text "$text" must be ${start.distance(end)} '
'characters long.');
}
}
}
// TODO(#52): Move these to instance methods in the next breaking release.
/// Extension methods on the base [SourceSpan] API.
extension SourceSpanExtension on SourceSpan {
/// Like [SourceSpan.message], but also highlights [secondarySpans] to provide
/// the user with additional context.
///
/// Each span takes a label ([label] for this span, and the values of the
/// [secondarySpans] map for the secondary spans) that's used to indicate to
/// the user what that particular span represents.
///
/// If [color] is `true`, [ANSI terminal color escapes][] are used to color
/// the resulting string. By default this span is colored red and the
/// secondary spans are colored blue, but that can be customized by passing
/// ANSI escape strings to [primaryColor] or [secondaryColor].
///
/// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
///
/// Each span in [secondarySpans] must refer to the same document as this
/// span. Throws an [ArgumentError] if any secondary span has a different
/// source URL than this span.
///
/// Note that while this will work with plain [SourceSpan]s, it will produce
/// much more useful output with [SourceSpanWithContext]s (including
/// [FileSpan]s).
String messageMultiple(
String message, String label, Map<SourceSpan, String> secondarySpans,
{bool color = false, String? primaryColor, String? secondaryColor}) {
final buffer = StringBuffer()
..write('line ${start.line + 1}, column ${start.column + 1}');
if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
buffer
..writeln(': $message')
..write(highlightMultiple(label, secondarySpans,
color: color,
primaryColor: primaryColor,
secondaryColor: secondaryColor));
return buffer.toString();
}
/// Like [SourceSpan.highlight], but also highlights [secondarySpans] to
/// provide the user with additional context.
///
/// Each span takes a label ([label] for this span, and the values of the
/// [secondarySpans] map for the secondary spans) that's used to indicate to
/// the user what that particular span represents.
///
/// If [color] is `true`, [ANSI terminal color escapes][] are used to color
/// the resulting string. By default this span is colored red and the
/// secondary spans are colored blue, but that can be customized by passing
/// ANSI escape strings to [primaryColor] or [secondaryColor].
///
/// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
///
/// Each span in [secondarySpans] must refer to the same document as this
/// span. Throws an [ArgumentError] if any secondary span has a different
/// source URL than this span.
///
/// Note that while this will work with plain [SourceSpan]s, it will produce
/// much more useful output with [SourceSpanWithContext]s (including
/// [FileSpan]s).
String highlightMultiple(String label, Map<SourceSpan, String> secondarySpans,
{bool color = false, String? primaryColor, String? secondaryColor}) =>
Highlighter.multiple(this, label, secondarySpans,
color: color,
primaryColor: primaryColor,
secondaryColor: secondaryColor)
.highlight();
/// Returns a span from [start] code units (inclusive) to [end] code units
/// (exclusive) after the beginning of this span.
SourceSpan subspan(int start, [int? end]) {
RangeError.checkValidRange(start, end, length);
if (start == 0 && (end == null || end == length)) return this;
final text = this.text;
final startLocation = this.start;
var line = startLocation.line;
var column = startLocation.column;
// Adjust [line] and [column] as necessary if the character at [i] in [text]
// is a newline.
void consumeCodePoint(int i) {
final codeUnit = text.codeUnitAt(i);
if (codeUnit == $lf ||
// A carriage return counts as a newline, but only if it's not
// followed by a line feed.
(codeUnit == $cr &&
(i + 1 == text.length || text.codeUnitAt(i + 1) != $lf))) {
line += 1;
column = 0;
} else {
column += 1;
}
}
for (var i = 0; i < start; i++) {
consumeCodePoint(i);
}
final newStartLocation = SourceLocation(startLocation.offset + start,
sourceUrl: sourceUrl, line: line, column: column);
SourceLocation newEndLocation;
if (end == null || end == length) {
newEndLocation = this.end;
} else if (end == start) {
newEndLocation = newStartLocation;
} else {
for (var i = start; i < end; i++) {
consumeCodePoint(i);
}
newEndLocation = SourceLocation(startLocation.offset + end,
sourceUrl: sourceUrl, line: line, column: column);
}
return SourceSpan(
newStartLocation, newEndLocation, text.substring(start, end));
}
}