提交 40b6ac6b 编写于 作者: M Matt Warren 提交者: GitHub

Merge pull request #13446 from mattwar/LongChanges

Long changes
......@@ -620,5 +620,170 @@ public void TestEmptyChangeAfterChange2()
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]
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_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);
}
[Fact]
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);
}
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);
}
}
}
\ No newline at end of file
......@@ -4,18 +4,16 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
namespace Microsoft.CodeAnalysis.Text
{
internal sealed class ChangedText : SourceText
{
private readonly SourceText _newText;
// store old text weakly so we don't form unwanted chains of old texts (especially chains of ChangedTexts)
// It is only used to identify the old text in GetChangeRanges which only returns the changes if old text matches identity.
private readonly WeakReference<SourceText> _weakOldText;
private readonly ImmutableArray<TextChangeRange> _changes;
private readonly ChangeInfo _info;
public ChangedText(SourceText oldText, SourceText newText, ImmutableArray<TextChangeRange> changeRanges)
: base(checksumAlgorithm: oldText.ChecksumAlgorithm)
......@@ -27,8 +25,50 @@ public ChangedText(SourceText oldText, SourceText newText, ImmutableArray<TextCh
Debug.Assert(!changeRanges.IsDefault);
_newText = newText;
_weakOldText = new WeakReference<SourceText>(oldText);
_changes = changeRanges;
_info = new ChangeInfo(changeRanges, new WeakReference<SourceText>(oldText), (oldText as ChangedText)?._info);
}
private class ChangeInfo
{
public ImmutableArray<TextChangeRange> ChangeRanges { get; }
// store old text weakly so we don't form unwanted chains of old texts (especially chains of ChangedTexts)
// used to identify the changes in GetChangeRanges.
public WeakReference<SourceText> WeakOldText { get; }
public ChangeInfo Previous { get; private set; }
public ChangeInfo(ImmutableArray<TextChangeRange> changeRanges, WeakReference<SourceText> weakOldText, ChangeInfo previous)
{
this.ChangeRanges = changeRanges;
this.WeakOldText = weakOldText;
this.Previous = previous;
Clean();
}
// clean up
private void Clean()
{
// look for last info in the chain that still has reference to old text
ChangeInfo lastInfo = this;
for (var info = this; info != null; info = info.Previous)
{
SourceText tmp;
if (info.WeakOldText.TryGetTarget(out tmp))
{
lastInfo = info;
}
}
// break chain for any info's beyond that so they get GC'd
ChangeInfo prev;
while (lastInfo != null)
{
prev = lastInfo.Previous;
lastInfo.Previous = null;
lastInfo = prev;
}
}
}
public override Encoding Encoding
......@@ -38,7 +78,7 @@ public override Encoding Encoding
public IEnumerable<TextChangeRange> Changes
{
get { return _changes; }
get { return _info.ChangeRanges; }
}
public override int Length
......@@ -89,7 +129,7 @@ public override SourceText WithChanges(IEnumerable<TextChange> changes)
var changed = _newText.WithChanges(changes) as ChangedText;
if (changed != null)
{
return new ChangedText(this, changed._newText, changed._changes);
return new ChangedText(this, changed._newText, changed._info.ChangeRanges);
}
else
{
......@@ -110,23 +150,232 @@ public override IReadOnlyList<TextChangeRange> GetChangeRanges(SourceText oldTex
return TextChangeRange.NoChanges;
}
// try this quick check first
SourceText actualOldText;
if (_weakOldText.TryGetTarget(out actualOldText))
if (_info.WeakOldText.TryGetTarget(out actualOldText) && actualOldText == oldText)
{
// the supplied old text is the one we directly reference, so the changes must be the ones we have.
return _info.ChangeRanges;
}
// otherwise look to see if there are a series of changes from the old text to this text and merge them.
if (IsChangedFrom(oldText))
{
var changes = GetChangesBetween(oldText, this);
if (changes.Count > 1)
{
return Merge(changes);
}
}
// the SourceText subtype for editor snapshots knows when two snapshots from the same buffer have the same contents
if (actualOldText != null && actualOldText.GetChangeRanges(oldText).Count == 0)
{
// the texts are different instances, but the contents are considered to be the same.
return _info.ChangeRanges;
}
return ImmutableArray.Create(new TextChangeRange(new TextSpan(0, oldText.Length), _newText.Length));
}
private bool IsChangedFrom(SourceText oldText)
{
for (var info = _info; info != null; info = info.Previous)
{
SourceText text;
if (info.WeakOldText.TryGetTarget(out text) && text == oldText)
{
return true;
}
}
return false;
}
private static IReadOnlyList<ImmutableArray<TextChangeRange>> GetChangesBetween(SourceText oldText, ChangedText newText)
{
var list = new List<ImmutableArray<TextChangeRange>>();
var change = newText._info;
list.Add(change.ChangeRanges);
while (change != null)
{
SourceText actualOldText;
change.WeakOldText.TryGetTarget(out actualOldText);
if (actualOldText == oldText)
{
// same identity, so the changes must be the ones we have.
return _changes;
return list;
}
if (actualOldText.GetChangeRanges(oldText).Count == 0)
change = change.Previous;
if (change != null)
{
// the bases are different instances, but the contents are considered to be the same.
return _changes;
list.Insert(0, change.ChangeRanges);
}
}
return ImmutableArray.Create(new TextChangeRange(new TextSpan(0, oldText.Length), _newText.Length));
// did not find old text, so not connected?
list.Clear();
return list;
}
private static ImmutableArray<TextChangeRange> Merge(IReadOnlyList<ImmutableArray<TextChangeRange>> changeSets)
{
Debug.Assert(changeSets.Count > 1);
var merged = changeSets[0];
for (int i = 1; i < changeSets.Count; i++)
{
merged = Merge(merged, changeSets[i]);
}
return merged;
}
private static ImmutableArray<TextChangeRange> Merge(ImmutableArray<TextChangeRange> oldChanges, ImmutableArray<TextChangeRange> newChanges)
{
var list = new List<TextChangeRange>(oldChanges.Length + newChanges.Length);
int oldIndex = 0;
int newIndex = 0;
int oldDelta = 0;
nextNewChange:
if (newIndex < newChanges.Length)
{
var newChange = newChanges[newIndex];
nextOldChange:
if (oldIndex < oldChanges.Length)
{
var oldChange = oldChanges[oldIndex];
tryAgain:
if (oldChange.Span.Length == 0 && oldChange.NewLength == 0)
{
// old change is a non-change, just ignore it and move on
oldIndex++;
goto nextOldChange;
}
else if (newChange.Span.Length == 0 && newChange.NewLength == 0)
{
// new change is a non-change, just ignore it and move on
newIndex++;
goto nextNewChange;
}
else if (newChange.Span.End < (oldChange.Span.Start + oldDelta))
{
// 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++;
goto nextNewChange;
}
else if (newChange.Span.Start > oldChange.Span.Start + oldDelta + oldChange.NewLength)
{
// new change occurs entirely after old change
AddRange(list, oldChange);
oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength;
oldIndex++;
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);
AddRange(list, new TextChangeRange(oldChange.Span, oldChangeLeadingInsertion));
oldDelta = oldDelta - oldChange.Span.Length + oldChangeLeadingInsertion;
oldChange = new TextChangeRange(new TextSpan(oldChange.Span.Start, 0), oldChange.NewLength - oldChangeLeadingInsertion);
newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length), newChange.NewLength);
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++;
goto nextOldChange;
}
else if (newChange.Span.Length == 0)
{
// new change is just an insertion, go ahead and tack it on with old change
AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength + newChange.NewLength));
oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength;
oldIndex++;
newIndex++;
goto nextNewChange;
}
else
{
// delete as much from old change as new change can
// a new change deletion is a reduction in the old change insertion
var oldChangeReduction = Math.Min(oldChange.NewLength, newChange.Span.Length);
AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength - oldChangeReduction));
oldDelta = oldDelta - oldChange.Span.Length + (oldChange.NewLength - oldChangeReduction);
oldIndex++;
// deduct the amount removed from oldChange from newChange's deletion span (since its already been applied)
newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length - oldChangeReduction), newChange.NewLength);
goto nextOldChange;
}
}
}
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++;
goto nextNewChange;
}
}
else
{
// no more new changes, just add remaining old changes
while (oldIndex < oldChanges.Length)
{
AddRange(list, oldChanges[oldIndex]);
oldIndex++;
}
}
return list.ToImmutableArray();
}
private static void AddRange(List<TextChangeRange> list, TextChangeRange range)
{
if (list.Count > 0)
{
var last = list[list.Count - 1];
if (last.Span.End == range.Span.Start)
{
// 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;
}
else
{
Debug.Assert(range.Span.Start > last.Span.End);
}
}
list.Add(range);
}
/// <summary>
......@@ -137,7 +386,7 @@ protected override TextLineCollection GetLinesCore()
SourceText oldText;
TextLineCollection oldLineInfo;
if (!_weakOldText.TryGetTarget(out oldText) || !oldText.TryGetLines(out oldLineInfo))
if (!_info.WeakOldText.TryGetTarget(out oldText) || !oldText.TryGetLines(out oldLineInfo))
{
// no old line starts? do it the hard way.
return base.GetLinesCore();
......@@ -156,7 +405,7 @@ protected override TextLineCollection GetLinesCore()
// 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 _changes)
foreach (var change in _info.ChangeRanges)
{
// include existing line starts that occur before this change
if (change.Span.Start > position)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册