/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs
1 308 строк
53 KB
Todd Grunke
Fix SubText ctor parameter verification. (#78989)
17 июн 2025, 18:51
Не верифицирован
17 июн 2025, 18:51
97445ba
Код
Авторство
О чём код?
// 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. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.UnitTests { public class TextChangeTests { private readonly ITestOutputHelper _output; public TextChangeTests(ITestOutputHelper output) { _output = output; } [Fact] public void TestSubTextStart() { var text = SourceText.From("Hello World"); var subText = text.GetSubText(6); Assert.Equal("World", subText.ToString()); } [Fact] public void TestSubTextSpanFirst() { var text = SourceText.From("Hello World"); var subText = text.GetSubText(new TextSpan(0, 5)); Assert.Equal("Hello", subText.ToString()); } [Fact] public void TestSubTextSpanLast() { var text = SourceText.From("Hello World"); var subText = text.GetSubText(new TextSpan(6, 5)); Assert.Equal("World", subText.ToString()); } [Fact] public void TestSubTextSpanMid() { var text = SourceText.From("Hello World"); var subText = text.GetSubText(new TextSpan(4, 3)); Assert.Equal("o W", subText.ToString()); } [Fact] public void TestSubTextSubTextStart() { var text = SourceText.From("Hello World"); var subText = text.GetSubText(new TextSpan(0, 5)); var subSubText = subText.GetSubText(new TextSpan(0, 0)); Assert.Equal("Hello", subText.ToString()); Assert.Equal(0, subSubText.Length); } [Fact] [WorkItem(78989, "https://github.com/dotnet/roslyn/pull/78989")] public void TestSubTextSubTextEnd() { var text = SourceText.From("Hello World"); var subText = text.GetSubText(6); var subSubText = subText.GetSubText(subText.Length); Assert.Equal("World", subText.ToString()); Assert.Equal(0, subSubText.Length); } [Fact] public void TestChangedText() { var text = SourceText.From("Hello World"); var newText = text.Replace(6, 0, "Beautiful "); Assert.Equal("Hello Beautiful World", newText.ToString()); } [Fact] public void TestChangedTextChanges() { var text = SourceText.From("Hello World"); var newText = text.Replace(6, 0, "Beautiful "); var changes = newText.GetChangeRanges(text); Assert.NotNull(changes); Assert.Equal(1, changes.Count); Assert.Equal(6, changes[0].Span.Start); Assert.Equal(0, changes[0].Span.Length); Assert.Equal(10, changes[0].NewLength); } [Fact] public void TestChangedTextWithMultipleChanges() { var text = SourceText.From("Hello World"); var newText = text.WithChanges( new TextChange(new TextSpan(0, 5), "Halo"), new TextChange(new TextSpan(6, 5), "Universe")); Assert.Equal("Halo Universe", newText.ToString()); } [Fact] public void TestChangedTextWithMultipleOverlappingChanges() { var text = SourceText.From("Hello World"); var changes = new[] { new TextChange(new TextSpan(0, 5), "Halo"), new TextChange(new TextSpan(3, 5), "Universe") }; Assert.Throws<ArgumentException>(() => text.WithChanges(changes)); } [Fact] public void TestChangedTextWithMultipleUnorderedChanges() { var text = SourceText.From("Hello World"); var changes = new[] { new TextChange(new TextSpan(6, 5), "Universe"), new TextChange(new TextSpan(0, 5), "Halo") }; var newText = text.WithChanges(changes); Assert.Equal("Halo Universe", newText.ToString()); } [Fact] public void TestChangedTextWithMultipleUnorderedChangesAndOneIsOutOfBounds() { var text = SourceText.From("Hello World"); var changes = new[] { new TextChange(new TextSpan(6, 7), "Universe"), new TextChange(new TextSpan(0, 5), "Halo") }; Assert.ThrowsAny<ArgumentException>(() => { var newText = text.WithChanges(changes); }); } [Fact] public void TestChangedTextWithMultipleConsecutiveInsertsSamePosition() { var text = SourceText.From("Hello World"); var newText = text.WithChanges( new TextChange(new TextSpan(6, 0), "Super "), new TextChange(new TextSpan(6, 0), "Spectacular ")); Assert.Equal("Hello Super Spectacular World", newText.ToString()); } [Fact] public void TestChangedTextWithReplaceAfterInsertSamePosition() { var text = SourceText.From("Hello World"); var newText = text.WithChanges( new TextChange(new TextSpan(6, 0), "Super "), new TextChange(new TextSpan(6, 2), "Vu")); Assert.Equal("Hello Super Vurld", newText.ToString()); } [Fact] public void TestChangedTextWithReplaceBeforeInsertSamePosition() { var text = SourceText.From("Hello World"); var changes = new[] { new TextChange(new TextSpan(6, 2), "Vu"), new TextChange(new TextSpan(6, 0), "Super ") }; var newText = text.WithChanges(changes); Assert.Equal("Hello Super Vurld", newText.ToString()); } [Fact] public void TestChangedTextWithDeleteAfterDeleteAdjacent() { var text = SourceText.From("Hello World"); var newText = text.WithChanges( new TextChange(new TextSpan(4, 1), string.Empty), new TextChange(new TextSpan(5, 1), string.Empty)); Assert.Equal("HellWorld", newText.ToString()); } [Fact] public void TestSubTextAfterMultipleChanges() { var text = SourceText.From("Hello World", Encoding.Unicode, SourceHashAlgorithms.Default); var newText = text.WithChanges( new TextChange(new TextSpan(4, 1), string.Empty), new TextChange(new TextSpan(6, 5), "Universe")); var subText = newText.GetSubText(new TextSpan(3, 4)); Assert.Equal("l Un", subText.ToString()); Assert.Equal(SourceHashAlgorithms.Default, subText.ChecksumAlgorithm); Assert.Same(Encoding.Unicode, subText.Encoding); } [Fact] public void TestLinesInChangedText() { var text = SourceText.From("Hello World"); var newText = text.WithChanges( new TextChange(new TextSpan(4, 1), string.Empty)); Assert.Equal(1, newText.Lines.Count); } [Fact] public void TestCopyTo() { var text = SourceText.From("Hello World"); var newText = text.WithChanges( new TextChange(new TextSpan(6, 5), "Universe")); var destination = new char[32]; newText.CopyTo(0, destination, 0, 0); //should copy nothing and not throw. Assert.Throws<ArgumentOutOfRangeException>(() => newText.CopyTo(-1, destination, 0, 2)); Assert.Throws<ArgumentOutOfRangeException>(() => newText.CopyTo(0, destination, -1, 2)); Assert.Throws<ArgumentOutOfRangeException>(() => newText.CopyTo(0, destination, 0, -1)); Assert.Throws<ArgumentNullException>(() => newText.CopyTo(0, null, 0, 2)); Assert.Throws<ArgumentOutOfRangeException>(() => newText.CopyTo(newText.Length - 1, destination, 0, 2)); Assert.Throws<ArgumentOutOfRangeException>(() => newText.CopyTo(0, destination, destination.Length - 1, 2)); } [Fact] public void TestGetTextChangesToChangedText() { var text = SourceText.From(new string('.', 2048), Encoding.Unicode, SourceHashAlgorithms.Default); // start bigger than GetText() copy buffer var changes = new TextChange[] { new TextChange(new TextSpan(0, 1), "[1]"), new TextChange(new TextSpan(1, 1), "[2]"), new TextChange(new TextSpan(5, 0), "[3]"), new TextChange(new TextSpan(25, 2), "[4]") }; var newText = text.WithChanges(changes); Assert.Equal(SourceHashAlgorithms.Default, newText.ChecksumAlgorithm); Assert.Same(Encoding.Unicode, newText.Encoding); var result = newText.GetTextChanges(text).ToList(); Assert.Equal(changes.Length, result.Count); for (int i = 0; i < changes.Length; i++) { var expected = changes[i]; var actual = result[i]; Assert.Equal(expected.Span, actual.Span); Assert.Equal(expected.NewText, actual.NewText); } } private sealed class TextLineEqualityComparer : IEqualityComparer<TextLine> { public bool Equals(TextLine x, TextLine y) { return x.Span == y.Span; } public int GetHashCode(TextLine obj) { return obj.Span.GetHashCode(); } } private static void AssertChangedTextLinesHelper(string originalText, params TextChange[] changes) { var changedText = SourceText.From(originalText).WithChanges(changes); Assert.Equal(SourceText.From(changedText.ToString()).Lines, changedText.Lines, new TextLineEqualityComparer()); } [Fact] public void TestOptimizedSourceTextLinesSimpleSubstitution() { AssertChangedTextLinesHelper("Line1\r\nLine2\r\nLine3", new TextChange(new TextSpan(8, 2), "IN"), new TextChange(new TextSpan(15, 2), "IN")); } [Fact] public void TestOptimizedSourceTextLinesSubstitutionWithLongerText() { AssertChangedTextLinesHelper("Line1\r\nLine2\r\nLine3", new TextChange(new TextSpan(8, 2), new string('a', 10)), new TextChange(new TextSpan(15, 2), new string('a', 10))); } [Fact] public void TestOptimizedSourceTextLinesInsertCrLf() { AssertChangedTextLinesHelper("Line1\r\nLine2\r\nLine3", new TextChange(new TextSpan(8, 2), "\r\n"), new TextChange(new TextSpan(15, 2), "\r\n")); } [Fact] public void TestOptimizedSourceTextLinesSimpleCr() { AssertChangedTextLinesHelper("Line1\rLine2\rLine3", new TextChange(new TextSpan(6, 0), "aa\r"), new TextChange(new TextSpan(11, 0), "aa\r")); } [Fact] public void TestOptimizedSourceTextLinesSimpleLf() { AssertChangedTextLinesHelper("Line1\nLine2\nLine3", new TextChange(new TextSpan(6, 0), "aa\n"), new TextChange(new TextSpan(11, 0), "aa\n")); } [Fact] public void TestOptimizedSourceTextLinesRemoveCrLf() { AssertChangedTextLinesHelper("Line1\r\nLine2\r\nLine3", new TextChange(new TextSpan(4, 4), "aaaaaa"), new TextChange(new TextSpan(15, 4), "aaaaaa")); } [Fact] public void TestOptimizedSourceTextLinesBreakCrLf() { AssertChangedTextLinesHelper("Test\r\nMessage", new TextChange(new TextSpan(5, 0), "aaaaaa")); } [Fact] public void TestOptimizedSourceTextLinesBreakCrLfWithLfPrefixedAndCrSuffixed() { AssertChangedTextLinesHelper("Test\r\nMessage", new TextChange(new TextSpan(5, 0), "\naaaaaa\r")); } [Fact] public void TestOptimizedSourceTextLineInsertAtEnd() { AssertChangedTextLinesHelper("Line1\r\nLine2\r\nLine3\r\n", new TextChange(new TextSpan(21, 0), "Line4\r\n"), new TextChange(new TextSpan(21, 0), "Line5\r\n")); } [Fact] public void TestManySingleCharacterAdds() { var str = new String('.', 1024); var text = SourceText.From(str); var lines = text.Lines; int n = 20000; var expected = str; for (int i = 0; i < n; i++) { char c = (char)(((ushort)'a') + (i % 26)); text = text.Replace(50 + i, 0, c.ToString()); expected = expected.Substring(0, 50 + i) + c + expected.Substring(50 + i); } Assert.Equal(str.Length + n, text.Length); Assert.Equal(expected, text.ToString()); } [Fact] public void TestManySingleCharacterReplacements() { var str = new String('.', 1024); var text = SourceText.From(str); var lines = text.Lines; var expected = str; for (int i = 0; i < str.Length; i++) { char c = (char)(((ushort)'a') + (i % 26)); text = text.Replace(i, 1, c.ToString()); expected = expected.Substring(0, i) + c + str.Substring(i + 1); } Assert.Equal(str.Length, text.Length); Assert.Equal(expected, text.ToString()); } [Fact] public void TestSubTextCausesSizeLengthDifference() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); Assert.Equal(26, text.Length); Assert.Equal(26, text.StorageSize); var subtext = text.GetSubText(new TextSpan(5, 10)); Assert.Equal(10, subtext.Length); Assert.Equal("fghijklmno", subtext.ToString()); Assert.Equal(26, subtext.StorageSize); } [Fact] public void TestRemovingMajorityOfTextCompressesStorage() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); var newText = text.Replace(new TextSpan(0, 20), ""); Assert.Equal(6, newText.Length); Assert.Equal(6, newText.StorageSize); } [Fact] public void TestRemovingMinorityOfTextDoesNotCompressesStorage() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); var newText = text.Replace(new TextSpan(10, 6), ""); Assert.Equal(20, newText.Length); Assert.Equal(26, newText.StorageSize); } [Fact] public void TestRemovingTextCreatesSegments() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); Assert.Equal(0, text.Segments.Length); var newText = text.Replace(new TextSpan(10, 1), ""); Assert.Equal(25, newText.Length); Assert.Equal(26, newText.StorageSize); Assert.Equal(2, newText.Segments.Length); Assert.Equal("abcdefghij", newText.Segments[0].ToString()); Assert.Equal("lmnopqrstuvwxyz", newText.Segments[1].ToString()); } [Fact] public void TestAddingTextCreatesSegments() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); Assert.Equal(0, text.Segments.Length); var textWithSegments = text.Replace(new TextSpan(10, 0), "*"); Assert.Equal(27, textWithSegments.Length); Assert.Equal("abcdefghij*klmnopqrstuvwxyz", textWithSegments.ToString()); Assert.Equal(3, textWithSegments.Segments.Length); Assert.Equal("abcdefghij", textWithSegments.Segments[0].ToString()); Assert.Equal("*", textWithSegments.Segments[1].ToString()); Assert.Equal("klmnopqrstuvwxyz", textWithSegments.Segments[2].ToString()); } [Fact] public void TestRemovingAcrossExistingSegmentsRemovesSegments() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); Assert.Equal(0, text.Segments.Length); var textWithSegments = text.Replace(new TextSpan(10, 0), "*"); Assert.Equal(27, textWithSegments.Length); Assert.Equal(27, textWithSegments.StorageSize); var textWithFewerSegments = textWithSegments.Replace(new TextSpan(9, 3), ""); Assert.Equal("abcdefghilmnopqrstuvwxyz", textWithFewerSegments.ToString()); Assert.Equal(24, textWithFewerSegments.Length); Assert.Equal(26, textWithFewerSegments.StorageSize); Assert.Equal(2, textWithFewerSegments.Segments.Length); Assert.Equal("abcdefghi", textWithFewerSegments.Segments[0].ToString()); Assert.Equal("lmnopqrstuvwxyz", textWithFewerSegments.Segments[1].ToString()); } [Fact] public void TestRemovingEverythingSucceeds() { var text = SourceText.From("abcdefghijklmnopqrstuvwxyz"); Assert.Equal(0, text.Segments.Length); var textWithSegments = text.Replace(new TextSpan(0, text.Length), ""); Assert.Equal(0, textWithSegments.Length); Assert.Equal(0, textWithSegments.StorageSize); } [Fact] public void TestCompressingSegmentsCompressesSmallerSegmentsFirst() { var a = new string('a', 64); var b = new string('b', 64); var t = SourceText.From(a); t = t.Replace(t.Length, 0, b); // add b's var segs = t.Segments.Length; Assert.Equal(2, segs); Assert.Equal(a, t.Segments[0].ToString()); Assert.Equal(b, t.Segments[1].ToString()); // keep appending little segments until we trigger compression do { segs = t.Segments.Length; t = t.Replace(t.Length, 0, "c"); } while (t.Segments.Length > segs); // this should compact all the 'c' segments into one Assert.Equal(3, t.Segments.Length); Assert.Equal(a, t.Segments[0].ToString()); Assert.Equal(b, t.Segments[1].ToString()); Assert.Equal(new string('c', t.Segments[2].Length), t.Segments[2].ToString()); } [Fact] public void TestCompressingSegmentsCompressesLargerSegmentsIfNecessary() { var a = new string('a', 64); var b = new string('b', 64); var c = new string('c', 64); var t = SourceText.From(a); t = t.Replace(t.Length, 0, b); // add b's var segs = t.Segments.Length; Assert.Equal(2, segs); Assert.Equal(a, t.Segments[0].ToString()); Assert.Equal(b, t.Segments[1].ToString()); // keep appending larger segments (larger than initial size) do { segs = t.Segments.Length; t = t.Replace(t.Length, 0, c); // add c's that are the same segment size as the a's and b's } while (t.Segments.Length > segs); // this should compact all the segments since they all were the same size and // compress at the same time Assert.Equal(0, t.Segments.Length); } [Fact] public void TestOldEditsCanBeCollected() { // this test proves that intermediate edits are not being kept alive by successive edits. WeakReference weakFirstEdit; SourceText secondEdit; CreateEdits(out weakFirstEdit, out secondEdit); int tries = 0; while (weakFirstEdit.IsAlive) { tries++; if (tries > 10) { throw new InvalidOperationException("Failed to GC old edit"); } GC.Collect(2, GCCollectionMode.Forced, blocking: true); } } private void CreateEdits(out WeakReference weakFirstEdit, out SourceText secondEdit) { var text = SourceText.From("This is the old text"); var firstEdit = text.Replace(11, 3, "new"); secondEdit = firstEdit.Replace(11, 3, "newer"); weakFirstEdit = new WeakReference(firstEdit); } [Fact] public void TestLargeTextWriterReusesLargeChunks() { var chunk1 = "this is the large text".ToArray(); var largeText = CreateLargeText(chunk1); // chunks are considered large because they are bigger than the expected size var writer = new LargeTextWriter(largeText.Encoding, largeText.ChecksumAlgorithm, 10); largeText.Write(writer); var newText = (LargeText)writer.ToSourceText(); Assert.NotSame(largeText, newText); Assert.Equal(1, GetChunks(newText).Length); Assert.Same(chunk1, GetChunks(newText)[0]); } private SourceText CreateLargeText(params char[][] chunks) { return new LargeText(ImmutableArray.Create(chunks), Encoding.UTF8, default(ImmutableArray<byte>), SourceHashAlgorithms.Default, default(ImmutableArray<byte>)); } private ImmutableArray<char[]> GetChunks(SourceText text) { var largeText = text as LargeText; if (largeText != null) { var chunkField = text.GetType().GetField("_chunks", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); return (ImmutableArray<char[]>)chunkField.GetValue(text); } else { return ImmutableArray<char[]>.Empty; } } [Fact] public void TestLargeTextWriterDoesNotReuseSmallChunks() { var text = SourceText.From("small preamble"); var chunk1 = "this is the large text".ToArray(); var largeText = CreateLargeText(chunk1); // chunks are considered small because they fit within the buffer (which is the expected length for this test) var writer = new LargeTextWriter(largeText.Encoding, largeText.ChecksumAlgorithm, chunk1.Length * 4); // write preamble so buffer is allocated and has contents. text.Write(writer); // large text fits within the remaining buffer largeText.Write(writer); var newText = (LargeText)writer.ToSourceText(); Assert.NotSame(largeText, newText); Assert.Equal(text.Length + largeText.Length, newText.Length); Assert.Equal(1, GetChunks(newText).Length); Assert.NotSame(chunk1, GetChunks(newText)[0]); } [Fact] [WorkItem(10452, "https://github.com/dotnet/roslyn/issues/10452")] public void TestEmptyChangeAfterChange() { var original = SourceText.From("Hello World"); var change1 = original.WithChanges(new TextChange(new TextSpan(5, 6), string.Empty)); // prepare a ChangedText instance var change2 = change1.WithChanges(); // this should not cause exception Assert.Same(change1, change2); // this was a no-op and returned the same instance } [Fact] [WorkItem(10452, "https://github.com/dotnet/roslyn/issues/10452")] public void TestEmptyChangeAfterChange2() { var original = SourceText.From("Hello World"); var change1 = original.WithChanges(new TextChange(new TextSpan(5, 6), string.Empty)); // prepare a ChangedText instance var change2 = change1.WithChanges(new TextChange(new TextSpan(2, 0), string.Empty)); // this should not cause exception Assert.Same(change1, change2); // this was a no-op and returned the same instance } [Fact] public void TestMergeChanges_Overlapping_NewInsideOld() { var original = SourceText.From("Hello World"); var change1 = original.WithChanges(new TextChange(new TextSpan(6, 0), "Cruel ")); var change2 = change1.WithChanges(new TextChange(new TextSpan(7, 3), "oo")); Assert.Equal("Hello Cool World", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(6, 0), changes[0].Span); Assert.Equal("Cool ", changes[0].NewText); } [Fact] [WorkItem(22289, "https://github.com/dotnet/roslyn/issues/22289")] public void TestMergeChanges_Overlapping_NewInsideOld_AndOldHasDeletion() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(2, 0), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa4", change1.ToString()); Assert.Equal("0abba4", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "abba") }, changes); } [Fact] [WorkItem(22289, "https://github.com/dotnet/roslyn/issues/22289")] public void TestMergeChanges_Overlapping_NewInsideOld_AndOldHasLeadingDeletion_SmallerThanLeadingInsertion() { var original = SourceText.From("012"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 1), "aaa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(3, 0), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aaa2", change1.ToString()); Assert.Equal("0aabba2", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 1), "aabba") }, changes); } [Fact] [WorkItem(22289, "https://github.com/dotnet/roslyn/issues/22289")] public void TestMergeChanges_Overlapping_NewInsideOld_AndBothHaveDeletion_NewDeletionSmallerThanOld() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(2, 1), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa4", change1.ToString()); Assert.Equal("0abb4", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "abb") }, changes); } [Fact] public void TestMergeChanges_Overlapping_OldInsideNew() { var original = SourceText.From("Hello World"); var change1 = original.WithChanges(new TextChange(new TextSpan(6, 0), "Cruel ")); var change2 = change1.WithChanges(new TextChange(new TextSpan(2, 14), "ar")); Assert.Equal("Heard", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(2, 8), changes[0].Span); Assert.Equal("ar", changes[0].NewText); } [Fact] public void TestMergeChanges_Overlapping_NewBeforeOld() { var original = SourceText.From("Hello World"); var change1 = original.WithChanges(new TextChange(new TextSpan(6, 0), "Cruel ")); var change2 = change1.WithChanges(new TextChange(new TextSpan(4, 6), " Bel")); Assert.Equal("Hell Bell World", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(4, 2), changes[0].Span); Assert.Equal(" Bell ", changes[0].NewText); } [Fact] public void TestMergeChanges_Overlapping_OldBeforeNew() { var original = SourceText.From("Hello World"); var change1 = original.WithChanges(new TextChange(new TextSpan(6, 0), "Cruel ")); var change2 = change1.WithChanges(new TextChange(new TextSpan(7, 6), "wazy V")); Assert.Equal("Hello Cwazy Vorld", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(6, 1), changes[0].Span); Assert.Equal("Cwazy V", changes[0].NewText); } [Fact] public void TestMergeChanges_SameStart() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 0), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 0), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa1234", change1.ToString()); Assert.Equal("0bbaa1234", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 0), "bbaa") }, changes); } [Fact] public void TestMergeChanges_SameStart_AndOldHasDeletion() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 0), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa4", change1.ToString()); Assert.Equal("0bbaa4", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "bbaa") }, changes); } [Fact] public void TestMergeChanges_SameStart_AndNewHasDeletion_SmallerThanOldInsertion() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 0), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 1), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa1234", change1.ToString()); Assert.Equal("0bba1234", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 0), "bba") }, changes); } [Fact] public void TestMergeChanges_SameStart_AndNewHasDeletion_EqualToOldInsertion() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 0), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 2), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa1234", change1.ToString()); Assert.Equal("0bb1234", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 0), "bb") }, changes); } [Fact] public void TestMergeChanges_SameStart_AndNewHasDeletion_LargerThanOldInsertion() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 0), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 3), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa1234", change1.ToString()); Assert.Equal("0bb234", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 1), "bb") }, changes); } [Fact] [WorkItem(22289, "https://github.com/dotnet/roslyn/issues/22289")] public void TestMergeChanges_SameStart_AndBothHaveDeletion_NewDeletionSmallerThanOld() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 1), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa4", change1.ToString()); Assert.Equal("0bba4", change2.ToString()); Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "bba") }, changes); } [Fact] [WorkItem(39405, "https://github.com/dotnet/roslyn/issues/39405")] public void TestMergeChanges_NewDeletionLargerThanOld() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 3), "bb")); var changes = change2.GetTextChanges(original); Assert.Equal("0aa4", change1.ToString()); Assert.Equal("0bb", change2.ToString()); } [Fact] public void TestMergeChanges_AfterAdjacent() { var original = SourceText.From("Hell"); var change1 = original.WithChanges(new TextChange(new TextSpan(4, 0), "o ")); var change2 = change1.WithChanges(new TextChange(new TextSpan(6, 0), "World")); Assert.Equal("Hello World", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(4, 0), changes[0].Span); Assert.Equal("o World", changes[0].NewText); } [Fact] public void TestMergeChanges_AfterSeparated() { var original = SourceText.From("Hell "); var change1 = original.WithChanges(new TextChange(new TextSpan(4, 0), "o")); var change2 = change1.WithChanges(new TextChange(new TextSpan(6, 0), "World")); Assert.Equal("Hello World", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(2, changes.Count); Assert.Equal(new TextSpan(4, 0), changes[0].Span); Assert.Equal("o", changes[0].NewText); Assert.Equal(new TextSpan(5, 0), changes[1].Span); Assert.Equal("World", changes[1].NewText); } [Fact] public void TestMergeChanges_BeforeSeparated() { var original = SourceText.From("Hell Word"); var change1 = original.WithChanges(new TextChange(new TextSpan(8, 0), "l")); var change2 = change1.WithChanges(new TextChange(new TextSpan(4, 0), "o")); Assert.Equal("Hello World", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(2, changes.Count); Assert.Equal(new TextSpan(4, 0), changes[0].Span); Assert.Equal("o", changes[0].NewText); Assert.Equal(new TextSpan(8, 0), changes[1].Span); Assert.Equal("l", changes[1].NewText); } [Fact] public void TestMergeChanges_BeforeAdjacent() { var original = SourceText.From("Hell"); var change1 = original.WithChanges(new TextChange(new TextSpan(4, 0), " World")); Assert.Equal("Hell World", change1.ToString()); var change2 = change1.WithChanges(new TextChange(new TextSpan(4, 0), "o")); Assert.Equal("Hello World", change2.ToString()); var changes = change2.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(4, 0), changes[0].Span); Assert.Equal("o World", changes[0].NewText); } [ConditionalFact(typeof(ClrOnly), Reason = "https://github.com/mono/mono/issues/10961")] public void TestMergeChanges_NoMiddleMan() { var original = SourceText.From("Hell"); var final = GetChangesWithoutMiddle( original, c => c.WithChanges(new TextChange(new TextSpan(4, 0), "o ")), c => c.WithChanges(new TextChange(new TextSpan(6, 0), "World"))); Assert.Equal("Hello World", final.ToString()); var changes = final.GetTextChanges(original); Assert.Equal(1, changes.Count); Assert.Equal(new TextSpan(4, 0), changes[0].Span); Assert.Equal("o World", changes[0].NewText); } [Fact] public void TestMergeChanges_IntegrationTestCase1() { var oldChanges = ImmutableArray.Create( new TextChangeRange(new TextSpan(919, 10), 466), new TextChangeRange(new TextSpan(936, 33), 29), new TextChangeRange(new TextSpan(1098, 0), 70), new TextChangeRange(new TextSpan(1125, 4), 34), new TextChangeRange(new TextSpan(1138, 0), 47)); var newChanges = ImmutableArray.Create( new TextChangeRange(new TextSpan(997, 0), 2), new TextChangeRange(new TextSpan(1414, 0), 2), new TextChangeRange(new TextSpan(1419, 0), 2), new TextChangeRange(new TextSpan(1671, 5), 5), new TextChangeRange(new TextSpan(1681, 0), 4)); var merged = ChangedText.TestAccessor.Merge(oldChanges, newChanges); var expected = ImmutableArray.Create( new TextChangeRange(new TextSpan(919, 10), 468), new TextChangeRange(new TextSpan(936, 33), 33), new TextChangeRange(new TextSpan(1098, 0), 70), new TextChangeRange(new TextSpan(1125, 4), 38), new TextChangeRange(new TextSpan(1138, 0), 47)); Assert.Equal<TextChangeRange>(expected, merged); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void DebuggerDisplay() { Assert.Equal("new TextChange(new TextSpan(0, 0), null)", default(TextChange).GetDebuggerDisplay()); Assert.Equal("new TextChange(new TextSpan(0, 1), \"abc\")", new TextChange(new TextSpan(0, 1), "abc").GetDebuggerDisplay()); Assert.Equal("new TextChange(new TextSpan(0, 1), (NewLength = 10))", new TextChange(new TextSpan(0, 1), "0123456789").GetDebuggerDisplay()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz() { var random = new Random(); // Adjust upper bound as needed to generate a simpler reproducer for an error scenario var originalText = SourceText.From(string.Join("", Enumerable.Range(0, random.Next(10)))); for (var iteration = 0; iteration < 100000; iteration++) { var editedLength = originalText.Length; ArrayBuilder<TextChange> oldChangesBuilder = ArrayBuilder<TextChange>.GetInstance(); // Adjust as needed to get a simpler error reproducer. var oldMaxInsertLength = originalText.Length * 2; const int maxSkipLength = 2; // generate sequence of "old edits" which meet invariants for (int i = 0; i < originalText.Length; i += random.Next(maxSkipLength)) { var newText = string.Join("", Enumerable.Repeat('a', random.Next(oldMaxInsertLength))); var newChange = new TextChange(new TextSpan(i, length: random.Next(originalText.Length - i)), newText); i = newChange.Span.End; editedLength = editedLength - newChange.Span.Length + newChange.NewText.Length; oldChangesBuilder.Add(newChange); // Adjust as needed to generate a simpler reproducer for an error scenario if (oldChangesBuilder.Count == 5) break; } var change1 = originalText.WithChanges(oldChangesBuilder); ArrayBuilder<TextChange> newChangesBuilder = ArrayBuilder<TextChange>.GetInstance(); // Adjust as needed to get a simpler error reproducer. var newMaxInsertLength = editedLength * 2; // generate sequence of "new edits" which meet invariants for (int i = 0; i < editedLength; i += random.Next(maxSkipLength)) { var newText = string.Join("", Enumerable.Repeat('b', random.Next(newMaxInsertLength))); var newChange = new TextChange(new TextSpan(i, length: random.Next(editedLength - i)), newText); i = newChange.Span.End; newChangesBuilder.Add(newChange); // Adjust as needed to generate a simpler reproducer for an error scenario if (newChangesBuilder.Count == 5) break; } var change2 = change1.WithChanges(newChangesBuilder); try { var textChanges = change2.GetTextChanges(originalText); Assert.Equal(originalText.WithChanges(textChanges).ToString(), change2.ToString()); } catch { _output.WriteLine($@" [Fact] public void Fuzz_{iteration}() {{ var originalText = SourceText.From(""{originalText}""); var change1 = originalText.WithChanges({string.Join(", ", oldChangesBuilder.Select(c => c.GetDebuggerDisplay()))}); var change2 = change1.WithChanges({string.Join(", ", newChangesBuilder.Select(c => c.GetDebuggerDisplay()))}); Assert.Equal(""{change1}"", change1.ToString()); // double-check for correctness Assert.Equal(""{change2}"", change2.ToString()); // double-check for correctness var changes = change2.GetTextChanges(originalText); Assert.Equal(""{change2}"", originalText.WithChanges(changes).ToString()); }} "); throw; } finally { // we delay freeing so that if we need to debug the fuzzer // it's easier to see what changes were introduced at each stage. oldChangesBuilder.Free(); newChangesBuilder.Free(); } } } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_0() { var originalText = SourceText.From("01234"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 2), "a")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 2), "bb")); Assert.Equal("a234", change1.ToString()); Assert.Equal("bb34", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("bb34", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_1() { var original = SourceText.From("01234"); var change1 = original.WithChanges(new TextChange(new TextSpan(0, 0), "aa"), new TextChange(new TextSpan(1, 1), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 1), "b"), new TextChange(new TextSpan(2, 2), "")); var changes = change2.GetTextChanges(original); Assert.Equal("aa0aa234", change1.ToString()); Assert.Equal("baa234", change2.ToString()); Assert.Equal(change2.ToString(), original.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_2() { var originalText = SourceText.From("01234"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 0), "a")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 2), ""), new TextChange(new TextSpan(2, 0), "bb")); Assert.Equal("a01234", change1.ToString()); Assert.Equal("bb1234", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("bb1234", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_3() { var originalText = SourceText.From("01234"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 1), "aa"), new TextChange(new TextSpan(3, 1), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 0), "bbb")); Assert.Equal("aa12aa4", change1.ToString()); Assert.Equal("bbbaa12aa4", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("bbbaa12aa4", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_4() { var originalText = SourceText.From("012345"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 3), "a"), new TextChange(new TextSpan(5, 0), "aaa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 2), ""), new TextChange(new TextSpan(3, 1), "bb")); Assert.Equal("a34aaa5", change1.ToString()); Assert.Equal("4bbaa5", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("4bbaa5", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_7() { var originalText = SourceText.From("01234567"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 1), "aaaaa"), new TextChange(new TextSpan(3, 1), "aaaa"), new TextChange(new TextSpan(6, 1), "aaaaa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 0), "b"), new TextChange(new TextSpan(2, 0), "b"), new TextChange(new TextSpan(3, 4), "bbbbb"), new TextChange(new TextSpan(9, 5), "bbbbb"), new TextChange(new TextSpan(15, 3), "")); Assert.Equal("aaaaa12aaaa45aaaaa7", change1.ToString()); Assert.Equal("baababbbbbaabbbbba7", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("baababbbbbaabbbbba7", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_10() { var originalText = SourceText.From("01234"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 1), "a")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 1), "b"), new TextChange(new TextSpan(2, 2), "b")); Assert.Equal("a1234", change1.ToString()); Assert.Equal("b1b4", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("b1b4", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_23() { var originalText = SourceText.From("01234"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 1), "aa")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 0), "b"), new TextChange(new TextSpan(1, 2), "b")); Assert.Equal("aa1234", change1.ToString()); Assert.Equal("bab234", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("bab234", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_32() { var originalText = SourceText.From("012345"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 2), "a"), new TextChange(new TextSpan(3, 2), "a")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 3), "bbb")); Assert.Equal("a2a5", change1.ToString()); Assert.Equal("bbb5", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("bbb5", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_39() { var originalText = SourceText.From("0123456"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 4), ""), new TextChange(new TextSpan(5, 1), "")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 1), ""), new TextChange(new TextSpan(1, 0), "")); Assert.Equal("46", change1.ToString()); Assert.Equal("6", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("6", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_55() { var originalText = SourceText.From("012345"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 2), ""), new TextChange(new TextSpan(3, 1), ""), new TextChange(new TextSpan(4, 0), ""), new TextChange(new TextSpan(4, 0), ""), new TextChange(new TextSpan(4, 0), "")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 1), ""), new TextChange(new TextSpan(1, 1), ""), new TextChange(new TextSpan(2, 0), "")); Assert.Equal("245", change1.ToString()); Assert.Equal("5", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("5", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(47234, "https://github.com/dotnet/roslyn/issues/47234")] public void Fuzz_110() { var originalText = SourceText.From("01234"); var change1 = originalText.WithChanges(new TextChange(new TextSpan(0, 1), ""), new TextChange(new TextSpan(2, 1), "")); var change2 = change1.WithChanges(new TextChange(new TextSpan(0, 0), ""), new TextChange(new TextSpan(1, 1), "")); Assert.Equal("134", change1.ToString()); Assert.Equal("14", change2.ToString()); var changes = change2.GetTextChanges(originalText); Assert.Equal("14", originalText.WithChanges(changes).ToString()); } [Fact] [WorkItem(41413, "https://github.com/dotnet/roslyn/issues/41413")] public void GetTextChanges_NonOverlappingSpans() { var content = @"@functions{ public class Foo { void Method() { } } }"; var text = SourceText.From(content); var edits1 = new TextChange[] { new TextChange(new TextSpan(39, 0), " "), new TextChange(new TextSpan(42, 0), " "), new TextChange(new TextSpan(57, 0), " "), new TextChange(new TextSpan(58, 0), "\r\n"), new TextChange(new TextSpan(64, 2), " "), new TextChange(new TextSpan(69, 0), " "), }; var changedText = text.WithChanges(edits1); var edits2 = new TextChange[] { new TextChange(new TextSpan(35, 4), string.Empty), new TextChange(new TextSpan(46, 4), string.Empty), new TextChange(new TextSpan(73, 4), string.Empty), new TextChange(new TextSpan(88, 0), " "), new TextChange(new TextSpan(90, 4), string.Empty), new TextChange(new TextSpan(105, 4), string.Empty), }; var changedText2 = changedText.WithChanges(edits2); var changes = changedText2.GetTextChanges(text); var position = 0; foreach (var change in changes) { Assert.True(position <= change.Span.Start); position = change.Span.End; } } private SourceText GetChangesWithoutMiddle( SourceText original, Func<SourceText, SourceText> fnChange1, Func<SourceText, SourceText> fnChange2) { WeakReference change1; SourceText change2; GetChangesWithoutMiddle_Helper(original, fnChange1, fnChange2, out change1, out change2); while (change1.IsAlive) { GC.Collect(2); GC.WaitForFullGCComplete(); } return change2; } private void GetChangesWithoutMiddle_Helper( SourceText original, Func<SourceText, SourceText> fnChange1, Func<SourceText, SourceText> fnChange2, out WeakReference change1, out SourceText change2) { var c1 = fnChange1(original); change1 = new WeakReference(c1); change2 = fnChange2(c1); } } }