From 080f1697e97e13461ec6df4d31c8924d01257a1b Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Tue, 9 Apr 2019 01:47:48 +0300 Subject: MERGE --- .../Document/OffsetChangeMap.cs | 347 +++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs') diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs new file mode 100644 index 000000000..9494af56b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs @@ -0,0 +1,347 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Document +{ + /// + /// Contains predefined offset change mapping types. + /// + public enum OffsetChangeMappingType + { + /// + /// Normal replace. + /// Anchors in front of the replaced region will stay in front, anchors after the replaced region will stay after. + /// Anchors in the middle of the removed region will be deleted. If they survive deletion, + /// they move depending on their AnchorMovementType. + /// + /// + /// This is the default implementation of DocumentChangeEventArgs when OffsetChangeMap is null, + /// so using this option usually works without creating an OffsetChangeMap instance. + /// This is equivalent to an OffsetChangeMap with a single entry describing the replace operation. + /// + Normal, + /// + /// First the old text is removed, then the new text is inserted. + /// Anchors immediately in front (or after) the replaced region may move to the other side of the insertion, + /// depending on the AnchorMovementType. + /// + /// + /// This is implemented as an OffsetChangeMap with two entries: the removal, and the insertion. + /// + RemoveAndInsert, + /// + /// The text is replaced character-by-character. + /// Anchors keep their position inside the replaced text. + /// Anchors after the replaced region will move accordingly if the replacement text has a different length than the replaced text. + /// If the new text is shorter than the old text, anchors inside the old text that would end up behind the replacement text + /// will be moved so that they point to the end of the replacement text. + /// + /// + /// On the OffsetChangeMap level, growing text is implemented by replacing the last character in the replaced text + /// with itself and the additional text segment. A simple insertion of the additional text would have the undesired + /// effect of moving anchors immediately after the replaced text into the replacement text if they used + /// AnchorMovementStyle.BeforeInsertion. + /// Shrinking text is implemented by removing the text segment that's too long; but in a special mode that + /// causes anchors to always survive irrespective of their setting. + /// If the text keeps its old size, this is implemented as OffsetChangeMap.Empty. + /// + CharacterReplace, + /// + /// Like 'Normal', but anchors with = Default will stay in front of the + /// insertion instead of being moved behind it. + /// + KeepAnchorBeforeInsertion + } + + /// + /// Describes a series of offset changes. + /// + [Serializable] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", + Justification="It's a mapping old offsets -> new offsets")] + public sealed class OffsetChangeMap : Collection + { + /// + /// Immutable OffsetChangeMap that is empty. + /// + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification="The Empty instance is immutable")] + public static readonly OffsetChangeMap Empty = new OffsetChangeMap(Empty.Array, true); + + /// + /// Creates a new OffsetChangeMap with a single element. + /// + /// The entry. + /// Returns a frozen OffsetChangeMap with a single entry. + public static OffsetChangeMap FromSingleElement(OffsetChangeMapEntry entry) + { + return new OffsetChangeMap(new OffsetChangeMapEntry[] { entry }, true); + } + + bool isFrozen; + + /// + /// Creates a new OffsetChangeMap instance. + /// + public OffsetChangeMap() + { + } + + internal OffsetChangeMap(int capacity) + : base(new List(capacity)) + { + } + + private OffsetChangeMap(IList entries, bool isFrozen) + : base(entries) + { + this.isFrozen = isFrozen; + } + + /// + /// Gets the new offset where the specified offset moves after this document change. + /// + public int GetNewOffset(int offset, AnchorMovementType movementType) + { + IList items = this.Items; + int count = items.Count; + for (int i = 0; i < count; i++) { + offset = items[i].GetNewOffset(offset, movementType); + } + return offset; + } + + /// + /// Gets whether this OffsetChangeMap is a valid explanation for the specified document change. + /// + public bool IsValidForDocumentChange(int offset, int removalLength, int insertionLength) + { + int endOffset = offset + removalLength; + foreach (OffsetChangeMapEntry entry in this) { + // check that ChangeMapEntry is in valid range for this document change + if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset) + return false; + endOffset += entry.InsertionLength - entry.RemovalLength; + } + // check that the total delta matches + return endOffset == offset + insertionLength; + } + + /// + /// Calculates the inverted OffsetChangeMap (used for the undo operation). + /// + public OffsetChangeMap Invert() + { + if (this == Empty) + return this; + OffsetChangeMap newMap = new OffsetChangeMap(this.Count); + for (int i = this.Count - 1; i >= 0; i--) { + OffsetChangeMapEntry entry = this[i]; + // swap InsertionLength and RemovalLength + newMap.Add(new OffsetChangeMapEntry(entry.Offset, entry.InsertionLength, entry.RemovalLength)); + } + return newMap; + } + + /// + protected override void ClearItems() + { + CheckFrozen(); + base.ClearItems(); + } + + /// + protected override void InsertItem(int index, OffsetChangeMapEntry item) + { + CheckFrozen(); + base.InsertItem(index, item); + } + + /// + protected override void RemoveItem(int index) + { + CheckFrozen(); + base.RemoveItem(index); + } + + /// + protected override void SetItem(int index, OffsetChangeMapEntry item) + { + CheckFrozen(); + base.SetItem(index, item); + } + + void CheckFrozen() + { + if (isFrozen) + throw new InvalidOperationException("This instance is frozen and cannot be modified."); + } + + /// + /// Gets if this instance is frozen. Frozen instances are immutable and thus thread-safe. + /// + public bool IsFrozen { + get { return isFrozen; } + } + + /// + /// Freezes this instance. + /// + public void Freeze() + { + isFrozen = true; + } + } + + /// + /// An entry in the OffsetChangeMap. + /// This represents the offset of a document change (either insertion or removal, not both at once). + /// + [Serializable] + public struct OffsetChangeMapEntry : IEquatable + { + readonly int offset; + + // MSB: DefaultAnchorMovementIsBeforeInsertion + readonly uint insertionLengthWithMovementFlag; + + // MSB: RemovalNeverCausesAnchorDeletion; other 31 bits: RemovalLength + readonly uint removalLengthWithDeletionFlag; + + /// + /// The offset at which the change occurs. + /// + public int Offset { + get { return offset; } + } + + /// + /// The number of characters inserted. + /// Returns 0 if this entry represents a removal. + /// + public int InsertionLength { + get { return (int)(insertionLengthWithMovementFlag & 0x7fffffff); } + } + + /// + /// The number of characters removed. + /// Returns 0 if this entry represents an insertion. + /// + public int RemovalLength { + get { return (int)(removalLengthWithDeletionFlag & 0x7fffffff); } + } + + /// + /// Gets whether the removal should not cause any anchor deletions. + /// + public bool RemovalNeverCausesAnchorDeletion { + get { return (removalLengthWithDeletionFlag & 0x80000000) != 0; } + } + + /// + /// Gets whether default anchor movement causes the anchor to stay in front of the caret. + /// + public bool DefaultAnchorMovementIsBeforeInsertion { + get { return (insertionLengthWithMovementFlag & 0x80000000) != 0; } + } + + /// + /// Gets the new offset where the specified offset moves after this document change. + /// + public int GetNewOffset(int oldOffset, AnchorMovementType movementType) + { + int insertionLength = this.InsertionLength; + int removalLength = this.RemovalLength; + if (!(removalLength == 0 && oldOffset == offset)) { + // we're getting trouble (both if statements in here would apply) + // if there's no removal and we insert at the offset + // -> we'd need to disambiguate by movementType, which is handled after the if + + // offset is before start of change: no movement + if (oldOffset <= offset) + return oldOffset; + // offset is after end of change: movement by normal delta + if (oldOffset >= offset + removalLength) + return oldOffset + insertionLength - removalLength; + } + // we reach this point if + // a) the oldOffset is inside the deleted segment + // b) there was no removal and we insert at the caret position + if (movementType == AnchorMovementType.AfterInsertion) + return offset + insertionLength; + else if (movementType == AnchorMovementType.BeforeInsertion) + return offset; + else + return this.DefaultAnchorMovementIsBeforeInsertion ? offset : offset + insertionLength; + } + + /// + /// Creates a new OffsetChangeMapEntry instance. + /// + public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength) + { + ThrowUtil.CheckNotNegative(offset, "offset"); + ThrowUtil.CheckNotNegative(removalLength, "removalLength"); + ThrowUtil.CheckNotNegative(insertionLength, "insertionLength"); + + this.offset = offset; + this.removalLengthWithDeletionFlag = (uint)removalLength; + this.insertionLengthWithMovementFlag = (uint)insertionLength; + } + + /// + /// Creates a new OffsetChangeMapEntry instance. + /// + public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength, bool removalNeverCausesAnchorDeletion, bool defaultAnchorMovementIsBeforeInsertion) + : this(offset, removalLength, insertionLength) + { + if (removalNeverCausesAnchorDeletion) + this.removalLengthWithDeletionFlag |= 0x80000000; + if (defaultAnchorMovementIsBeforeInsertion) + this.insertionLengthWithMovementFlag |= 0x80000000; + } + + /// + public override int GetHashCode() + { + unchecked { + return offset + 3559 * (int)insertionLengthWithMovementFlag + 3571 * (int)removalLengthWithDeletionFlag; + } + } + + /// + public override bool Equals(object obj) + { + return obj is OffsetChangeMapEntry && this.Equals((OffsetChangeMapEntry)obj); + } + + /// + public bool Equals(OffsetChangeMapEntry other) + { + return offset == other.offset && insertionLengthWithMovementFlag == other.insertionLengthWithMovementFlag && removalLengthWithDeletionFlag == other.removalLengthWithDeletionFlag; + } + + /// + /// Tests the two entries for equality. + /// + public static bool operator ==(OffsetChangeMapEntry left, OffsetChangeMapEntry right) + { + return left.Equals(right); + } + + /// + /// Tests the two entries for inequality. + /// + public static bool operator !=(OffsetChangeMapEntry left, OffsetChangeMapEntry right) + { + return !left.Equals(right); + } + } +} -- cgit v1.3.1