| File: Text\ChangedText.cs | Web Access |
| Project: src\roslyn\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis) |
// 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. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Text { internal sealed class ChangedText : SourceText { private readonly SourceText _newText; private readonly ChangeInfo _info; public ChangedText(SourceText oldText, SourceText newText, ImmutableArray<TextChangeRange> changeRanges) : base(checksumAlgorithm: oldText.ChecksumAlgorithm) { RoslynDebug.Assert(newText != null); Debug.Assert(newText is CompositeText || newText is SubText || newText is StringText || newText is LargeText); RoslynDebug.Assert(oldText != null); Debug.Assert(oldText != newText); Debug.Assert(!changeRanges.IsDefault); RequiresChangeRangesAreValid(oldText, newText, changeRanges); _newText = newText; _info = new ChangeInfo(changeRanges, new WeakReference<SourceText>(oldText), (oldText as ChangedText)?._info); } private static void RequiresChangeRangesAreValid( SourceText oldText, SourceText newText, ImmutableArray<TextChangeRange> changeRanges) { var deltaLength = 0; foreach (var change in changeRanges) deltaLength += change.NewLength - change.Span.Length; if (oldText.Length + deltaLength != newText.Length) throw new InvalidOperationException("Delta length difference of change ranges didn't match before/after text length."); var position = 0; foreach (var change in changeRanges) { if (change.Span.Start < position) throw new InvalidOperationException("Change preceded current position in oldText"); if (change.Span.Start > oldText.Length) throw new InvalidOperationException("Change start was after the end of oldText"); if (change.Span.End > oldText.Length) throw new InvalidOperationException("Change end was after the end of oldText"); position = change.Span.End; } } 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 (ChangeInfo? 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 { get { return _newText.Encoding; } } public IEnumerable<TextChangeRange> Changes { get { return _info.ChangeRanges; } } public override int Length { get { return _newText.Length; } } internal override int StorageSize { get { return _newText.StorageSize; } } internal override ImmutableArray<SourceText> Segments { get { return _newText.Segments; } } internal override SourceText StorageKey { get { return _newText.StorageKey; } } public override char this[int position] { get { return _newText[position]; } } public override string ToString(TextSpan span) { return _newText.ToString(span); } public override SourceText GetSubText(TextSpan span) { return _newText.GetSubText(span); } public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) { _newText.CopyTo(sourceIndex, destination, destinationIndex, count); } public override SourceText WithChanges(IEnumerable<TextChange> changes) { // compute changes against newText to avoid capturing strong references to this ChangedText instance. // _newText will only ever be one of CompositeText, SubText, StringText or LargeText, so calling WithChanges on it // will either produce a ChangeText instance or the original instance in case of a empty change. var changed = _newText.WithChanges(changes) as ChangedText; if (changed != null) { return new ChangedText(this, changed._newText, changed._info.ChangeRanges); } else { // change was empty, so just return this same instance return this; } } public override IReadOnlyList<TextChangeRange> GetChangeRanges(SourceText oldText) { if (oldText == null) { throw new ArgumentNullException(nameof(oldText)); } if (this == oldText) { return TextChangeRange.NoChanges; } // try this quick check first SourceText? 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 (ChangeInfo? 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>>(); ChangeInfo? change = newText._info; list.Add(change.ChangeRanges); while (change != null) { SourceText? actualOldText; change.WeakOldText.TryGetTarget(out actualOldText); if (actualOldText == oldText) { return list; } change = change.Previous; if (change != null) { list.Insert(0, change.ChangeRanges); } } // 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 = TextChangeRangeExtensions.Merge(merged, changeSets[i]); } return merged; } protected override TextLineCollection GetLinesCore() => _newText.Lines; internal static class TestAccessor { public static ImmutableArray<TextChangeRange> Merge(ImmutableArray<TextChangeRange> oldChanges, ImmutableArray<TextChangeRange> newChanges) => TextChangeRangeExtensions.Merge(oldChanges, newChanges); } } }