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