File: Text\SubText.cs
Web Access
Project: src\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));
            }
 
            if (span.Start < 0
                || span.Start >= text.Length
                || span.End < 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 _startsWithinSplitCRLF;
            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 underlyingSpanStart = _subText.UnderlyingSpan.Start;
                if (underlyingSpanStart == startLineInUnderlyingText.End + 1 &&
                    underlyingSpanStart == startLineInUnderlyingText.EndIncludingLineBreak - 1)
                {
                    Debug.Assert(_subText.UnderlyingText[underlyingSpanStart - 1] == '\r' && _subText.UnderlyingText[underlyingSpanStart] == '\n');
                    _startsWithinSplitCRLF = true;
                }
 
                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.End, 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;
                    var resultLine = TextLine.FromSpanUnsafe(_subText, new TextSpan(startInSubText, length));
 
                    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);
 
                if (_startsWithinSplitCRLF && position != 0)
                {
                    // The \n contributes a line to the count in this subtext, but not in the UnderlyingText.
                    underlyingLineNumber += 1;
                }
 
                return underlyingLineNumber - _startLineNumberInUnderlyingText;
            }
        }
    }
}