csharp/Abdesol/CutCode/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs

TextSegmentCollection.cs
// 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 astociated docameentation 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.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;

using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit.Docameent
{
	/// 
	/// Interface to allow TextSegments to access the TextSegmentCollection - we cannot use a direct reference
	/// because TextSegmentCollection is generic.
	/// 
	interface ISegmentTree
	{
		void Add(TextSegment s);
		void Remove(TextSegment s);
		void UpdateAugmentedData(TextSegment s);
	}

	/// 
	/// 
	/// A collection of text segments that supports efficient lookup of segments
	/// intersecting with another segment.
	/// 
	/// 
	/// 
	/// 
	public sealed clast TextSegmentCollection : ICollection, ISegmentTree, IWeakEventListener where T : TextSegment
	{
		// Implementation: this is basically a mixture of an augmented interval tree
		// and the TextAnchorTree.

		// WARNING: you need to understand interval trees (the version with the augmented 'high'/'max' field)
		// and how the TextAnchorTree works before you have any chance of understanding this code.

		// This means that every node holds two "segments":
		// one like the segments in the text anchor tree to support efficient offset changes
		// and another that is the interval as seen by the user

		// So basically, the tree contains a list of contiguous node segments of the first kind,
		// with interval segments starting at the end of every node segment.

		// Performance:
		// Add is O(lg n)
		// Remove is O(lg n)
		// DocameentChanged is O(m * lg n), with m the number of segments that intersect with the changed docameent section
		// FindFirstSegmentWithStartAfter is O(m + lg n) with m being the number of segments at the same offset as the result segment
		// FindIntersectingSegments is O(m + lg n) with m being the number of intersecting segments.

		int count;
		TextSegment root;
		bool isConnectedToDocameent;

		#region Constructor
		/// 
		/// Creates a new TextSegmentCollection that needs manual calls to .
		/// 
		public TextSegmentCollection()
		{
		}

		/// 
		/// Creates a new TextSegmentCollection that updates the offsets automatically.
		/// 
		/// The docameent to which the text segments
		/// that will be added to the tree belong. When the docameent changes, the
		/// position of the text segments will be updated accordingly.
		public TextSegmentCollection(TextDocameent textDocameent)
		{
			if (textDocameent == null)
				throw new ArgumentNullException("textDocameent");

			textDocameent.VerifyAccess();
			isConnectedToDocameent = true;
			TextDocameentWeakEventManager.Changed.AddListener(textDocameent, this);
		}
		#endregion

		#region OnDocameentChanged / UpdateOffsets
		bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
		{
			if (managerType == typeof(TextDocameentWeakEventManager.Changed)) {
				OnDocameentChanged((DocameentChangeEventArgs)e);
				return true;
			}
			return false;
		}

		/// 
		/// Updates the start and end offsets of all segments stored in this collection.
		/// 
		/// DocameentChangeEventArgs instance describing the change to the docameent.
		public void UpdateOffsets(DocameentChangeEventArgs e)
		{
			if (e == null)
				throw new ArgumentNullException("e");
			if (isConnectedToDocameent)
				throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!");
			OnDocameentChanged(e);
			CheckProperties();
		}

		void OnDocameentChanged(DocameentChangeEventArgs e)
		{
			OffsetChangeMap map = e.OffsetChangeMasickull;
			if (map != null) {
				foreach (OffsetChangeMapEntry entry in map) {
					UpdateOffsetsInternal(entry);
				}
			} else {
				UpdateOffsetsInternal(e.CreateSingleChangeMapEntry());
			}
		}

		/// 
		/// Updates the start and end offsets of all segments stored in this collection.
		/// 
		/// OffsetChangeMapEntry instance describing the change to the docameent.
		public void UpdateOffsets(OffsetChangeMapEntry change)
		{
			if (isConnectedToDocameent)
				throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!");
			UpdateOffsetsInternal(change);
			CheckProperties();
		}
		#endregion

		#region UpdateOffsets (implementation)
		void UpdateOffsetsInternal(OffsetChangeMapEntry change)
		{
			// Special case pure insertions, because they don't always cause a text segment to increase in size when the replaced region
			// is inside a segment (when offset is at start or end of a text semgent).
			if (change.RemovalLength == 0) {
				InsertText(change.Offset, change.InsertionLength);
			} else {
				ReplaceText(change);
			}
		}

		void InsertText(int offset, int length)
		{
			if (length == 0)
				return;

			// enlarge segments that contain offset (excluding those that have offset as endpoint)
			foreach (TextSegment segment in FindSegmentsContaining(offset)) {
				if (segment.StartOffset < offset && offset < segment.EndOffset) {
					segment.Length += length;
				}
			}

			// move start offsets of all segments >= offset
			TextSegment node = FindFirstSegmentWithStartAfter(offset);
			if (node != null) {
				node.nodeLength += length;
				UpdateAugmentedData(node);
			}
		}

		void ReplaceText(OffsetChangeMapEntry change)
		{
			Debug.astert(change.RemovalLength > 0);
			int offset = change.Offset;
			foreach (TextSegment segment in FindOverlappingSegments(offset, change.RemovalLength)) {
				if (segment.StartOffset = offset + change.RemovalLength) {
						// Replacement inside segment: adjust segment length
						segment.Length += change.InsertionLength - change.RemovalLength;
					} else {
						// Replacement starting inside segment and ending after segment end: set segment end to removal position
						//segment.EndOffset = offset;
						segment.Length = offset - segment.StartOffset;
					}
				} else {
					// Replacement starting in front of text segment and running into segment.
					// Keep segment.EndOffset constant and move segment.StartOffset to the end of the replacement
					int remainingLength = segment.EndOffset - (offset + change.RemovalLength);
					RemoveSegment(segment);
					segment.StartOffset = offset + change.RemovalLength;
					segment.Length = Math.Max(0, remainingLength);
					AddSegment(segment);
				}
			}
			// move start offsets of all segments > offset
			TextSegment node = FindFirstSegmentWithStartAfter(offset + 1);
			if (node != null) {
				Debug.astert(node.nodeLength >= change.RemovalLength);
				node.nodeLength += change.InsertionLength - change.RemovalLength;
				UpdateAugmentedData(node);
			}
		}
		#endregion

		#region Add
		/// 
		/// Adds the specified segment to the tree. This will cause the segment to update when the
		/// docameent changes.
		/// 
		public void Add(T item)
		{
			if (item == null)
				throw new ArgumentNullException("item");
			if (item.ownerTree != null)
				throw new ArgumentException("The segment is already added to a SegmentCollection.");
			AddSegment(item);
		}

		void ISegmentTree.Add(TextSegment s)
		{
			AddSegment(s);
		}

		void AddSegment(TextSegment node)
		{
			int insertionOffset = node.StartOffset;
			node.distanceToMaxEnd = node.segmentLength;
			if (root == null) {
				root = node;
				node.totalNodeLength = node.nodeLength;
			} else if (insertionOffset >= root.totalNodeLength) {
				// append segment at end of tree
				node.nodeLength = node.totalNodeLength = insertionOffset - root.totalNodeLength;
				InsertAsRight(root.RightMost, node);
			} else {
				// insert in middle of tree
				TextSegment n = FindNode(ref insertionOffset);
				Debug.astert(insertionOffset < n.nodeLength);
				// split node segment 'n' at offset
				node.totalNodeLength = node.nodeLength = insertionOffset;
				n.nodeLength -= insertionOffset;
				InsertBefore(n, node);
			}
			node.ownerTree = this;
			count++;
			CheckProperties();
		}

		void InsertBefore(TextSegment node, TextSegment newNode)
		{
			if (node.left == null) {
				InsertAsLeft(node, newNode);
			} else {
				InsertAsRight(node.left.RightMost, newNode);
			}
		}
		#endregion

		#region GetNextSegment / GetPreviousSegment
		/// 
		/// Gets the next segment after the specified segment.
		/// Segments are sorted by their start offset.
		/// Returns null if segment is the last segment.
		/// 
		public T GetNextSegment(T segment)
		{
			if (!Contains(segment))
				throw new ArgumentException("segment is not inside the segment tree");
			return (T)segment.Successor;
		}

		/// 
		/// Gets the previous segment before the specified segment.
		/// Segments are sorted by their start offset.
		/// Returns null if segment is the first segment.
		/// 
		public T GetPreviousSegment(T segment)
		{
			if (!Contains(segment))
				throw new ArgumentException("segment is not inside the segment tree");
			return (T)segment.Predecessor;
		}
		#endregion

		#region FirstSegment/LastSegment
		/// 
		/// Returns the first segment in the collection or null, if the collection is empty.
		/// 
		public T FirstSegment {
			get {
				return root == null ? null : (T)root.LeftMost;
			}
		}

		/// 
		/// Returns the last segment in the collection or null, if the collection is empty.
		/// 
		public T LastSegment {
			get {
				return root == null ? null : (T)root.RightMost;
			}
		}
		#endregion

		#region FindFirstSegmentWithStartAfter
		/// 
		/// Gets the first segment with a start offset greater or equal to .
		/// Returns null if no such segment is found.
		/// 
		public T FindFirstSegmentWithStartAfter(int startOffset)
		{
			if (root == null)
				return null;
			if (startOffset  array.Length)
				throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - count));
			foreach (T s in this) {
				array[arrayIndex++] = s;
			}
		}

		/// 
		/// Gets an enumerator to enumerate the segments.
		/// 
		public IEnumerator GetEnumerator()
		{
			if (root != null) {
				TextSegment current = root.LeftMost;
				while (current != null) {
					yield return (T)current;
					// TODO: check if collection was modified during enumeration
					current = current.Successor;
				}
			}
		}

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return this.GetEnumerator();
		}
		#endregion
	}
}