namespace DotPython.Language.Text; public sealed class SourceText { private readonly int[] _lineStarts; public SourceText(string text, string? filePath = null) { ArgumentNullException.ThrowIfNull(text); Content = text; FilePath = filePath; _lineStarts = FindLineStarts(text); } public string Content { get; } public string? FilePath { get; } public int Length => Content.Length; public int LineCount => _lineStarts.Length; public char this[int index] => Content[index]; public string GetText(TextSpan span) { ValidateSpan(span); return Content.Substring(span.Start, span.Length); } public LinePosition GetLinePosition(int position) { ArgumentOutOfRangeException.ThrowIfNegative(position); ArgumentOutOfRangeException.ThrowIfGreaterThan(position, Length); var line = Array.BinarySearch(_lineStarts, position); if (line < 0) { line = ~line - 1; } return new LinePosition(line, position - _lineStarts[line]); } public TextSpan GetLineSpan(int line) { ArgumentOutOfRangeException.ThrowIfNegative(line); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(line, LineCount); var start = _lineStarts[line]; var end = line + 1 < LineCount ? _lineStarts[line + 1] : Length; return TextSpan.FromBounds(start, end); } private static int[] FindLineStarts(string text) { var starts = new List { 0 }; for (var position = 0; position < text.Length; position++) { switch (text[position]) { case '\r' when position + 1 < text.Length && text[position + 1] == '\n': position++; starts.Add(position + 1); break; case '\r': case '\n': starts.Add(position + 1); break; } } return [.. starts]; } private void ValidateSpan(TextSpan span) { if (span.End > Length) { throw new ArgumentOutOfRangeException(nameof(span)); } } }