| // 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<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); | |
| 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; | |
| } | |
| } |