File: Text\TextLine.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 Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Text
{
    /// <summary>
    /// Information about the character boundaries of a single line of text.
    /// </summary>
    public readonly struct TextLine : IEquatable<TextLine>
    {
        private readonly SourceText? _text;
 
        // Encoding (64 bits total):
        //   Bits 63-62  (2 bits): line break length at the end of this line (0 = no break / last line,
        //                         1 = single-char break like \n, \r, etc., 2 = the \r\n Windows pair)
        //   Bits 61-31  (31 bits): start position
        //   Bits 30-0   (31 bits): total length including break = EndIncludingLineBreak - Start
        private readonly ulong _data;
 
        private const int BreakLenShift = 62;
        private const int StartShift = 31;
        private const ulong RawValueMask = 0x7FFFFFFFUL; // 31 bits, used for both start and length
 
        private TextLine(SourceText text, ulong data)
        {
            _text = text;
            _data = data;
        }
 
        private static ulong Pack(int start, int totalLength, int lineBreakLength)
            => ((ulong)(uint)lineBreakLength << BreakLenShift) | ((ulong)(uint)start << StartShift) | (uint)totalLength;
 
        /// <summary>
        /// Creates a <see cref="TextLine"/> instance.
        /// </summary>
        /// <param name="text">The source text.</param>
        /// <param name="span">The span of the line.</param>
        /// <returns>An instance of <see cref="TextLine"/>.</returns>
        /// <exception cref="ArgumentOutOfRangeException">The span does not represent a text line.</exception>
        public static TextLine FromSpan(SourceText text, TextSpan span)
        {
            if (text == null)
            {
                throw new ArgumentNullException(nameof(text));
            }
 
            if (span.Start > text.Length || span.Start < 0 || span.End > text.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(span));
            }
 
            if (text.Length > 0)
            {
                // check span is start of line
                if (span.Start > 0 && !TextUtilities.IsAnyLineBreakCharacter(text[span.Start - 1]))
                {
                    throw new ArgumentOutOfRangeException(nameof(span), CodeAnalysisResources.SpanDoesNotIncludeStartOfLine);
                }
 
                var lineBreakLen = 0;
                if (span.End > span.Start && TextUtilities.IsAnyLineBreakCharacter(text[span.End - 1]))
                {
                    // End already includes line break - determine its length
                    TextUtilities.GetStartAndLengthOfLineBreakEndingAt(text, span.End - 1, out _, out lineBreakLen);
                }
                else if (span.End < text.Length)
                {
                    lineBreakLen = TextUtilities.GetLengthOfLineBreak(text, span.End);
                    if (lineBreakLen > 0)
                    {
                        // adjust span to include line breaks
                        span = new TextSpan(span.Start, span.Length + lineBreakLen);
                    }
                }
 
                // check end of span is at end of line
                if (span.End < text.Length && lineBreakLen == 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(span), CodeAnalysisResources.SpanDoesNotIncludeEndOfLine);
                }
 
                return new TextLine(text, Pack(span.Start, span.Length, lineBreakLen));
            }
            else
            {
                return new TextLine(text, Pack(0, 0, 0));
            }
        }
 
        // Do not use unless you are certain the span you are passing in is valid!
        // This was added to allow SourceText.LineInfo's indexer to directly create TextLines
        // without the performance implications of calling FromSpan.
        internal static TextLine FromSpanUnsafe(SourceText text, TextSpan span, int lineBreakLength)
        {
            Debug.Assert(span.Start == 0 || TextUtilities.IsAnyLineBreakCharacter(text[span.Start - 1]));
            Debug.Assert(span.End == text.Length || TextUtilities.IsAnyLineBreakCharacter(text[span.End - 1]));
            Debug.Assert(lineBreakLength is >= 0 and <= 2);
            return new TextLine(text, Pack(span.Start, span.Length, lineBreakLength));
        }
 
        /// <summary>
        /// Gets the source text.
        /// </summary>
        public SourceText? Text
        {
            get { return _text; }
        }
 
        /// <summary>
        /// Gets the zero-based line number.
        /// </summary>
        public int LineNumber
        {
            get
            {
                return _text?.Lines.IndexOf(Start) ?? 0;
            }
        }
 
        /// <summary>
        /// Gets the start position of the line.
        /// </summary>
        public int Start
        {
            get { return (int)((_data >>> StartShift) & RawValueMask); }
        }
 
        /// <summary>
        /// Gets the length of the line break sequence at the end of this line (0, 1, or 2).
        /// </summary>
        internal int LineBreakLength
        {
            get { return (int)(_data >>> BreakLenShift); }
        }
 
        /// <summary>
        /// Gets the end position of the line not including the line break.
        /// </summary>
        public int End
        {
            get { return EndIncludingLineBreak - LineBreakLength; }
        }
 
        /// <summary>
        /// Gets the end position of the line including the line break.
        /// </summary>
        public int EndIncludingLineBreak
        {
            get { return Start + (int)(_data & RawValueMask); }
        }
 
        /// <summary>
        /// Gets the line span not including the line break.
        /// </summary>
        public TextSpan Span
        {
            get { return TextSpan.FromBounds(this.Start, this.End); }
        }
 
        /// <summary>
        /// Gets the line span including the line break.
        /// </summary>
        public TextSpan SpanIncludingLineBreak
        {
            get { return TextSpan.FromBounds(this.Start, this.EndIncludingLineBreak); }
        }
 
        public override string ToString()
        {
            if (_text == null || _text.Length == 0)
            {
                return string.Empty;
            }
            else
            {
                return _text.ToString(this.Span);
            }
        }
 
        public static bool operator ==(TextLine left, TextLine right)
        {
            return left.Equals(right);
        }
 
        public static bool operator !=(TextLine left, TextLine right)
        {
            return !left.Equals(right);
        }
 
        public bool Equals(TextLine other)
        {
            return other._text == _text && other._data == _data;
        }
 
        public override bool Equals(object? obj)
        {
            if (obj is TextLine)
            {
                return Equals((TextLine)obj);
            }
 
            return false;
        }
 
        public override int GetHashCode()
        {
            return Hash.Combine(_text, Hash.Combine((int)_data, (int)(_data >>> 32)));
        }
    }
}