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> /// 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. /// </summary> /// <remarks> /// Both `oldChanges` and `newChanges` must contain non-overlapping spans in ascending order. /// </remarks> private static ImmutableArray <TextChangeRange> Merge(ImmutableArray <TextChangeRange> oldChanges, ImmutableArray <TextChangeRange> newChanges) { // Earlier steps are expected to prevent us from ever reaching this point with empty change sets. if (oldChanges.IsEmpty) { throw new ArgumentException(nameof(oldChanges)); } if (newChanges.IsEmpty) { throw new ArgumentException(nameof(newChanges)); } var builder = ArrayBuilder <TextChangeRange> .GetInstance(); 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 (oldChange.Span.Length == 0 && oldChange.NewLength == 0) { // old change does not insert or delete any characters, so it can be dropped to no effect. if (tryGetNextOldChange()) { continue; } else { break; } } else if (newChange.SpanLength == 0 && newChange.NewLength == 0) { // 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); if (newChange.SpanLength <= oldChange.NewLength) { // 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 { // 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; var newDeletion = newChange.SpanLength + oldChange.Span.Length - oldChange.NewLength; newChange = new UnadjustedNewChange(oldChange.Span.Start + oldDelta, newDeletion, newChange.NewLength); if (tryGetNextOldChange()) { continue; } else { break; } } } } // there may be remaining old changes or remaining new changes (not both, and not neither) switch (oldIndex == oldChanges.Length, newIndex == newChanges.Length) {
public UnadjustedNewChange(TextChangeRange range) : this(range.Span.Start, range.Span.Length, range.NewLength) { }
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); 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++; goto nextOldChange; } else if (newChange.Span.Length <= oldChange.NewLength) { // new change deletes fewer characters than old change inserted // add new change insertion, then the remaining trailing characters of the old change insertion AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength + newChange.NewLength - newChange.Span.Length)); oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength - newChange.Span.Length; 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 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; var needNextNewChange = true; var needNextOldChange = true; TextChangeRange newChange = default; TextChangeRange oldChange = default; nextNewChange: if (newIndex < newChanges.Length) { if (needNextNewChange) { newChange = newChanges[newIndex]; needNextNewChange = false; } nextOldChange: if (oldIndex < oldChanges.Length) { if (needNextOldChange) { oldChange = oldChanges[oldIndex]; needNextOldChange = false; } 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)) { // 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; } 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++; 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; } } } 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 { // no more new changes, just add remaining old changes while (oldIndex < oldChanges.Length) { if (needNextOldChange) { oldChange = oldChanges[oldIndex]; } AddRange(list, oldChange); oldIndex++; needNextOldChange = true; } } return(list.ToImmutableArray()); }