Document
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
}
}