// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Linq; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Runtime.Serialization; using System.Text; namespace ICSharpCode.AvalonEdit.Utils { /// /// A kind of List<T>, but more efficient for random insertions/removal. /// Also has cheap Clone() and SubRope() implementations. /// /// /// This class is not thread-safe: multiple concurrent write operations or writes concurrent to reads have undefined behaviour. /// Concurrent reads, however, are safe. /// However, clones of a rope are safe to use on other threads even though they share data with the original rope. /// [Serializable] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] public sealed class Rope : IList, ICloneable { internal RopeNode root; internal Rope(RopeNode root) { this.root = root; root.CheckInvariants(); } /// /// Creates a new rope representing the empty string. /// public Rope() { // we'll construct the empty rope as a clone of an imaginary static empty rope this.root = RopeNode.emptyRopeNode; root.CheckInvariants(); } /// /// Creates a rope from the specified input. /// This operation runs in O(N). /// /// input is null. public Rope(IEnumerable input) { if (input == null) throw new ArgumentNullException("input"); Rope inputRope = input as Rope; if (inputRope != null) { // clone ropes instead of copying them inputRope.root.Publish(); this.root = inputRope.root; } else { string text = input as string; if (text != null) { // if a string is IEnumerable, then T must be char ((Rope)(object)this).root = CharRope.InitFromString(text); } else { T[] arr = ToArray(input); this.root = RopeNode.CreateFromArray(arr, 0, arr.Length); } } this.root.CheckInvariants(); } /// /// Creates a rope from a part of the array. /// This operation runs in O(N). /// /// input is null. public Rope(T[] array, int arrayIndex, int count) { VerifyArrayWithRange(array, arrayIndex, count); this.root = RopeNode.CreateFromArray(array, arrayIndex, count); this.root.CheckInvariants(); } /// /// Creates a new rope that lazily initalizes its content. /// /// The length of the rope that will be lazily loaded. /// /// The callback that provides the content for this rope. /// will be called exactly once when the content of this rope is first requested. /// It must return a rope with the specified length. /// Because the initializer function is not called when a rope is cloned, and such clones may be used on another threads, /// it is possible for the initializer callback to occur on any thread. /// /// /// Any modifications inside the rope will also cause the content to be initialized. /// However, insertions at the beginning and the end, as well as inserting this rope into another or /// using the method, allows constructions of larger ropes where parts are /// lazily loaded. /// However, even methods like Concat may sometimes cause the initializer function to be called, e.g. when /// two short ropes are concatenated. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] public Rope(int length, Func> initializer) { if (initializer == null) throw new ArgumentNullException("initializer"); if (length < 0) throw new ArgumentOutOfRangeException("length", length, "Length must not be negative"); if (length == 0) { this.root = RopeNode.emptyRopeNode; } else { this.root = new FunctionNode(length, initializer); } this.root.CheckInvariants(); } static T[] ToArray(IEnumerable input) { T[] arr = input as T[]; return arr ?? input.ToArray(); } /// /// Clones the rope. /// This operation runs in linear time to the number of rope nodes touched since the last clone was created. /// If you count the per-node cost to the operation modifying the rope (doing this doesn't increase the complexity of the modification operations); /// the remainder of Clone() runs in O(1). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public Rope Clone() { // The Publish() call actually modifies this rope instance; but this modification is thread-safe // as long as the tree structure doesn't change during the operation. root.Publish(); return new Rope(root); } object ICloneable.Clone() { return this.Clone(); } /// /// Resets the rope to an empty list. /// Runs in O(1). /// public void Clear() { root = RopeNode.emptyRopeNode; OnChanged(); } /// /// Gets the length of the rope. /// Runs in O(1). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public int Length { get { return root.length; } } /// /// Gets the length of the rope. /// Runs in O(1). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public int Count { get { return root.length; } } /// /// Inserts another rope into this rope. /// Runs in O(lg N + lg M), plus a per-node cost as if newElements.Clone() was called. /// /// newElements is null. /// index or length is outside the valid range. public void InsertRange(int index, Rope newElements) { if (index < 0 || index > this.Length) { throw new ArgumentOutOfRangeException("index", index, "0 <= index <= " + this.Length.ToString(CultureInfo.InvariantCulture)); } if (newElements == null) throw new ArgumentNullException("newElements"); newElements.root.Publish(); root = root.Insert(index, newElements.root); OnChanged(); } /// /// Inserts new elemetns into this rope. /// Runs in O(lg N + M), where N is the length of this rope and M is the number of new elements. /// /// newElements is null. /// index or length is outside the valid range. public void InsertRange(int index, IEnumerable newElements) { if (newElements == null) throw new ArgumentNullException("newElements"); Rope newElementsRope = newElements as Rope; if (newElementsRope != null) { InsertRange(index, newElementsRope); } else { T[] arr = ToArray(newElements); InsertRange(index, arr, 0, arr.Length); } } /// /// Inserts new elements into this rope. /// Runs in O(lg N + M), where N is the length of this rope and M is the number of new elements. /// /// newElements is null. /// index or length is outside the valid range. public void InsertRange(int index, T[] array, int arrayIndex, int count) { if (index < 0 || index > this.Length) { throw new ArgumentOutOfRangeException("index", index, "0 <= index <= " + this.Length.ToString(CultureInfo.InvariantCulture)); } VerifyArrayWithRange(array, arrayIndex, count); if (count > 0) { root = root.Insert(index, array, arrayIndex, count); OnChanged(); } } /// /// Appends multiple elements to the end of this rope. /// Runs in O(lg N + M), where N is the length of this rope and M is the number of new elements. /// /// newElements is null. public void AddRange(IEnumerable newElements) { InsertRange(this.Length, newElements); } /// /// Appends another rope to the end of this rope. /// Runs in O(lg N + lg M), plus a per-node cost as if newElements.Clone() was called. /// /// newElements is null. public void AddRange(Rope newElements) { InsertRange(this.Length, newElements); } /// /// Appends new elements to the end of this rope. /// Runs in O(lg N + M), where N is the length of this rope and M is the number of new elements. /// /// array is null. public void AddRange(T[] array, int arrayIndex, int count) { InsertRange(this.Length, array, arrayIndex, count); } /// /// Removes a range of elements from the rope. /// Runs in O(lg N). /// /// offset or length is outside the valid range. public void RemoveRange(int index, int count) { VerifyRange(index, count); if (count > 0) { root = root.RemoveRange(index, count); OnChanged(); } } /// /// Copies a range of the specified array into the rope, overwriting existing elements. /// Runs in O(lg N + M). /// public void SetRange(int index, T[] array, int arrayIndex, int count) { VerifyRange(index, count); VerifyArrayWithRange(array, arrayIndex, count); if (count > 0) { root = root.StoreElements(index, array, arrayIndex, count); OnChanged(); } } /// /// Creates a new rope and initializes it with a part of this rope. /// Runs in O(lg N) plus a per-node cost as if this.Clone() was called. /// /// offset or length is outside the valid range. /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public Rope GetRange(int index, int count) { VerifyRange(index, count); Rope newRope = Clone(); int endIndex = index + count; newRope.RemoveRange(endIndex, newRope.Length - endIndex); newRope.RemoveRange(0, index); return newRope; } /* #region Equality /// /// Gets whether the two ropes have the same content. /// Runs in O(N + M). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public bool Equals(Rope other) { if (other == null) return false; // quick detection for ropes that are clones of each other: if (other.root == this.root) return true; if (other.Length != this.Length) return false; using (RopeTextReader a = new RopeTextReader(this, false)) { using (RopeTextReader b = new RopeTextReader(other, false)) { int charA, charB; do { charA = a.Read(); charB = b.Read(); if (charA != charB) return false; } while (charA != -1); return true; } } } /// /// Gets whether two ropes have the same content. /// Runs in O(N + M). /// public override bool Equals(object obj) { return Equals(obj as Rope); } /// /// Calculates the hash code of the rope's content. /// Runs in O(N). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public override int GetHashCode() { int hashcode = 0; using (RopeTextReader reader = new RopeTextReader(this, false)) { unchecked { int val; while ((val = reader.Read()) != -1) { hashcode = hashcode * 31 + val; } } } return hashcode; } #endregion */ /// /// Concatenates two ropes. The input ropes are not modified. /// Runs in O(lg N + lg M). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes")] public static Rope Concat(Rope left, Rope right) { if (left == null) throw new ArgumentNullException("left"); if (right == null) throw new ArgumentNullException("right"); left.root.Publish(); right.root.Publish(); return new Rope(RopeNode.Concat(left.root, right.root)); } /// /// Concatenates multiple ropes. The input ropes are not modified. /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes")] public static Rope Concat(params Rope[] ropes) { if (ropes == null) throw new ArgumentNullException("ropes"); Rope result = new Rope(); foreach (Rope r in ropes) result.AddRange(r); return result; } #region Caches / Changed event internal struct RopeCacheEntry { internal readonly RopeNode node; internal readonly int nodeStartIndex; internal RopeCacheEntry(RopeNode node, int nodeStartOffset) { this.node = node; this.nodeStartIndex = nodeStartOffset; } internal bool IsInside(int offset) { return offset >= nodeStartIndex && offset < nodeStartIndex + node.length; } } // cached pointer to 'last used node', used to speed up accesses by index that are close together [NonSerialized] volatile ImmutableStack lastUsedNodeStack; internal void OnChanged() { lastUsedNodeStack = null; root.CheckInvariants(); } #endregion #region GetChar / SetChar /// /// Gets/Sets a single character. /// Runs in O(lg N) for random access. Sequential read-only access benefits from a special optimization and runs in amortized O(1). /// /// Offset is outside the valid range (0 to Length-1). /// /// The getter counts as a read access and may be called concurrently to other read accesses. /// public T this[int index] { get { // use unsigned integers - this way negative values for index overflow and can be tested for with the same check if (unchecked((uint)index >= (uint)this.Length)) { throw new ArgumentOutOfRangeException("index", index, "0 <= index < " + this.Length.ToString(CultureInfo.InvariantCulture)); } RopeCacheEntry entry = FindNodeUsingCache(index).PeekOrDefault(); return entry.node.contents[index - entry.nodeStartIndex]; } set { if (index < 0 || index >= this.Length) { throw new ArgumentOutOfRangeException("index", index, "0 <= index < " + this.Length.ToString(CultureInfo.InvariantCulture)); } root = root.SetElement(index, value); OnChanged(); /* Here's a try at implementing the setter using the cached node stack (UNTESTED code!). * However I don't use the code because it's complicated and doesn't integrate correctly with change notifications. * Instead, I'll use the much easier to understand recursive solution. * Oh, and it also doesn't work correctly with function nodes. ImmutableStack nodeStack = FindNodeUsingCache(offset); RopeCacheEntry entry = nodeStack.Peek(); if (!entry.node.isShared) { entry.node.contents[offset - entry.nodeStartOffset] = value; // missing: clear the caches except for the node stack cache (e.g. ToString() cache?) } else { RopeNode oldNode = entry.node; RopeNode newNode = oldNode.Clone(); newNode.contents[offset - entry.nodeStartOffset] = value; for (nodeStack = nodeStack.Pop(); !nodeStack.IsEmpty; nodeStack = nodeStack.Pop()) { RopeNode parentNode = nodeStack.Peek().node; RopeNode newParentNode = parentNode.CloneIfShared(); if (newParentNode.left == oldNode) { newParentNode.left = newNode; } else { Debug.Assert(newParentNode.right == oldNode); newParentNode.right = newNode; } if (parentNode == newParentNode) { // we were able to change the existing node (it was not shared); // there's no reason to go further upwards ClearCacheOnModification(); return; } else { oldNode = parentNode; newNode = newParentNode; } } // we reached the root of the rope. Debug.Assert(root == oldNode); root = newNode; ClearCacheOnModification(); }*/ } } internal ImmutableStack FindNodeUsingCache(int index) { Debug.Assert(index >= 0 && index < this.Length); // thread safety: fetch stack into local variable ImmutableStack stack = lastUsedNodeStack; ImmutableStack oldStack = stack; if (stack == null) { stack = ImmutableStack.Empty.Push(new RopeCacheEntry(root, 0)); } while (!stack.PeekOrDefault().IsInside(index)) stack = stack.Pop(); while (true) { RopeCacheEntry entry = stack.PeekOrDefault(); // check if we've reached a leaf or function node if (entry.node.height == 0) { if (entry.node.contents == null) { // this is a function node - go down into its subtree entry = new RopeCacheEntry(entry.node.GetContentNode(), entry.nodeStartIndex); // entry is now guaranteed NOT to be another function node } if (entry.node.contents != null) { // this is a node containing actual content, so we're done break; } } // go down towards leaves if (index - entry.nodeStartIndex >= entry.node.left.length) stack = stack.Push(new RopeCacheEntry(entry.node.right, entry.nodeStartIndex + entry.node.left.length)); else stack = stack.Push(new RopeCacheEntry(entry.node.left, entry.nodeStartIndex)); } // write back stack to volatile cache variable // (in multithreaded access, it doesn't matter which of the threads wins - it's just a cache) if (oldStack != stack) { // no need to write when we the cache variable didn't change lastUsedNodeStack = stack; } // this method guarantees that it finds a leaf node Debug.Assert(stack.Peek().node.contents != null); return stack; } #endregion #region ToString / WriteTo internal void VerifyRange(int startIndex, int length) { if (startIndex < 0 || startIndex > this.Length) { throw new ArgumentOutOfRangeException("startIndex", startIndex, "0 <= startIndex <= " + this.Length.ToString(CultureInfo.InvariantCulture)); } if (length < 0 || startIndex + length > this.Length) { throw new ArgumentOutOfRangeException("length", length, "0 <= length, startIndex(" + startIndex + ")+length <= " + this.Length.ToString(CultureInfo.InvariantCulture)); } } internal static void VerifyArrayWithRange(T[] array, int arrayIndex, int count) { if (array == null) throw new ArgumentNullException("array"); if (arrayIndex < 0 || arrayIndex > array.Length) { throw new ArgumentOutOfRangeException("startIndex", arrayIndex, "0 <= arrayIndex <= " + array.Length.ToString(CultureInfo.InvariantCulture)); } if (count < 0 || arrayIndex + count > array.Length) { throw new ArgumentOutOfRangeException("count", count, "0 <= length, arrayIndex(" + arrayIndex + ")+count <= " + array.Length.ToString(CultureInfo.InvariantCulture)); } } /// /// Creates a string from the rope. Runs in O(N). /// /// A string consisting of all elements in the rope as comma-separated list in {}. /// As a special case, Rope<char> will return its contents as string without any additional separators or braces, /// so it can be used like StringBuilder.ToString(). /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public override string ToString() { Rope charRope = this as Rope; if (charRope != null) { return charRope.ToString(0, this.Length); } else { StringBuilder b = new StringBuilder(); foreach (T element in this) { if (b.Length == 0) b.Append('{'); else b.Append(", "); b.Append(element.ToString()); } b.Append('}'); return b.ToString(); } } internal string GetTreeAsString() { #if DEBUG return root.GetTreeAsString(); #else return "Not available in release build."; #endif } #endregion bool ICollection.IsReadOnly { get { return false; } } /// /// Finds the first occurance of item. /// Runs in O(N). /// /// The index of the first occurance of item, or -1 if it cannot be found. /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public int IndexOf(T item) { return IndexOf(item, 0, this.Length); } /// /// Gets the index of the first occurrence the specified item. /// /// Item to search for. /// Start index of the search. /// Length of the area to search. /// The first index where the item was found; or -1 if no occurrence was found. /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public int IndexOf(T item, int startIndex, int count) { VerifyRange(startIndex, count); while (count > 0) { var entry = FindNodeUsingCache(startIndex).PeekOrDefault(); T[] contents = entry.node.contents; int startWithinNode = startIndex - entry.nodeStartIndex; int nodeLength = Math.Min(entry.node.length, startWithinNode + count); int r = Array.IndexOf(contents, item, startWithinNode, nodeLength - startWithinNode); if (r >= 0) return entry.nodeStartIndex + r; count -= nodeLength - startWithinNode; startIndex = entry.nodeStartIndex + nodeLength; } return -1; } /// /// Gets the index of the last occurrence of the specified item in this rope. /// public int LastIndexOf(T item) { return LastIndexOf(item, 0, this.Length); } /// /// Gets the index of the last occurrence of the specified item in this rope. /// /// The search item /// Start index of the area to search. /// Length of the area to search. /// The last index where the item was found; or -1 if no occurrence was found. /// The search proceeds backwards from (startIndex+count) to startIndex. /// This is different than the meaning of the parameters on Array.LastIndexOf! public int LastIndexOf(T item, int startIndex, int count) { VerifyRange(startIndex, count); var comparer = EqualityComparer.Default; for (int i = startIndex + count - 1; i >= startIndex; i--) { if (comparer.Equals(this[i], item)) return i; } return -1; } /// /// Inserts the item at the specified index in the rope. /// Runs in O(lg N). /// public void Insert(int index, T item) { InsertRange(index, new[] { item }, 0, 1); } /// /// Removes a single item from the rope. /// Runs in O(lg N). /// public void RemoveAt(int index) { RemoveRange(index, 1); } /// /// Appends the item at the end of the rope. /// Runs in O(lg N). /// public void Add(T item) { InsertRange(this.Length, new[] { item }, 0, 1); } /// /// Searches the item in the rope. /// Runs in O(N). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public bool Contains(T item) { return IndexOf(item) >= 0; } /// /// Copies the whole content of the rope into the specified array. /// Runs in O(N). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public void CopyTo(T[] array, int arrayIndex) { CopyTo(0, array, arrayIndex, this.Length); } /// /// Copies the a part of the rope into the specified array. /// Runs in O(lg N + M). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public void CopyTo(int index, T[] array, int arrayIndex, int count) { VerifyRange(index, count); VerifyArrayWithRange(array, arrayIndex, count); this.root.CopyTo(index, array, arrayIndex, count); } /// /// Removes the first occurance of an item from the rope. /// Runs in O(N). /// public bool Remove(T item) { int index = IndexOf(item); if (index >= 0) { RemoveAt(index); return true; } return false; } /// /// Retrieves an enumerator to iterate through the rope. /// The enumerator will reflect the state of the rope from the GetEnumerator() call, further modifications /// to the rope will not be visible to the enumerator. /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public IEnumerator GetEnumerator() { this.root.Publish(); return Enumerate(root); } /// /// Creates an array and copies the contents of the rope into it. /// Runs in O(N). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public T[] ToArray() { T[] arr = new T[this.Length]; this.root.CopyTo(0, arr, 0, arr.Length); return arr; } /// /// Creates an array and copies the contents of the rope into it. /// Runs in O(N). /// /// /// This method counts as a read access and may be called concurrently to other read accesses. /// public T[] ToArray(int startIndex, int count) { VerifyRange(startIndex, count); T[] arr = new T[count]; CopyTo(startIndex, arr, 0, count); return arr; } static IEnumerator Enumerate(RopeNode node) { Stack> stack = new Stack>(); while (node != null) { // go to leftmost node, pushing the right parts that we'll have to visit later while (node.contents == null) { if (node.height == 0) { // go down into function nodes node = node.GetContentNode(); continue; } Debug.Assert(node.right != null); stack.Push(node.right); node = node.left; } // yield contents of leaf node for (int i = 0; i < node.length; i++) { yield return node.contents[i]; } // go up to the next node not visited yet if (stack.Count > 0) node = stack.Pop(); else node = null; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } }