diff --git a/src/Compilers/Core/CodeAnalysisTest/Text/CompositeTextTests.cs b/src/Compilers/Core/CodeAnalysisTest/Text/CompositeTextTests.cs new file mode 100644 index 0000000000000..5be719081f2be --- /dev/null +++ b/src/Compilers/Core/CodeAnalysisTest/Text/CompositeTextTests.cs @@ -0,0 +1,90 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.Text; + +public sealed class CompositeTextTests +{ + [Theory] + [InlineData("abcdefghijkl")] + [InlineData(["\r\r\r\r\r\r\r\r\r\r\r\r"])] + [InlineData(["\n\n\n\n\n\n\n\n\n\n\n\n"])] + [InlineData(["\r\n\r\n\r\n\r\n\r\n\r\n"])] + [InlineData(["\n\r\n\r\n\r\n\r\n\r\n\r"])] + [InlineData(["a\r\nb\r\nc\r\nd\r\n"])] + [InlineData(["\ra\n\rb\n\rc\n\rd\n"])] + [InlineData(["\na\r\nb\r\nc\r\nd\r"])] + [InlineData(["ab\r\ncd\r\nef\r\n"])] + [InlineData(["ab\r\r\ncd\r\r\nef"])] + [InlineData(["ab\n\n\rcd\n\n\ref"])] + [InlineData(["ab\u0085cdef\u2028ijkl\u2029op"])] + [InlineData(["\u0085\u2028\u2029\u0085\u2028\u2029\u0085\u2028\u2029\u0085\u2028\u2029"])] + public void CompositeTextLinesEqualSourceTextLinesPermutations(string contents) + { + // Please try to limit the inputs to this method to around 12 chars or less, as much longer than that + // will blow up the number of potential permutations. + foreach (var (sourceText, compositeText) in CreateSourceAndCompositeTexts(contents)) + { + var sourceLinesText = GetLinesTexts(sourceText.Lines); + var compositeLinesText = GetLinesTexts(compositeText.Lines); + + Assert.True(sourceLinesText.SequenceEqual(compositeLinesText)); + + for (var i = 0; i < sourceText.Length; i++) + { + Assert.Equal(sourceText.Lines.IndexOf(i), compositeText.Lines.IndexOf(i)); + } + } + } + + private static IEnumerable GetLinesTexts(TextLineCollection textLines) + { + return textLines.Select(l => l.Text!.ToString(l.SpanIncludingLineBreak)); + } + + // Returns all possible permutations of contents into SourceText arrays of length between minSourceTextCount and maxSourceTextCount + private static IEnumerable<(SourceText, CompositeText)> CreateSourceAndCompositeTexts(string contents, int minSourceTextCount = 2, int maxSourceTextCount = 4) + { + var sourceText = SourceText.From(contents); + + for (var sourceTextCount = minSourceTextCount; sourceTextCount <= Math.Min(maxSourceTextCount, contents.Length); sourceTextCount++) + { + foreach (var sourceTexts in CreateSourceTextPermutations(contents, sourceTextCount)) + { + var sourceTextsBuilder = ArrayBuilder.GetInstance(); + sourceTextsBuilder.AddRange(sourceTexts); + + var compositeText = (CompositeText)CompositeText.ToSourceText(sourceTextsBuilder, sourceText, adjustSegments: false); + yield return (sourceText, compositeText); + } + } + } + + private static IEnumerable CreateSourceTextPermutations(string contents, int requestedSourceTextCount) + { + if (requestedSourceTextCount == 1) + { + yield return [SourceText.From(contents)]; + } + else + { + var maximalSourceTextLength = (contents.Length - requestedSourceTextCount) + 1; + for (int i = 1; i <= maximalSourceTextLength; i++) + { + var sourceText = SourceText.From(contents[..i]); + foreach (var otherSourceTexts in CreateSourceTextPermutations(contents.Substring(i), requestedSourceTextCount - 1)) + { + yield return [sourceText, .. otherSourceTexts]; + } + } + } + } +} diff --git a/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs b/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs index 19b2d90213633..ba0f572cbe928 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs @@ -320,14 +320,14 @@ public void TestOptimizedSourceTextLinesRemoveCrLf() } [Fact] - public void TestOptimizedSourceTextLinesBrakeCrLf() + public void TestOptimizedSourceTextLinesBreakCrLf() { AssertChangedTextLinesHelper("Test\r\nMessage", new TextChange(new TextSpan(5, 0), "aaaaaa")); } [Fact] - public void TestOptimizedSourceTextLinesBrakeCrLfWithLfPrefixedAndCrSuffixed() + public void TestOptimizedSourceTextLinesBreakCrLfWithLfPrefixedAndCrSuffixed() { AssertChangedTextLinesHelper("Test\r\nMessage", new TextChange(new TextSpan(5, 0), "\naaaaaa\r")); diff --git a/src/Compilers/Core/Portable/Text/ChangedText.cs b/src/Compilers/Core/Portable/Text/ChangedText.cs index c8f7d46b8d71d..acd1e57c4ea04 100644 --- a/src/Compilers/Core/Portable/Text/ChangedText.cs +++ b/src/Compilers/Core/Portable/Text/ChangedText.cs @@ -7,8 +7,6 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Text; -using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Text @@ -264,108 +262,8 @@ private static ImmutableArray Merge(IReadOnlyList - /// Computes line starts faster given already computed line starts from text before the change. - /// protected override TextLineCollection GetLinesCore() - { - SourceText? oldText; - TextLineCollection? oldLineInfo; - - if (!_info.WeakOldText.TryGetTarget(out oldText) || !oldText.TryGetLines(out oldLineInfo)) - { - // no old line starts? do it the hard way. - return base.GetLinesCore(); - } - - // compute line starts given changes and line starts already computed from previous text - var lineStarts = new SegmentedList(capacity: oldLineInfo.Count) - { - 0 - }; - - // position in the original document - var position = 0; - - // delta generated by already processed changes (position in the new document = position + delta) - var delta = 0; - - // true if last segment ends with CR and we need to check for CR+LF code below assumes that both CR and LF are also line breaks alone - var endsWithCR = false; - - foreach (var change in _info.ChangeRanges) - { - // include existing line starts that occur before this change - if (change.Span.Start > position) - { - if (endsWithCR && _newText[position + delta] == '\n') - { - // remove last added line start (it was due to previous CR) - // a new line start including the LF will be added next - lineStarts.RemoveAt(lineStarts.Count - 1); - } - - var lps = oldLineInfo.GetLinePositionSpan(TextSpan.FromBounds(position, change.Span.Start)); - for (int i = lps.Start.Line + 1; i <= lps.End.Line; i++) - { - lineStarts.Add(oldLineInfo[i].Start + delta); - } - - endsWithCR = oldText[change.Span.Start - 1] == '\r'; - - // in case change is inserted between CR+LF we treat CR as line break alone, - // but this line break might be retracted and replaced with new one in case LF is inserted - if (endsWithCR && change.Span.Start < oldText.Length && oldText[change.Span.Start] == '\n') - { - lineStarts.Add(change.Span.Start + delta); - } - } - - // include line starts that occur within newly inserted text - if (change.NewLength > 0) - { - var changeStart = change.Span.Start + delta; - var text = GetSubText(new TextSpan(changeStart, change.NewLength)); - - if (endsWithCR && text[0] == '\n') - { - // remove last added line start (it was due to previous CR) - // a new line start including the LF will be added next - lineStarts.RemoveAt(lineStarts.Count - 1); - } - - // Skip first line (it is always at offset 0 and corresponds to the previous line) - for (int i = 1; i < text.Lines.Count; i++) - { - lineStarts.Add(changeStart + text.Lines[i].Start); - } - - endsWithCR = text[change.NewLength - 1] == '\r'; - } - - position = change.Span.End; - delta += (change.NewLength - change.Span.Length); - } - - // include existing line starts that occur after all changes - if (position < oldText.Length) - { - if (endsWithCR && _newText[position + delta] == '\n') - { - // remove last added line start (it was due to previous CR) - // a new line start including the LF will be added next - lineStarts.RemoveAt(lineStarts.Count - 1); - } - - var lps = oldLineInfo.GetLinePositionSpan(TextSpan.FromBounds(position, oldText.Length)); - for (int i = lps.Start.Line + 1; i <= lps.End.Line; i++) - { - lineStarts.Add(oldLineInfo[i].Start + delta); - } - } - - return new LineInfo(this, lineStarts); - } + => _newText.Lines; internal static class TestAccessor { diff --git a/src/Compilers/Core/Portable/Text/CompositeText.cs b/src/Compilers/Core/Portable/Text/CompositeText.cs index d72ccf5f8d7c9..b4fdc92beec36 100644 --- a/src/Compilers/Core/Portable/Text/CompositeText.cs +++ b/src/Compilers/Core/Portable/Text/CompositeText.cs @@ -6,8 +6,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -29,6 +29,7 @@ private CompositeText(ImmutableArray segments, Encoding? encoding, S : base(checksumAlgorithm: checksumAlgorithm) { Debug.Assert(!segments.IsDefaultOrEmpty); + Debug.Assert(segments.Length > 0); _segments = segments; _encoding = encoding; @@ -40,10 +41,14 @@ private CompositeText(ImmutableArray segments, Encoding? encoding, S for (int i = 0; i < _segmentOffsets.Length; i++) { _segmentOffsets[i] = offset; + Debug.Assert(_segments[i].Length > 0); offset += _segments[i].Length; } } + protected override TextLineCollection GetLinesCore() + => new CompositeTextLineInfo(this); + public override Encoding? Encoding { get { return _encoding; } @@ -182,6 +187,8 @@ internal static SourceText ToSourceText(ArrayBuilder segments, Sourc ReduceSegmentCountIfNecessary(segments); } + RemoveSplitLineBreaksAndEmptySegments(segments); + if (segments.Count == 0) { return SourceText.From(string.Empty, original.Encoding, original.ChecksumAlgorithm); @@ -196,6 +203,38 @@ internal static SourceText ToSourceText(ArrayBuilder segments, Sourc } } + private static void RemoveSplitLineBreaksAndEmptySegments(ArrayBuilder segments) + { + if (segments.Count > 1) + { + // Remove empty segments before checking for split line breaks + segments.RemoveWhere(static (s, _, _) => s.Length == 0, default(VoidResult)); + + var splitLineBreakFound = false; + for (int i = 1; i < segments.Count; i++) + { + var prevSegment = segments[i - 1]; + var curSegment = segments[i]; + if (prevSegment.Length > 0 && prevSegment[^1] == '\r' && curSegment[0] == '\n') + { + splitLineBreakFound = true; + + segments[i - 1] = prevSegment.GetSubText(new TextSpan(0, prevSegment.Length - 1)); + segments.Insert(i, SourceText.From("\r\n")); + segments[i + 1] = curSegment.GetSubText(new TextSpan(1, curSegment.Length - 1)); + i++; + } + } + + if (splitLineBreakFound) + { + // If a split line break was present, ensure there aren't any empty lines again + // due to the sourcetexts created from the GetSubText calls. + segments.RemoveWhere(static (s, _, _) => s.Length == 0, default(VoidResult)); + } + } + } + // both of these numbers are currently arbitrary. internal const int TARGET_SEGMENT_COUNT_AFTER_REDUCTION = 32; internal const int MAXIMUM_SEGMENT_COUNT_BEFORE_REDUCTION = 64; @@ -373,5 +412,161 @@ private static void TrimInaccessibleText(ArrayBuilder segments) segments.Add(writer.ToSourceText()); } } + + /// + /// Delegates to SourceTexts within the CompositeText to determine line information. + /// + private sealed class CompositeTextLineInfo : TextLineCollection + { + private readonly CompositeText _compositeText; + + /// + /// The starting line number for the correspondingly indexed SourceTexts in _compositeText.Segments. + /// Multiple consecutive entries could indicate the same line number if the corresponding + /// segments don't contain newline characters. + /// + /// + /// This will be of the same length as _compositeText.Segments + /// + private readonly ImmutableArray _segmentLineNumbers; + + // The total number of lines in our _compositeText + private readonly int _lineCount; + + public CompositeTextLineInfo(CompositeText compositeText) + { + var segmentLineNumbers = new int[compositeText.Segments.Length]; + var accumulatedLineCount = 0; + + Debug.Assert(compositeText.Segments.Length > 0); + for (int i = 0; i < compositeText.Segments.Length; i++) + { + segmentLineNumbers[i] = accumulatedLineCount; + + var segment = compositeText.Segments[i]; + + // Account for this segments lines in our accumulated lines. Subtract one as each segment + // views its line count as one greater than the number of line breaks it contains. + accumulatedLineCount += (segment.Lines.Count - 1); + + Debug.Assert(segment.Length > 0); + + // RemoveSplitLineBreaksAndEmptySegments ensured no split line breaks + Debug.Assert(i == compositeText.Segments.Length - 1 || segment[^1] != '\r' || compositeText.Segments[i + 1][0] != '\n'); + } + + _compositeText = compositeText; + _segmentLineNumbers = ImmutableCollectionsMarshal.AsImmutableArray(segmentLineNumbers); + + // Add one to the accumulatedLineCount for our stored line count (so that we maintain the + // invariant that a text's line count is one greater than the number of newlines it contains) + _lineCount = accumulatedLineCount + 1; + } + + public override int Count => _lineCount; + + /// + /// Determines the line number of a position in this CompositeText + /// + public override int IndexOf(int position) + { + if (position < 0 || position > _compositeText.Length) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + _compositeText.GetIndexAndOffset(position, out var segmentIndex, out var segmentOffset); + + var segment = _compositeText.Segments[segmentIndex]; + var lineNumberWithinSegment = segment.Lines.IndexOf(segmentOffset); + + return _segmentLineNumbers[segmentIndex] + lineNumberWithinSegment; + } + + public override TextLine this[int lineNumber] + { + get + { + if (lineNumber < 0 || lineNumber >= _lineCount) + { + throw new ArgumentOutOfRangeException(nameof(lineNumber)); + } + + // Determine the indices for segments that contribute to our view of the requested line's contents + GetSegmentIndexRangeContainingLine(lineNumber, out var firstSegmentIndexInclusive, out var lastSegmentIndexInclusive); + Debug.Assert(firstSegmentIndexInclusive <= lastSegmentIndexInclusive); + + var firstSegmentFirstLineNumber = _segmentLineNumbers[firstSegmentIndexInclusive]; + var firstSegment = _compositeText.Segments[firstSegmentIndexInclusive]; + var firstSegmentOffset = _compositeText._segmentOffsets[firstSegmentIndexInclusive]; + var firstSegmentTextLine = firstSegment.Lines[lineNumber - firstSegmentFirstLineNumber]; + + var lineLength = firstSegmentTextLine.SpanIncludingLineBreak.Length; + + // walk forward through segments between firstSegmentIndexInclusive and lastSegmentIndexInclusive, and add their + // view of the length of this line. This loop handles all segments between firstSegmentIndexInclusive and lastSegmentIndexInclusive. + for (var nextSegmentIndex = firstSegmentIndexInclusive + 1; nextSegmentIndex < lastSegmentIndexInclusive; nextSegmentIndex++) + { + var nextSegment = _compositeText.Segments[nextSegmentIndex]; + + // Segments between firstSegmentIndexInclusive and lastSegmentIndexInclusive should have either exactly one line or + // exactly two lines and the second line being empty. + Debug.Assert((nextSegment.Lines.Count == 1) || + (nextSegment.Lines.Count == 2 && nextSegment.Lines[1].SpanIncludingLineBreak.IsEmpty)); + + lineLength += nextSegment.Lines[0].SpanIncludingLineBreak.Length; + } + + if (firstSegmentIndexInclusive != lastSegmentIndexInclusive) + { + var lastSegment = _compositeText.Segments[lastSegmentIndexInclusive]; + + // lastSegment should have at least one line. + Debug.Assert(lastSegment.Lines.Count >= 1); + + lineLength += lastSegment.Lines[0].SpanIncludingLineBreak.Length; + } + + var resultLine = TextLine.FromSpanUnsafe(_compositeText, new TextSpan(firstSegmentOffset + firstSegmentTextLine.Start, lineLength)); + + // Assert resultLine only has line breaks in the appropriate locations + Debug.Assert(resultLine.ToString().All(static c => !TextUtilities.IsAnyLineBreakCharacter(c))); + + return resultLine; + } + } + + private void GetSegmentIndexRangeContainingLine(int lineNumber, out int firstSegmentIndexInclusive, out int lastSegmentIndexInclusive) + { + var idx = _segmentLineNumbers.BinarySearch(lineNumber); + var binarySearchSegmentIndex = idx >= 0 ? idx : (~idx - 1); + + // Walk backwards starting at binarySearchSegmentIndex to find the earliest segment index that intersects this line number + for (firstSegmentIndexInclusive = binarySearchSegmentIndex; firstSegmentIndexInclusive > 0; firstSegmentIndexInclusive--) + { + if (_segmentLineNumbers[firstSegmentIndexInclusive] != lineNumber) + { + // This segment doesn't start at the requested line, no need to continue to earlier segments. + break; + } + + // No need to include the previous segment if it ends in a newline character + var previousSegment = _compositeText.Segments[firstSegmentIndexInclusive - 1]; + var previousSegmentLastChar = previousSegment[^1]; + if (TextUtilities.IsAnyLineBreakCharacter(previousSegmentLastChar)) + { + break; + } + } + + for (lastSegmentIndexInclusive = binarySearchSegmentIndex; lastSegmentIndexInclusive < _compositeText.Segments.Length - 1; lastSegmentIndexInclusive++) + { + if (_segmentLineNumbers[lastSegmentIndexInclusive + 1] != lineNumber) + { + break; + } + } + } + } } } diff --git a/src/Compilers/Core/Portable/Text/SubText.cs b/src/Compilers/Core/Portable/Text/SubText.cs index 7769617224043..e0935e8cebc0d 100644 --- a/src/Compilers/Core/Portable/Text/SubText.cs +++ b/src/Compilers/Core/Portable/Text/SubText.cs @@ -3,6 +3,8 @@ // 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 @@ -63,6 +65,9 @@ public override char this[int position] } } + protected override TextLineCollection GetLinesCore() + => new SubTextLineInfo(this); + public override string ToString(TextSpan span) { CheckSubSpan(span); @@ -89,5 +94,130 @@ private TextSpan GetCompositeSpan(int start, int length) int compositeEnd = Math.Min(UnderlyingText.Length, compositeStart + length); return new TextSpan(compositeStart, compositeEnd - compositeStart); } + + /// + /// Delegates to the SubText's to determine line information. + /// + 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; + + /// + /// Determines the line number of a position in this SubText + /// + 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; + } + } } }