diff --git a/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs b/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs index 63ec173c9b74f..840f48bf76577 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs @@ -5,18 +5,27 @@ #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 System.Collections.Generic; +using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.UnitTests { public class TextChangeTests { + private readonly ITestOutputHelper _output; + + public TextChangeTests(ITestOutputHelper output) + { + _output = output; + } + [Fact] public void TestSubTextStart() { @@ -803,6 +812,19 @@ public void TestMergeChanges_SameStart_AndBothHaveDeletion_NewDeletionSmallerTha 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() { @@ -904,12 +926,318 @@ public void TestMergeChanges_IntegrationTestCase1() 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), 34), - new TextChangeRange(new TextSpan(1134, 0), 4), + new TextChangeRange(new TextSpan(1125, 4), 38), new TextChangeRange(new TextSpan(1138, 0), 47)); Assert.Equal(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 oldChangesBuilder = ArrayBuilder.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 newChangesBuilder = ArrayBuilder.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 fnChange1, diff --git a/src/Compilers/Core/Portable/Text/ChangedText.cs b/src/Compilers/Core/Portable/Text/ChangedText.cs index e6e4a570542b8..0c17a34792f5e 100644 --- a/src/Compilers/Core/Portable/Text/ChangedText.cs +++ b/src/Compilers/Core/Portable/Text/ChangedText.cs @@ -6,9 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -238,177 +236,331 @@ private static ImmutableArray Merge(IReadOnlyList + /// Represents a new change being processed by . + /// Such a new change must be adjusted before being added to the result list. + /// + /// + /// A value of this type may represent the intermediate state of merging of an old change into an unadjusted new change, + /// resulting in a temporary unadjusted new change whose is negative (not valid) until it is adjusted. + /// This tends to happen when we need to merge an old change deletion into a new change near the beginning of the text. (see TextChangeTests.Fuzz_4) + /// + private readonly struct UnadjustedNewChange + { + public readonly int SpanStart { get; } + public readonly int SpanLength { get; } + public readonly int NewLength { get; } + + public int SpanEnd => SpanStart + SpanLength; + + public UnadjustedNewChange(int spanStart, int spanLength, int newLength) + { + SpanStart = spanStart; + SpanLength = spanLength; + NewLength = newLength; + } + + public UnadjustedNewChange(TextChangeRange range) + : this(range.Span.Start, range.Span.Length, range.NewLength) + { + } + } + + /// + /// Merges the new change ranges into the old change ranges, adjusting the new ranges to be with respect to the original text + /// (with neither old or new changes applied) instead of with respect to the original text after "old changes" are applied. + /// + /// This may require splitting, concatenation, etc. of individual change ranges. + /// + /// + /// Both `oldChanges` and `newChanges` must contain non-overlapping spans in ascending order. + /// private static ImmutableArray Merge(ImmutableArray oldChanges, ImmutableArray newChanges) { - var list = new List(oldChanges.Length + newChanges.Length); + // Earlier steps are expected to prevent us from ever reaching this point with empty change sets. + if (oldChanges.IsEmpty) + { + throw new ArgumentException(nameof(oldChanges)); + } - int oldIndex = 0; - int newIndex = 0; - int oldDelta = 0; + if (newChanges.IsEmpty) + { + throw new ArgumentException(nameof(newChanges)); + } - var needNextNewChange = true; - var needNextOldChange = true; - TextChangeRange newChange = default; - TextChangeRange oldChange = default; + var builder = ArrayBuilder.GetInstance(); -nextNewChange: - if (newIndex < newChanges.Length) + var oldChange = oldChanges[0]; + var newChange = new UnadjustedNewChange(newChanges[0]); + + var oldIndex = 0; + var newIndex = 0; + + // The sum of characters inserted by old changes minus characters deleted by old changes. + // This value must be adjusted whenever characters from an old change are added to `builder`. + var oldDelta = 0; + + // In this loop we "zip" together potentially overlapping old and new changes. + // It's important that when overlapping changes are found, we don't consume past the end of the overlapping section until the next iteration. + // so that we don't miss scenarios where the section after the overlap we found itself overlaps with another change + // e.g.: + // [-------oldChange1------] + // [--newChange1--] [--newChange2--] + while (true) { - if (needNextNewChange) + if (oldChange.Span.Length == 0 && oldChange.NewLength == 0) { - newChange = newChanges[newIndex]; - needNextNewChange = false; + // old change does not insert or delete any characters, so it can be dropped to no effect. + if (tryGetNextOldChange()) continue; + else break; } - -nextOldChange: - if (oldIndex < oldChanges.Length) + else if (newChange.SpanLength == 0 && newChange.NewLength == 0) { - if (needNextOldChange) - { - oldChange = oldChanges[oldIndex]; - needNextOldChange = false; - } + // new change does not insert or delete any characters, so it can be dropped to no effect. + if (tryGetNextNewChange()) continue; + else break; + } + else if (newChange.SpanEnd <= oldChange.Span.Start + oldDelta) + { + // new change is entirely before old change, so just take the new change + // old[--------] + // new[--------] + adjustAndAddNewChange(builder, oldDelta, newChange); + if (tryGetNextNewChange()) continue; + else break; + } + else if (newChange.SpanStart >= oldChange.NewEnd + oldDelta) + { + // new change is entirely after old change, so just take the old change + // old[--------] + // new[--------] + addAndAdjustOldDelta(builder, ref oldDelta, oldChange); + if (tryGetNextOldChange()) continue; + else break; + } + else if (newChange.SpanStart < oldChange.Span.Start + oldDelta) + { + // new change starts before old change, but the new change deletion overlaps with the old change insertion + // note: 'd' represents a deleted character, 'a' represents a character inserted by an old change, and 'b' represents a character inserted by a new change. + // + // old|dddddd| + // |aaaaaa| + // --------------- + // new|dddddd| + // |bbbbbb| + + // align the new change and old change start by consuming the part of the new deletion before the old change + // (this only deletes characters of the original text) + // + // old|dddddd| + // |aaaaaa| + // --------------- + // new|ddd| + // |bbbbbb| + var newChangeLeadingDeletion = oldChange.Span.Start + oldDelta - newChange.SpanStart; + adjustAndAddNewChange(builder, oldDelta, new UnadjustedNewChange(newChange.SpanStart, newChangeLeadingDeletion, newLength: 0)); + newChange = new UnadjustedNewChange(oldChange.Span.Start + oldDelta, newChange.SpanLength - newChangeLeadingDeletion, newChange.NewLength); + continue; + } + else if (newChange.SpanStart > oldChange.Span.Start + oldDelta) + { + // new change starts after old change, but overlaps + // + // old|dddddd| + // |aaaaaa| + // --------------- + // new|dddddd| + // |bbbbbb| + + // align the old change to the new change by consuming the part of the old change which is before the new change. + // + // old|ddd| + // |aaa| + // --------------- + // new|dddddd| + // |bbbbbb| + + var oldChangeLeadingInsertion = newChange.SpanStart - (oldChange.Span.Start + oldDelta); + // we must make sure to delete at most as many characters as the entire oldChange deletes + var oldChangeLeadingDeletion = Math.Min(oldChange.Span.Length, oldChangeLeadingInsertion); + addAndAdjustOldDelta(builder, ref oldDelta, new TextChangeRange(new TextSpan(oldChange.Span.Start, oldChangeLeadingDeletion), oldChangeLeadingInsertion)); + oldChange = new TextChangeRange(new TextSpan(newChange.SpanStart - oldDelta, oldChange.Span.Length - oldChangeLeadingDeletion), oldChange.NewLength - oldChangeLeadingInsertion); + continue; + } + else + { + // old and new change start at same adjusted position + Debug.Assert(newChange.SpanStart == oldChange.Span.Start + oldDelta); -tryAgain: - if (oldChange.Span.Length == 0 && oldChange.NewLength == 0) - { - // old change is a non-change, just ignore it and move on - oldIndex++; - needNextOldChange = true; - goto nextOldChange; - } - else if (newChange.Span.Length == 0 && newChange.NewLength == 0) - { - // new change is a non-change, just ignore it and move on - newIndex++; - needNextNewChange = true; - goto nextNewChange; - } - else if (newChange.Span.End < (oldChange.Span.Start + oldDelta)) + if (newChange.SpanLength <= oldChange.NewLength) { - // new change occurs entirely before old change - var adjustedNewChange = new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, newChange.Span.Length), newChange.NewLength); - AddRange(list, adjustedNewChange); - newIndex++; - needNextNewChange = true; - goto nextNewChange; + // new change deletes fewer characters than old change inserted + // + // old|dddddd| + // |aaaaaa| + // --------------- + // new|ddd| + // |bbbbbb| + + // - apply the new change deletion to the old change insertion + // + // old|dddddd| + // |aaa| + // --------------- + // new|| + // |bbbbbb| + // + // - move the new change insertion forward by the same amount as its consumed deletion to remain aligned with the old change. + // (because the old change and new change have the same adjusted start position, the new change insertion appears directly before the old change insertion in the final text) + // + // old|dddddd| + // |aaa| + // --------------- + // new|| + // |bbbbbb| + + oldChange = new TextChangeRange(oldChange.Span, oldChange.NewLength - newChange.SpanLength); + + // the new change deletion is equal to the subset of the old change insertion that we are consuming this iteration + oldDelta = oldDelta + newChange.SpanLength; + + // since the new change insertion occurs before the old change, consume it now + newChange = new UnadjustedNewChange(newChange.SpanEnd, spanLength: 0, newChange.NewLength); + adjustAndAddNewChange(builder, oldDelta, newChange); + if (tryGetNextNewChange()) continue; + else break; } - else if (newChange.Span.Start > oldChange.Span.Start + oldDelta + oldChange.NewLength) + else { - // new change occurs entirely after old change - AddRange(list, oldChange); + // new change deletes more characters than old change inserted + // + // old|d| + // |aa| + // --------------- + // new|ddd| + // |bbb| + + // merge the old change into the new change: + // - new change deletion deletes all of the old change insertion. reduce the new change deletion accordingly + // + // old|d| + // || + // --------------- + // new|d| + // |bbb| + // + // - old change deletion is simply added to the new change deletion. + // + // old|| + // || + // --------------- + // new|dd| + // |bbb| + // + // - new change is moved to put its adjusted position equal to the old change we just merged in + // + // old|| + // || + // --------------- + // new|dd| + // |bbb| + + // adjust the oldDelta to reflect that the old change has been consumed oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; - oldIndex++; - needNextOldChange = true; - goto nextOldChange; - } - else if (newChange.Span.Start < oldChange.Span.Start + oldDelta) - { - // new change starts before old change, but overlaps - // add as much of new change deletion as possible and try again - var newChangeLeadingDeletion = (oldChange.Span.Start + oldDelta) - newChange.Span.Start; - AddRange(list, new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, newChangeLeadingDeletion), 0)); - newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length - newChangeLeadingDeletion), newChange.NewLength); - goto tryAgain; - } - else if (newChange.Span.Start > oldChange.Span.Start + oldDelta) - { - // new change starts after old change, but overlaps - // add as much of the old change as possible and try again - var oldChangeLeadingInsertion = newChange.Span.Start - (oldChange.Span.Start + oldDelta); - var oldChangeLeadingDeletion = Math.Min(oldChange.Span.Length, oldChangeLeadingInsertion); - AddRange(list, new TextChangeRange(new TextSpan(oldChange.Span.Start, oldChangeLeadingDeletion), oldChangeLeadingInsertion)); - oldDelta = oldDelta - oldChangeLeadingDeletion + oldChangeLeadingInsertion; - oldChange = new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, oldChange.Span.Length - oldChangeLeadingDeletion), oldChange.NewLength - oldChangeLeadingInsertion); - goto tryAgain; - } - else if (newChange.Span.Start == oldChange.Span.Start + oldDelta) - { - // new change and old change start at same position - if (oldChange.NewLength == 0) - { - // old change is just a deletion, go ahead and old change now and deal with new change separately - AddRange(list, oldChange); - oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; - oldIndex++; - needNextOldChange = true; - goto nextOldChange; - } - else if (newChange.Span.Length < oldChange.NewLength) - { - // new change deletes fewer characters than old change inserted - // apply as much of the new change as possible, and adjust the old change - var appliedDeletion = Math.Min(oldChange.Span.Length, newChange.Span.Length); - var adjustedNewChange = new TextChangeRange(new TextSpan(oldChange.Span.Start, appliedDeletion), newChange.NewLength); - AddRange(list, adjustedNewChange); - oldChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + appliedDeletion, oldChange.Span.Length - appliedDeletion), oldChange.NewLength - newChange.Span.Length); - newIndex++; - needNextNewChange = true; - goto nextNewChange; - } - else - { - // new change deletes the entire old change. apply as much of the new change as possible and - // adjust the remaining. - var appliedDeletion = oldChange.NewLength; - var adjustedNewChange = new TextChangeRange(oldChange.Span, 0); - AddRange(list, adjustedNewChange); - newChange = new TextChangeRange(new TextSpan(newChange.Span.Start + appliedDeletion, newChange.Span.Length - appliedDeletion), newChange.NewLength); - - oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; - oldIndex++; - needNextOldChange = true; - goto nextOldChange; - } + + var newDeletion = newChange.SpanLength + oldChange.Span.Length - oldChange.NewLength; + newChange = new UnadjustedNewChange(oldChange.Span.Start + oldDelta, newDeletion, newChange.NewLength); + if (tryGetNextOldChange()) continue; + else break; } } - else - { - // no more old changes, just add adjusted new change - var adjustedNewChange = new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, newChange.Span.Length), newChange.NewLength); - AddRange(list, adjustedNewChange); - newIndex++; - needNextNewChange = true; - goto nextNewChange; - } } - else + + // there may be remaining old changes or remaining new changes (not both, and not neither) + switch (oldIndex == oldChanges.Length, newIndex == newChanges.Length) { - // no more new changes, just add remaining old changes - while (oldIndex < oldChanges.Length) - { - if (needNextOldChange) - { - oldChange = oldChanges[oldIndex]; - } + case (true, true): + case (false, false): + throw new InvalidOperationException(); + } - AddRange(list, oldChange); - oldIndex++; - needNextOldChange = true; - } + while (oldIndex < oldChanges.Length) + { + addAndAdjustOldDelta(builder, ref oldDelta, oldChange); + tryGetNextOldChange(); } - return list.ToImmutableArray(); - } + while (newIndex < newChanges.Length) + { + adjustAndAddNewChange(builder, oldDelta, newChange); + tryGetNextNewChange(); + } - private static void AddRange(List list, TextChangeRange range) - { - if (list.Count > 0) + return builder.ToImmutableAndFree(); + + bool tryGetNextOldChange() { - var last = list[list.Count - 1]; - if (last.Span.End == range.Span.Start) + oldIndex++; + if (oldIndex < oldChanges.Length) { - // merge changes together if they are adjacent - list[list.Count - 1] = new TextChangeRange(new TextSpan(last.Span.Start, last.Span.Length + range.Span.Length), last.NewLength + range.NewLength); - return; + oldChange = oldChanges[oldIndex]; + return true; } else { - Debug.Assert(range.Span.Start > last.Span.End); + oldChange = default; + return false; } } - list.Add(range); + bool tryGetNextNewChange() + { + newIndex++; + if (newIndex < newChanges.Length) + { + newChange = new UnadjustedNewChange(newChanges[newIndex]); + return true; + } + else + { + newChange = default; + return false; + } + } + + static void addAndAdjustOldDelta(ArrayBuilder builder, ref int oldDelta, TextChangeRange oldChange) + { + // modify oldDelta to reflect characters deleted and inserted by an old change + oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; + add(builder, oldChange); + } + + static void adjustAndAddNewChange(ArrayBuilder builder, int oldDelta, UnadjustedNewChange newChange) + { + // unadjusted new change is relative to the original text with old changes applied. Subtract oldDelta to make it relative to the original text. + add(builder, new TextChangeRange(new TextSpan(newChange.SpanStart - oldDelta, newChange.SpanLength), newChange.NewLength)); + } + + static void add(ArrayBuilder builder, TextChangeRange change) + { + if (builder.Count > 0) + { + var last = builder[^1]; + if (last.Span.End == change.Span.Start) + { + // merge changes together if they are adjacent + builder[^1] = new TextChangeRange(new TextSpan(last.Span.Start, last.Span.Length + change.Span.Length), last.NewLength + change.NewLength); + return; + } + else if (last.Span.End > change.Span.Start) + { + throw new ArgumentOutOfRangeException(nameof(change)); + } + + } + + builder.Add(change); + } } /// diff --git a/src/Compilers/Core/Portable/Text/TextChange.cs b/src/Compilers/Core/Portable/Text/TextChange.cs index 7b63c1a8336a1..c6ca7b96b2486 100644 --- a/src/Compilers/Core/Portable/Text/TextChange.cs +++ b/src/Compilers/Core/Portable/Text/TextChange.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.Serialization; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Text @@ -15,6 +14,7 @@ namespace Microsoft.CodeAnalysis.Text /// Describes a single change when a particular span is replaced with a new text. /// [DataContract] + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] public readonly struct TextChange : IEquatable { /// @@ -95,5 +95,16 @@ public static implicit operator TextChangeRange(TextChange change) /// An empty set of changes. /// public static IReadOnlyList NoChanges => SpecializedCollections.EmptyReadOnlyList(); + + internal string GetDebuggerDisplay() + { + var newTextDisplay = NewText switch + { + null => "null", + { Length: < 10 } => $"\"{NewText}\"", + { Length: var length } => $"(NewLength = {length})" + }; + return $"new TextChange(new TextSpan({Span.Start}, {Span.Length}), {newTextDisplay})"; + } } } diff --git a/src/Compilers/Core/Portable/Text/TextChangeRange.cs b/src/Compilers/Core/Portable/Text/TextChangeRange.cs index 85920b714672b..24d65b45216b8 100644 --- a/src/Compilers/Core/Portable/Text/TextChangeRange.cs +++ b/src/Compilers/Core/Portable/Text/TextChangeRange.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using Microsoft.CodeAnalysis.Text; +using System.Diagnostics; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Text @@ -12,6 +12,7 @@ namespace Microsoft.CodeAnalysis.Text /// /// Represents the change to a span of text. /// + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] public readonly struct TextChangeRange : IEquatable { /// @@ -24,6 +25,8 @@ namespace Microsoft.CodeAnalysis.Text /// public int NewLength { get; } + internal int NewEnd => Span.Start + NewLength; + /// /// Initializes a new instance of . /// @@ -125,5 +128,10 @@ public static TextChangeRange Collapse(IEnumerable changes) return new TextChangeRange(combined, newLen); } + + private string GetDebuggerDisplay() + { + return $"new TextChangeRange(new TextSpan({Span.Start}, {Span.Length}), {NewLength})"; + } } }