blob: 41ce1acdac3d566aeff66ac2eae8eae8806ddb4f [file] [log] [blame]
// Copyright (c) 2018, 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.
/// The location of a character represented as a line and column pair.
class CharacterLocation {
/// The one-based index of the line containing the character.
final int lineNumber;
/// The one-based index of the column containing the character.
final int columnNumber;
/// Initialize a newly created location to represent the location of the
/// character at the given [lineNumber] and [columnNumber].
CharacterLocation(this.lineNumber, this.columnNumber);
@override
bool operator ==(Object object) =>
object is CharacterLocation &&
lineNumber == object.lineNumber &&
columnNumber == object.columnNumber;
@override
String toString() => '$lineNumber:$columnNumber';
}
/// Information about line and column information within a source file.
class LineInfo {
/// A list containing the offsets of the first character of each line in the
/// source code.
final List<int> lineStarts;
/// The zero-based [lineStarts] index resulting from the last call to
/// [getLocation].
int _previousLine = 0;
/// Initialize a newly created set of line information to represent the data
/// encoded in the given list of [lineStarts].
LineInfo(this.lineStarts) {
if (lineStarts.isEmpty) {
throw ArgumentError("lineStarts must be non-empty");
}
}
/// Initialize a newly created set of line information corresponding to the
/// given file [content]. Lines end with `\r`, `\n` or `\r\n`.
factory LineInfo.fromContent(String content) {
const slashN = 0x0A;
const slashR = 0x0D;
var lineStarts = <int>[0];
var length = content.length;
for (var i = 0; i < length; i++) {
var unit = content.codeUnitAt(i);
// Special-case \r\n.
if (unit == slashR) {
// Peek ahead to detect a following \n.
if (i + 1 < length && content.codeUnitAt(i + 1) == slashN) {
// Line start will get registered at next index at the \n.
} else {
lineStarts.add(i + 1);
}
}
// \n
if (unit == slashN) {
lineStarts.add(i + 1);
}
}
return LineInfo(lineStarts);
}
/// The number of lines.
int get lineCount => lineStarts.length;
/// Return the location information for the character at the given [offset].
CharacterLocation getLocation(int offset) {
var min = 0;
var max = lineStarts.length - 1;
// Subsequent calls to [getLocation] are often for offsets near each other.
// To take advantage of that, we cache the index of the line start we found
// when this was last called. If the current offset is on that line or
// later, we'll skip those early indices completely when searching.
if (offset >= lineStarts[_previousLine]) {
min = _previousLine;
// Before kicking off a full binary search, do a quick check here to see
// if the new offset is on that exact line.
if (min == lineStarts.length - 1 || offset < lineStarts[min + 1]) {
return CharacterLocation(min + 1, offset - lineStarts[min] + 1);
}
}
// Binary search to find the line containing this offset.
while (min < max) {
var midpoint = (max - min + 1) ~/ 2 + min;
if (lineStarts[midpoint] > offset) {
max = midpoint - 1;
} else {
min = midpoint;
}
}
_previousLine = min;
return CharacterLocation(min + 1, offset - lineStarts[min] + 1);
}
/// Return the offset of the first character on the line with the given
/// [lineNumber].
int getOffsetOfLine(int lineNumber) {
if (lineNumber < 0 || lineNumber >= lineCount) {
throw ArgumentError(
'Invalid line number: $lineNumber; must be between 0 and ${lineCount - 1}');
}
return lineStarts[lineNumber];
}
/// Return the offset of the first character on the line following the line
/// containing the given [offset].
int getOffsetOfLineAfter(int offset) {
return getOffsetOfLine(getLocation(offset).lineNumber);
}
}