// 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 other) => other is CharacterLocation && lineNumber == other.lineNumber && columnNumber == other.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 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 = [0]; var length = content.length; for (var i = 0; i < length; i++) { var unit = content.codeUnitAt(i); if (unit > slashR) continue; // 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 else 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); } /// Return the difference in line numbers between [offset1] and [offset2]. int lineNumberDifference(int offset1, int offset2) { return getLocation(offset2).lineNumber - getLocation(offset1).lineNumber; } /// Return whether both [offset1] and [offset2] are on the same line. bool onSameLine(int offset1, int offset2) { return lineNumberDifference(offset1, offset2) == 0; } /// Get the offset of the 0-indexed line [line] in [content]. /// /// If [line] does not exist it will return null. static int? getOffsetForLine(int line, String content) { if (line == 0) return 0; const slashN = 0x0A; const slashR = 0x0D; var length = content.length; var inLine = 0; for (var i = 0; i < length; i++) { var unit = content.codeUnitAt(i); if (unit > slashR) continue; // 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 { inLine++; if (inLine == line) return i + 1; } } // \n else if (unit == slashN) { inLine++; if (inLine == line) return i + 1; } } return null; } }