| File: Text\SubText.cs | Web Access |
| Project: src\roslyn\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Diagnostics; using System.Linq; using System.Text; namespace Microsoft.CodeAnalysis.Text { /// <summary> /// A <see cref="SourceText"/> that represents a subrange of another <see cref="SourceText"/>. /// </summary> internal sealed class SubText : SourceText { public SubText(SourceText text, TextSpan span) : base(checksumAlgorithm: text.ChecksumAlgorithm) { if (text == null) { throw new ArgumentNullException(nameof(text)); } // span.Start and span.End are valid wrt eachother by nature of being passed in as a TextSpan, // so there is no need to verify span.Start against text.Length or span.End against zero. if (span.Start < 0 || span.End > text.Length) { throw new ArgumentOutOfRangeException(nameof(span)); } UnderlyingText = text; UnderlyingSpan = span; } public override Encoding? Encoding => UnderlyingText.Encoding; public SourceText UnderlyingText { get; } public TextSpan UnderlyingSpan { get; } public override int Length => UnderlyingSpan.Length; internal override int StorageSize { get { return this.UnderlyingText.StorageSize; } } internal override SourceText StorageKey { get { return this.UnderlyingText.StorageKey; } } public override char this[int position] { get { if (position < 0 || position > this.Length) { throw new ArgumentOutOfRangeException(nameof(position)); } return UnderlyingText[UnderlyingSpan.Start + position]; } } protected override TextLineCollection GetLinesCore() => new SubTextLineInfo(this); public override string ToString(TextSpan span) { CheckSubSpan(span); return UnderlyingText.ToString(GetCompositeSpan(span.Start, span.Length)); } public override SourceText GetSubText(TextSpan span) { CheckSubSpan(span); return new SubText(UnderlyingText, GetCompositeSpan(span.Start, span.Length)); } public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) { var span = GetCompositeSpan(sourceIndex, count); UnderlyingText.CopyTo(span.Start, destination, destinationIndex, span.Length); } private TextSpan GetCompositeSpan(int start, int length) { int compositeStart = Math.Min(UnderlyingText.Length, UnderlyingSpan.Start + start); int compositeEnd = Math.Min(UnderlyingText.Length, compositeStart + length); return new TextSpan(compositeStart, compositeEnd - compositeStart); } /// <summary> /// Delegates to the SubText's <see cref="UnderlyingText"/> to determine line information. /// </summary> private sealed class SubTextLineInfo : TextLineCollection { private readonly SubText _subText; private readonly int _startLineNumberInUnderlyingText; private readonly int _lineCount; private readonly bool _endsWithinSplitCRLF; public SubTextLineInfo(SubText subText) { _subText = subText; var startLineInUnderlyingText = _subText.UnderlyingText.Lines.GetLineFromPosition(_subText.UnderlyingSpan.Start); var endLineInUnderlyingText = _subText.UnderlyingText.Lines.GetLineFromPosition(_subText.UnderlyingSpan.End); _startLineNumberInUnderlyingText = startLineInUnderlyingText.LineNumber; _lineCount = (endLineInUnderlyingText.LineNumber - _startLineNumberInUnderlyingText) + 1; var underlyingSpanEnd = _subText.UnderlyingSpan.End; if (underlyingSpanEnd == endLineInUnderlyingText.End + 1 && underlyingSpanEnd == endLineInUnderlyingText.EndIncludingLineBreak - 1) { Debug.Assert(_subText.UnderlyingText[underlyingSpanEnd - 1] == '\r' && _subText.UnderlyingText[underlyingSpanEnd] == '\n'); _endsWithinSplitCRLF = true; // If this subtext ends in the middle of a CR/LF, then this object should view that CR as a separate line // whereas the UnderlyingText would not. _lineCount += 1; } } public override TextLine this[int lineNumber] { get { if (lineNumber < 0 || lineNumber >= _lineCount) { throw new ArgumentOutOfRangeException(nameof(lineNumber)); } if (_endsWithinSplitCRLF && lineNumber == _lineCount - 1) { // Special case splitting the CRLF at the end as the UnderlyingText doesn't view the position // after between the \r and \n as on a new line whereas this subtext doesn't contain the \n // and needs to view that position as on a new line. return TextLine.FromSpanUnsafe(_subText, new TextSpan(_subText.UnderlyingSpan.Length, 0), lineBreakLength: 0); } var underlyingTextLine = _subText.UnderlyingText.Lines[lineNumber + _startLineNumberInUnderlyingText]; // Consider input "a\r\nb" where ST1 contains "\a\r" and ST2 contains "\n\b", and requested lineNumber // per this table: // ---------------------------------------------------------------------------------------------------------------- // | SubText | lineNumber | underlyingTextLine | _subText | underlyingTextLine | _subText | // | | | .Start | .UnderlyingSpan | .EndIncludingLineBreak | .UnderlyingSpan | // | | | | .Start | | .End | // |--------------------------------------------------------------------------------------------------------------- // | ST1 | 0 | 0 | 0 | 3 | 2 | // | ST2 | 0 | 0 | 2 | 3 | 4 | // | ST2 | 1 | 3 | 2 | 4 | 4 | // ---------------------------------------------------------------------------------------------------------------- // These two variables represent this subtext's view on the start/end of the requested line, // but in the coordinate space of _subText.UnderlyingText. var startInUnderlyingText = Math.Max(underlyingTextLine.Start, _subText.UnderlyingSpan.Start); var endInUnderlyingText = Math.Min(underlyingTextLine.EndIncludingLineBreak, _subText.UnderlyingSpan.End); // This variable represent this subtext's view on start of the requested line, // in it's coordinate space var startInSubText = startInUnderlyingText - _subText.UnderlyingSpan.Start; var length = endInUnderlyingText - startInUnderlyingText; // How much of the underlying line's break region is visible in this subtext? // The break region in underlying coordinates is [underlyingTextLine.End .. endInUnderlyingText). // If our subtext starts inside the break (e.g. the \n half of a split \r\n), clamp the start. var breakRegionStart = Math.Max(underlyingTextLine.End, startInUnderlyingText); var lineBreakLen = Math.Max(0, endInUnderlyingText - breakRegionStart); var resultLine = TextLine.FromSpanUnsafe(_subText, new TextSpan(startInSubText, length), lineBreakLen); var shouldContainLineBreak = (lineNumber != _lineCount - 1); var resultContainsLineBreak = resultLine.EndIncludingLineBreak > resultLine.End; if (shouldContainLineBreak != resultContainsLineBreak) { throw new InvalidOperationException(); } // Assert resultLine only has line breaks in the appropriate locations Debug.Assert(resultLine.ToString().All(static c => !TextUtilities.IsAnyLineBreakCharacter(c))); return resultLine; } } public override int Count => _lineCount; /// <summary> /// Determines the line number of a position in this SubText /// </summary> public override int IndexOf(int position) { if (position < 0 || position > _subText.UnderlyingSpan.Length) { throw new ArgumentOutOfRangeException(nameof(position)); } var underlyingPosition = position + _subText.UnderlyingSpan.Start; var underlyingLineNumber = _subText.UnderlyingText.Lines.IndexOf(underlyingPosition); return underlyingLineNumber - _startLineNumberInUnderlyingText; } } } }