// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // // This file contain implementations details that are subject to change without notice. // Use at your own risk. // namespace Microsoft.VisualStudio.Text.Implementation { using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Microsoft.VisualStudio.Text.Differencing; using Microsoft.VisualStudio.Text.Utilities; internal partial class NormalizedTextChangeCollection : INormalizedTextChangeCollection { public static readonly NormalizedTextChangeCollection Empty = new NormalizedTextChangeCollection(Array.Empty()); private readonly IReadOnlyList _changes; public static INormalizedTextChangeCollection Create(IReadOnlyList changes) { INormalizedTextChangeCollection result = GetTrivialCollection(changes); return result != null ? result : new NormalizedTextChangeCollection(changes); } public static INormalizedTextChangeCollection Create(IReadOnlyList changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService, ITextSnapshot before = null, ITextSnapshot after = null) { INormalizedTextChangeCollection result = GetTrivialCollection(changes); return result != null ? result : new NormalizedTextChangeCollection(changes, differenceOptions, textDifferencingService, before, after); } private static INormalizedTextChangeCollection GetTrivialCollection(IReadOnlyList changes) { if (changes == null) { throw new ArgumentNullException(nameof(changes)); } if (changes.Count == 0) { return NormalizedTextChangeCollection.Empty; } else if (changes.Count == 1) { TextChange tc = changes[0]; if (tc.OldLength + tc.NewLength == 1 && tc.LineBreakBoundaryConditions == LineBreakBoundaryConditions.None && tc.LineCountDelta == 0) { bool isInsertion = tc.NewLength == 1; char data = isInsertion ? tc.NewText[0] : tc.OldText[0]; return new TrivialNormalizedTextChangeCollection(data, isInsertion, tc.OldPosition); } } return null; } /// /// Construct a normalized version of the given TextChange collection, /// but don't compute minimal edits. /// /// List of changes to normalize private NormalizedTextChangeCollection(IReadOnlyList changes) { _changes = Normalize(changes, null, null, null, null); } /// /// Construct a normalized version of the given TextChange collection. /// /// List of changes to normalize /// The difference options to use for minimal differencing, if any. /// The difference service to use, if differenceOptions were supplied. /// Text snapshot before the change (can be null). /// Text snapshot after the change (can be null). private NormalizedTextChangeCollection(IReadOnlyList changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService, ITextSnapshot before, ITextSnapshot after) { _changes = Normalize(changes, differenceOptions, textDifferencingService, before, after); } public bool IncludesLineChanges { get { foreach (ITextChange change in this) { if (change.LineCountDelta != 0) { return true; } } return false; } } /// /// Normalize a sequence of changes that were all applied consecutively to the same version of a buffer. Positions of the /// normalized changes are adjusted to account for other changes that occur at lower indexes in the /// buffer, and changes are sorted and merged if possible. /// /// The changes to normalize. /// The options to use for minimal differencing, if any. /// Text snapshot before the change (can be null). /// Text snapshot after the change (can be null). /// A (possibly empty) list of changes, sorted by Position, with adjacent and overlapping changes combined /// where possible. /// is null. private static IReadOnlyList Normalize(IReadOnlyList changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService, ITextSnapshot before, ITextSnapshot after) { if (changes.Count == 1 && differenceOptions == null) { // By far the most common path // If we are computing minimal changes, we need to go through the // algorithm anyway, since this change may be split up into many // smaller changes return new TextChange[] { changes[0] }; } else if (changes.Count == 0) { return Array.Empty(); } TextChange[] work = TextUtilities.StableSort(changes, TextChange.Compare); // work is now sorted by increasing Position int accumulatedDelta = 0; int a = 0; int b = 1; while (b < work.Length) { // examine a pair of changes and attempt to combine them TextChange aChange = work[a]; TextChange bChange = work[b]; int gap = bChange.OldPosition - aChange.OldEnd; if (gap > 0) { // independent changes aChange.NewPosition = aChange.OldPosition + accumulatedDelta; accumulatedDelta += aChange.Delta; a = b; b = a + 1; } else { // dependent changes. merge all adjacent dependent changes into a single change in one pass, // to avoid expensive pairwise concatenations. // // Use StringRebuilders (which allow strings to be concatenated without creating copies of the strings) to assemble the // pieces. StringRebuilder newRebuilder = aChange._newText; StringRebuilder oldRebuilder = aChange._oldText; int aChangeIncrementalDeletions = 0; do { newRebuilder = newRebuilder.Append(bChange._newText); if (gap == 0) { // abutting deletions oldRebuilder = oldRebuilder.Append(bChange._oldText); aChangeIncrementalDeletions += bChange.OldLength; aChange.LineBreakBoundaryConditions = (aChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) | (bChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline); } else { // overlapping deletions if (aChange.OldEnd + aChangeIncrementalDeletions < bChange.OldEnd) { int overlap = aChange.OldEnd + aChangeIncrementalDeletions - bChange.OldPosition; oldRebuilder = oldRebuilder.Append(bChange._oldText.GetSubText(Span.FromBounds(overlap, bChange._oldText.Length))); aChangeIncrementalDeletions += (bChange.OldLength - overlap); aChange.LineBreakBoundaryConditions = (aChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) | (bChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline); } // else bChange deletion subsumed by aChange deletion } work[b] = null; b++; if (b == work.Length) { break; } bChange = work[b]; gap = bChange.OldPosition - aChange.OldEnd - aChangeIncrementalDeletions; } while (gap <= 0); work[a]._oldText = oldRebuilder; work[a]._newText = newRebuilder; if (b < work.Length) { aChange.NewPosition = aChange.OldPosition + accumulatedDelta; accumulatedDelta += aChange.Delta; a = b; b = a + 1; } } } // a points to the last surviving change work[a].NewPosition = work[a].OldPosition + accumulatedDelta; List result = new List(); if (differenceOptions.HasValue) { Requires.NotNull(textDifferencingService, nameof(textDifferencingService)); foreach (TextChange change in work) { if (change == null) continue; // Make sure this is a replacement if (change.OldLength == 0 || change.NewLength == 0) { result.Add(change); continue; } if (change.OldLength >= TextModelOptions.DiffSizeThreshold || change.NewLength >= TextModelOptions.DiffSizeThreshold) { change.IsOpaque = true; result.Add(change); continue; // too big to even attempt a diff. This is aimed at the reload-a-giant-file scenario // where OOM during diff is a distinct possibility. } // Make sure to turn off IgnoreTrimWhiteSpace, since that doesn't make sense in // the context of a minimal edit StringDifferenceOptions options = new StringDifferenceOptions(differenceOptions.Value); options.IgnoreTrimWhiteSpace = false; IHierarchicalDifferenceCollection diffs; if (before != null && after != null) { // Don't materialize the strings when we know the before and after snapshots. They might be really huge and cause OOM. // We will take this path in the file reload case. diffs = textDifferencingService.DiffSnapshotSpans(new SnapshotSpan(before, change.OldSpan), new SnapshotSpan(after, change.NewSpan), options); } else { // We need to evaluate the old and new text for the differencing service string oldText = change.OldText; string newText = change.NewText; if (string.Equals(oldText, newText, StringComparison.Ordinal)) { // This change simply evaporates. This case occurs frequently in Venus and it is much // better to short circuit it here than to fire up the differencing engine. continue; } diffs = textDifferencingService.DiffStrings(oldText, newText, options); } // Keep track of deltas for the "new" position, for sanity check int delta = 0; // Add all the changes from the difference collection result.AddRange(GetChangesFromDifferenceCollection(ref delta, change, change._oldText, change._newText, diffs)); // Sanity check // If delta != 0, then we've constructed asymmetrical insertions and // deletions, which should be impossible Debug.Assert(delta == change.Delta, "Minimal edit delta should be equal to replaced text change's delta."); } } // If we aren't computing minimal changes, then copy over the non-null changes else { foreach (TextChange change in work) { if (change != null) result.Add(change); } } return result; } private static IList GetChangesFromDifferenceCollection(ref int delta, TextChange originalChange, StringRebuilder oldText, StringRebuilder newText, IHierarchicalDifferenceCollection diffCollection, int leftOffset = 0, int rightOffset = 0) { List changes = new List(); for (int i = 0; i < diffCollection.Differences.Count; i++) { Difference currentDiff = diffCollection.Differences[i]; Span leftDiffSpan = Translate(diffCollection.LeftDecomposition.GetSpanInOriginal(currentDiff.Left), leftOffset); Span rightDiffSpan = Translate(diffCollection.RightDecomposition.GetSpanInOriginal(currentDiff.Right), rightOffset); // TODO: Since this evaluates differences lazily, we should add something here to *not* compute the next // level of differences if we think it would be too expensive. IHierarchicalDifferenceCollection nextLevelDiffs = diffCollection.GetContainedDifferences(i); if (nextLevelDiffs != null) { changes.AddRange(GetChangesFromDifferenceCollection(ref delta, originalChange, oldText, newText, nextLevelDiffs, leftDiffSpan.Start, rightDiffSpan.Start)); } else { TextChange minimalChange = new TextChange(originalChange.OldPosition + leftDiffSpan.Start, oldText.GetSubText(leftDiffSpan), newText.GetSubText(rightDiffSpan), ComputeBoundaryConditions(originalChange, oldText, leftDiffSpan)); minimalChange.NewPosition = originalChange.NewPosition + rightDiffSpan.Start; if (minimalChange.OldLength > 0 && minimalChange.NewLength > 0) { minimalChange.IsOpaque = true; } delta += minimalChange.Delta; changes.Add(minimalChange); } } return changes; } private static LineBreakBoundaryConditions ComputeBoundaryConditions(TextChange outerChange, StringRebuilder oldText, Span leftSpan) { LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None; if (leftSpan.Start == 0) { bc = (outerChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn); } else if (oldText[leftSpan.Start - 1] == '\r') { bc = LineBreakBoundaryConditions.PrecedingReturn; } if (leftSpan.End == oldText.Length) { bc |= (outerChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline); } else if (oldText[leftSpan.End] == '\n') { bc |= LineBreakBoundaryConditions.SucceedingNewline; } return bc; } private static Span Translate(Span span, int amount) { return new Span(span.Start + amount, span.Length); } private static bool PriorTo(ITextChange denormalizedChange, ITextChange normalizedChange, int accumulatedDelta, int accumulatedNormalizedDelta) { // notice that denormalizedChange.OldPosition == denormalizedChange.NewPosition if ((denormalizedChange.OldLength != 0) && (normalizedChange.OldLength != 0)) { // both deletions return denormalizedChange.OldPosition <= normalizedChange.NewPosition - accumulatedDelta - accumulatedNormalizedDelta; } else { return denormalizedChange.OldPosition < normalizedChange.NewPosition - accumulatedDelta - accumulatedNormalizedDelta; } } /// /// Given a set of changes against a particular snapshot, merge in a list of normalized changes that occurred /// immediately after those changes (as part of the next snapshot) so that the merged changes refer to the /// earlier snapshot. /// /// The list of changes to be merged. /// The list of changes into which to merge. public static void Denormalize(INormalizedTextChangeCollection normalizedChanges, List denormChangesWithSentinel) { // denormalizedChangesWithSentinel contains a list of changes that have been denormalized to the origin snapshot // (the New positions in those changes are the same as the Old positions), and also has a sentinel at the end // that has int.MaxValue for its position. // args.Changes contains a list of changes that are normalized with respect to the most recent snapshot, so we know // that they are independent and properly ordered -- thus we can perform a single merge pass against the // denormalized changes. int rover = 0; int accumulatedDelta = 0; int accumulatedNormalizedDelta = 0; List normChanges = new List(normalizedChanges); for (int n = 0; n < normChanges.Count; ++n) { ITextChange normChange = normChanges[n]; // 1. skip past all denormalized changes that begin prior to the beginning of the current change. while (PriorTo(denormChangesWithSentinel[rover], normChange, accumulatedDelta, accumulatedNormalizedDelta)) { accumulatedDelta += denormChangesWithSentinel[rover++].Delta; } // 2. normChange will be inserted at [rover], but it may need to be split if ((normChange.OldEnd - accumulatedDelta) > denormChangesWithSentinel[rover].OldPosition) { // split required. for example, text at 5..10 was deleted in snapshot 1, and then text at 0..10 was deleted // in snapshot 2; the latter turns into two deletions in terms of snapshot 1: 0..5 and 10..15. int deletionSuffix = (normChange.OldEnd - accumulatedDelta) - denormChangesWithSentinel[rover].OldPosition; int deletionPrefix = normChange.OldLength - deletionSuffix; int normDelta = normChange.NewPosition - normChange.OldPosition; denormChangesWithSentinel.Insert (rover, new TextChange(normChange.OldPosition - accumulatedDelta, TextChange.ChangeOldSubText(normChange, 0, deletionPrefix), TextChange.NewStringRebuilder(normChange), LineBreakBoundaryConditions.None)); accumulatedNormalizedDelta += normDelta; // the second part remains 'normalized' in case it needs to be split again TextChange splitee = new TextChange(normChange.OldPosition + deletionPrefix, TextChange.ChangeOldSubText(normChange, deletionPrefix, deletionSuffix), StringRebuilder.Empty, LineBreakBoundaryConditions.None); splitee.NewPosition += normDelta; normChanges.Insert(n + 1, splitee); } else { denormChangesWithSentinel.Insert (rover, new TextChange(normChange.OldPosition - accumulatedDelta, TextChange.OldStringRebuilder(normChange), TextChange.NewStringRebuilder(normChange), LineBreakBoundaryConditions.None)); accumulatedNormalizedDelta += normChange.Delta; } rover++; } } int ICollection.Count => _changes.Count; bool ICollection.IsReadOnly => true; ITextChange IList.this[int index] { get => _changes[index]; set => throw new System.NotSupportedException(); } int IList.IndexOf(ITextChange item) { for (int i = 0; (i < _changes.Count); ++i) { if (item.Equals(_changes[i])) return i; } return -1; } void IList.Insert(int index, ITextChange item) { throw new System.NotSupportedException(); } void IList.RemoveAt(int index) { throw new System.NotSupportedException(); } void ICollection.Add(ITextChange item) { throw new System.NotSupportedException(); } void ICollection.Clear() { throw new System.NotSupportedException(); } bool ICollection.Contains(ITextChange item) { return ((IList)this).IndexOf(item) != -1; } void ICollection.CopyTo(ITextChange[] array, int arrayIndex) { for (int i = 0; (i < _changes.Count); ++i) { array[i + arrayIndex] = _changes[i]; } } bool ICollection.Remove(ITextChange item) { throw new System.NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return _changes.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _changes.GetEnumerator(); } } }