diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs
index bd4c45dbf0..b25a33096a 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs
@@ -130,6 +130,28 @@ namespace ICSharpCode.AvalonEdit.Document.Tests
GC.KeepAlive(anchors);
}
+ [Test]
+ public void MoveAnchorsDuringReplace()
+ {
+ document.Text = "abcd";
+ TextAnchor start = document.CreateAnchor(1);
+ TextAnchor middleDeletable = document.CreateAnchor(2);
+ TextAnchor middleSurvivorLeft = document.CreateAnchor(2);
+ middleSurvivorLeft.SurviveDeletion = true;
+ middleSurvivorLeft.MovementType = AnchorMovementType.BeforeInsertion;
+ TextAnchor middleSurvivorRight = document.CreateAnchor(2);
+ middleSurvivorRight.SurviveDeletion = true;
+ middleSurvivorRight.MovementType = AnchorMovementType.AfterInsertion;
+ TextAnchor end = document.CreateAnchor(3);
+ document.Replace(1, 2, "BxC");
+
+ Assert.AreEqual(1, start.Offset);
+ Assert.IsTrue(middleDeletable.IsDeleted);
+ Assert.AreEqual(1, middleSurvivorLeft.Offset);
+ Assert.AreEqual(4, middleSurvivorRight.Offset);
+ Assert.AreEqual(4, end.Offset);
+ }
+
[Test]
public void CreateAndMoveAnchors()
{
@@ -139,7 +161,7 @@ namespace ICSharpCode.AvalonEdit.Document.Tests
for (int t = 0; t < 250; t++) {
//Console.Write("t = " + t + " ");
int c = rnd.Next(50);
- switch (rnd.Next(4)) {
+ switch (rnd.Next(5)) {
case 0:
//Console.WriteLine("Add c=" + c + " anchors");
for (int i = 0; i < c; i++) {
@@ -196,6 +218,31 @@ namespace ICSharpCode.AvalonEdit.Document.Tests
}
}
break;
+ case 4:
+ int replaceOffset = rnd.Next(document.TextLength);
+ int replaceRemovalLength = rnd.Next(document.TextLength - replaceOffset);
+ int replaceInsertLength = rnd.Next(1000);
+ //Console.WriteLine("ReplaceOffset=" + replaceOffset + " RemovalLength="+replaceRemovalLength + " InsertLength=" + replaceInsertLength);
+ document.Replace(replaceOffset, replaceRemovalLength, new string(' ', replaceInsertLength));
+ for (int i = anchors.Count - 1; i >= 0; i--) {
+ if (expectedOffsets[i] > replaceOffset && expectedOffsets[i] < replaceOffset + replaceRemovalLength) {
+ if (anchors[i].SurviveDeletion) {
+ if (anchors[i].MovementType == AnchorMovementType.AfterInsertion)
+ expectedOffsets[i] = replaceOffset + replaceInsertLength;
+ else
+ expectedOffsets[i] = replaceOffset;
+ } else {
+ Assert.IsTrue(anchors[i].IsDeleted);
+ anchors.RemoveAt(i);
+ expectedOffsets.RemoveAt(i);
+ }
+ } else if (expectedOffsets[i] > replaceOffset) {
+ expectedOffsets[i] += replaceInsertLength - replaceRemovalLength;
+ } else if (expectedOffsets[i] == replaceOffset && replaceRemovalLength == 0 && anchors[i].MovementType == AnchorMovementType.AfterInsertion) {
+ expectedOffsets[i] += replaceInsertLength - replaceRemovalLength;
+ }
+ }
+ break;
}
Assert.AreEqual(anchors.Count, expectedOffsets.Count);
for (int j = 0; j < anchors.Count; j++) {
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
index bf13dc20de..228b98b417 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
@@ -5,6 +5,7 @@
// $Revision$
//
+using ICSharpCode.AvalonEdit.Utils;
using System;
namespace ICSharpCode.AvalonEdit.Document
@@ -47,17 +48,19 @@ namespace ICSharpCode.AvalonEdit.Document
OffsetChangeMap map = offsetChangeMap;
if (map == null) {
// create OffsetChangeMap on demand
- map = new OffsetChangeMap();
- if (this.RemovalLength > 0)
- map.Add(new OffsetChangeMapEntry(this.Offset, -this.RemovalLength));
- if (this.InsertionLength > 0)
- map.Add(new OffsetChangeMapEntry(this.Offset, this.InsertionLength));
+ map = new OffsetChangeMap(1);
+ map.Add(CreateSingleChangeMapEntry());
offsetChangeMap = map;
}
return map;
}
}
+ internal OffsetChangeMapEntry CreateSingleChangeMapEntry()
+ {
+ return new OffsetChangeMapEntry(this.Offset, this.RemovalLength, this.InsertionLength);
+ }
+
///
/// Gets the OffsetChangeMap, or null if the default offset map (=removal followed by insertion) is being used.
///
@@ -74,16 +77,8 @@ namespace ICSharpCode.AvalonEdit.Document
{
if (offsetChangeMap != null)
return offsetChangeMap.GetNewOffset(offset, movementType);
- if (offset >= this.Offset) {
- if (offset <= this.Offset + this.RemovalLength) {
- offset = this.Offset;
- if (movementType == AnchorMovementType.AfterInsertion)
- offset += this.InsertionLength;
- } else {
- offset += this.InsertionLength - this.RemovalLength;
- }
- }
- return offset;
+ else
+ return CreateSingleChangeMapEntry().GetNewOffset(offset, movementType);
}
///
@@ -99,8 +94,9 @@ namespace ICSharpCode.AvalonEdit.Document
///
public DocumentChangeEventArgs(int offset, int removalLength, string insertedText, OffsetChangeMap offsetChangeMap)
{
- if (insertedText == null)
- throw new ArgumentNullException("insertedText");
+ ThrowUtil.CheckNotNegative(offset, "offset");
+ ThrowUtil.CheckNotNegative(removalLength, "removalLength");
+ ThrowUtil.CheckNotNull(insertedText, "insertedText");
this.Offset = offset;
this.RemovalLength = removalLength;
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs
index 46b1def9de..09d815c2c7 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs
@@ -5,6 +5,7 @@
// $Revision$
//
+using ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -16,15 +17,41 @@ namespace ICSharpCode.AvalonEdit.Document
///
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 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.
- /// If the new text is longer than the old text, a single insertion at the end is used to account for the difference.
- /// If the new text is shorter than the old text, a single deletion at the end is used to account for the difference.
+ /// 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 removal the text segment that's too long.
+ /// If the text keeps its old size, this is implemented as OffsetChangeMap.Empty.
+ ///
CharacterReplace
}
@@ -83,7 +110,7 @@ namespace ICSharpCode.AvalonEdit.Document
// check that ChangeMapEntry is in valid range for this document change
if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset)
return false;
- endOffset += entry.Delta;
+ endOffset += entry.InsertionLength - entry.RemovalLength;
}
// check that the total delta matches
return endOffset == offset + insertionLength;
@@ -99,7 +126,8 @@ namespace ICSharpCode.AvalonEdit.Document
OffsetChangeMap newMap = new OffsetChangeMap(this.Count);
for (int i = this.Count - 1; i >= 0; i--) {
OffsetChangeMapEntry entry = this[i];
- newMap.Add(new OffsetChangeMapEntry(entry.Offset, -entry.Delta));
+ // swap InsertionLength and RemovalLength
+ newMap.Add(new OffsetChangeMapEntry(entry.Offset, entry.InsertionLength, entry.RemovalLength));
}
return newMap;
}
@@ -113,7 +141,8 @@ namespace ICSharpCode.AvalonEdit.Document
public struct OffsetChangeMapEntry
{
readonly int offset;
- readonly int delta;
+ readonly int removalLength;
+ readonly int insertionLength;
///
/// The offset at which the change occurs.
@@ -122,21 +151,12 @@ namespace ICSharpCode.AvalonEdit.Document
get { return offset; }
}
- ///
- /// The change delta. If positive, it is equal to InsertionLength; if negative, it is equal to RemovalLength.
- ///
- public int Delta {
- get { return delta; }
- }
-
///
/// The number of characters removed.
/// Returns 0 if this entry represents an insertion.
///
public int RemovalLength {
- get {
- return delta < 0 ? -delta : 0;
- }
+ get { return removalLength; }
}
///
@@ -144,9 +164,7 @@ namespace ICSharpCode.AvalonEdit.Document
/// Returns 0 if this entry represents a removal.
///
public int InsertionLength {
- get {
- return delta > 0 ? delta : 0;
- }
+ get { return insertionLength; }
}
///
@@ -154,24 +172,39 @@ namespace ICSharpCode.AvalonEdit.Document
///
public int GetNewOffset(int oldOffset, AnchorMovementType movementType)
{
- if (oldOffset < this.Offset)
- return oldOffset;
- if (oldOffset > this.Offset + this.RemovalLength)
- return oldOffset + this.Delta;
- // offset is inside removed region
+ if (!(this.removalLength == 0 && oldOffset == this.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 <= this.offset)
+ return oldOffset;
+ // offset is after end of change: movement by normal delta
+ if (oldOffset >= this.offset + this.removalLength)
+ return oldOffset + this.insertionLength - this.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 this.Offset + this.InsertionLength;
+ return this.offset + this.insertionLength;
else
- return this.Offset;
+ return this.offset;
}
///
/// Creates a new OffsetChangeMapEntry instance.
///
- public OffsetChangeMapEntry(int offset, int delta)
+ public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength)
{
+ ThrowUtil.CheckNotNegative(offset, "offset");
+ ThrowUtil.CheckNotNegative(removalLength, "removalLength");
+ ThrowUtil.CheckNotNegative(insertionLength, "insertionLength");
+
this.offset = offset;
- this.delta = delta;
+ this.removalLength = removalLength;
+ this.insertionLength = insertionLength;
}
}
}
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs
index 62dc8bf01a..df0411475f 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs
@@ -12,7 +12,7 @@ namespace ICSharpCode.AvalonEdit.Document
///
/// A TextAnchorNode is placed in the TextAnchorTree.
/// It describes a section of text with a text anchor at the end of the section.
- /// A weak reference is used to refer to the TextAnchor.
+ /// A weak reference is used to refer to the TextAnchor. (to save memory, we derive from WeakReference instead of referencing it)
///
sealed class TextAnchorNode : WeakReference
{
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs
index e2b6f5f430..49715a50f2 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs
@@ -19,6 +19,36 @@ namespace ICSharpCode.AvalonEdit.Document
///
sealed class TextAnchorTree
{
+ // The text anchor tree has difficult requirements:
+ // - it must QUICKLY update the offset in all anchors whenever there is a document change
+ // - it must not reference text anchors directly, using weak references instead
+
+ // Clearly, we cannot afford updating an Offset property on all anchors (that would be O(N)).
+ // So instead, the anchors need to be able to calculate their offset from a data structure
+ // that can be efficiently updated.
+
+ // This implementation is built using an augmented red-black-tree.
+ // There is a 'TextAnchorNode' for each text anchor.
+ // Such a node represents a section of text (just the length is stored) with a (weakly referenced) text anchor at the end.
+
+ // Basically, you can imagine the list of text anchors as a sorted list of text anchors, where each anchor
+ // just stores the distance to the previous anchor.
+ // (next node = TextAnchorNode.Successor, distance = TextAnchorNode.length)
+ // Distances are never negative, so this representation means anchors are always sorted by offset
+ // (the order of anchors at the same offset is undefined)
+
+ // Of course, a linked list of anchors would be way too slow (one would need to traverse the whole list
+ // every time the offset of an anchor is being looked up).
+ // Instead, we use a red-black-tree. We aren't actually using the tree for sorting - it's just a binary tree
+ // as storage format for what's conceptually a list, the red-black properties are used to keep the tree balanced.
+ // Other balanced binary trees would work, too.
+
+ // What makes the tree-form efficient is that is augments the data by a 'totalLength'. Where 'length'
+ // represents the distance to the previous node, 'totalLength' is the sum of all 'length' values in the subtree
+ // under that node.
+ // This allows computing the Offset from an anchor by walking up the list of parent nodes instead of going
+ // through all predecessor nodes. So computing the Offset runs in O(log N).
+
readonly TextDocument document;
readonly List nodesToDelete = new List();
TextAnchorNode root;
@@ -35,16 +65,15 @@ namespace ICSharpCode.AvalonEdit.Document
}
#region Insert Text
- public void InsertText(int offset, int length)
+ void InsertText(int offset, int length)
{
- //Log("InsertText(" + offset + ", " + length + ")");
if (length == 0 || root == null || offset > root.totalLength)
return;
// find the range of nodes that are placed exactly at offset
// beginNode is inclusive, endNode is exclusive
if (offset == root.totalLength) {
- PerformInsertText(root.RightMost, null, length);
+ PerformInsertText(FindActualBeginNode(root.RightMost), null, length);
} else {
TextAnchorNode endNode = FindNode(ref offset);
Debug.Assert(endNode.length > 0);
@@ -54,21 +83,31 @@ namespace ICSharpCode.AvalonEdit.Document
endNode.length += length;
UpdateAugmentedData(endNode);
} else {
- PerformInsertText(endNode.Predecessor, endNode, length);
+ PerformInsertText(FindActualBeginNode(endNode.Predecessor), endNode, length);
}
}
DeleteMarkedNodes();
}
- void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length)
+ TextAnchorNode FindActualBeginNode(TextAnchorNode node)
{
// now find the actual beginNode
- while (beginNode != null && beginNode.length == 0)
- beginNode = beginNode.Predecessor;
- if (beginNode == null) {
+ while (node != null && node.length == 0)
+ node = node.Predecessor;
+ if (node == null) {
// no predecessor = beginNode is first node in tree
- beginNode = root.LeftMost;
+ node = root.LeftMost;
}
+ return node;
+ }
+
+ // Sorts the nodes in the range [beginNode, endNode) by MovementType
+ // and inserts the length between the BeforeInsertion and the AfterInsertion nodes.
+ void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length)
+ {
+ Debug.Assert(beginNode != null);
+ // endNode may be null at the end of the anchor tree
+
// now we need to sort the nodes in the range [beginNode, endNode); putting those with
// MovementType.BeforeInsertion in front of those with MovementType.AfterInsertion
List beforeInsert = new List();
@@ -136,18 +175,42 @@ namespace ICSharpCode.AvalonEdit.Document
}
#endregion
- #region Remove Text
- public void RemoveText(int offset, int length, DelayedEvents delayedEvents)
+ #region Remove or Replace text
+ public void HandleTextChange(OffsetChangeMapEntry entry, DelayedEvents delayedEvents)
{
- //Log("RemoveText(" + offset + ", " + length + ")");
- if (length == 0 || root == null || offset >= root.totalLength)
+ //Log("HandleTextChange(" + entry + ")");
+ if (entry.RemovalLength == 0) {
+ // This is a pure insertion.
+ // Unlike a replace with removal, a pure insertion can result in nodes at the same location
+ // to split depending on their MovementType.
+ // Thus, we handle this case on a separate code path
+ // (the code below looks like it does something similar, but it can only split
+ // the set of deletion survivors, not all nodes at an offset)
+ InsertText(entry.Offset, entry.InsertionLength);
+ return;
+ }
+ // When handling a replacing text change, we need to:
+ // - find all anchors in the deleted segment and delete them / move them to the appropriate
+ // surviving side.
+ // - adjust the segment size between the left and right side
+
+ int offset = entry.Offset;
+ int remainingRemovalLength = entry.RemovalLength;
+ // if the text change is happening after the last anchor, we don't have to do anything
+ if (root == null || offset >= root.totalLength)
return;
TextAnchorNode node = FindNode(ref offset);
- while (node != null && offset + length > node.length) {
+ TextAnchorNode firstDeletionSurvivor = null;
+ // go forward through the tree and delete all nodes in the removal segment
+ while (node != null && offset + remainingRemovalLength > node.length) {
TextAnchor anchor = (TextAnchor)node.Target;
if (anchor != null && anchor.SurviveDeletion) {
- // shorten node
- length -= node.length - offset;
+ if (firstDeletionSurvivor == null)
+ firstDeletionSurvivor = node;
+ // This node should be deleted, but it wants to survive.
+ // We'll just remove the deleted length segment, so the node will be positioned
+ // in front of the removed segment.
+ remainingRemovalLength -= node.length - offset;
node.length = offset;
offset = 0;
UpdateAugmentedData(node);
@@ -155,7 +218,7 @@ namespace ICSharpCode.AvalonEdit.Document
} else {
// delete node
TextAnchorNode s = node.Successor;
- length -= node.length;
+ remainingRemovalLength -= node.length;
RemoveNode(node);
// we already deleted the node, don't delete it twice
nodesToDelete.Remove(node);
@@ -164,8 +227,35 @@ namespace ICSharpCode.AvalonEdit.Document
node = s;
}
}
+ // 'node' now is the first anchor after the deleted segment.
+ // If there are no anchors after the deleted segment, 'node' is null.
+
+ // firstDeletionSurvivor was set to the first node surviving deletion.
+ // Because all non-surviving nodes up to 'node' were deleted, the node range
+ // [firstDeletionSurvivor, node) now refers to the set of all deletion survivors.
+
+ // do the remaining job of the removal
+ if (node != null) {
+ node.length -= remainingRemovalLength;
+ Debug.Assert(node.length >= 0);
+ }
+ if (entry.InsertionLength > 0) {
+ // we are performing a replacement
+ if (firstDeletionSurvivor != null) {
+ // We got deletion survivors which need to be split into BeforeInsertion
+ // and AfterInsertion groups.
+ // Take care that we don't regroup everything at offset, but only the deletion
+ // survivors - from firstDeletionSurvivor (inclusive) to node (exclusive).
+ // This ensures that nodes immediately before or after the replaced segment
+ // stay where they are (independent from their MovementType)
+ PerformInsertText(firstDeletionSurvivor, node, entry.InsertionLength);
+ } else if (node != null) {
+ // No deletion survivors:
+ // just perform the insertion
+ node.length += entry.InsertionLength;
+ }
+ }
if (node != null) {
- node.length -= length;
UpdateAugmentedData(node);
}
DeleteMarkedNodes();
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs
index c2d24b2ee7..489534bb29 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs
@@ -334,7 +334,7 @@ namespace ICSharpCode.AvalonEdit.Document
Replace(offset, length, text, null);
}
- ///
+ ///
/// Replaces text.
/// Runtime:
/// for updating the text buffer: m=size of new text, d=distance to last change, n=total text length
@@ -351,23 +351,44 @@ namespace ICSharpCode.AvalonEdit.Document
{
if (text == null)
throw new ArgumentNullException("text");
+ // Please see OffsetChangeMappingType XML comments for details on how these modes work.
switch (offsetChangeMappingType) {
- case OffsetChangeMappingType.RemoveAndInsert:
+ case OffsetChangeMappingType.Normal:
Replace(offset, length, text, null);
break;
+ case OffsetChangeMappingType.RemoveAndInsert:
+ if (length == 0 || text.Length == 0) {
+ // only insertion or only removal?
+ // OffsetChangeMappingType doesn't matter, just use Normal.
+ Replace(offset, length, text, null);
+ } else {
+ OffsetChangeMap map = new OffsetChangeMap(2);
+ map.Add(new OffsetChangeMapEntry(offset, length, 0));
+ map.Add(new OffsetChangeMapEntry(offset, 0, text.Length));
+ Replace(offset, length, text, map);
+ }
+ break;
case OffsetChangeMappingType.CharacterReplace:
- if (text.Length > length) {
+ if (length == 0 || text.Length == 0) {
+ // only insertion or only removal?
+ // OffsetChangeMappingType doesn't matter, just use Normal.
+ Replace(offset, length, text, null);
+ } else if (text.Length > length) {
OffsetChangeMap map = new OffsetChangeMap(1);
- map.Add(new OffsetChangeMapEntry(offset + length, text.Length - length));
+ // look at OffsetChangeMappingType.CharacterReplace XML comments on why we need to replace
+ // the last
+ map.Add(new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.Length - length));
Replace(offset, length, text, map);
} else if (text.Length < length) {
OffsetChangeMap map = new OffsetChangeMap(1);
- map.Add(new OffsetChangeMapEntry(offset + length - text.Length, text.Length - length));
+ map.Add(new OffsetChangeMapEntry(offset + length - text.Length, length - text.Length, 0));
Replace(offset, length, text, map);
} else {
Replace(offset, length, text, OffsetChangeMap.Empty);
}
break;
+ default:
+ throw new ArgumentOutOfRangeException("offsetChangeMappingType", offsetChangeMappingType, "Invalid enum value");
}
}
@@ -384,10 +405,11 @@ namespace ICSharpCode.AvalonEdit.Document
/// The new text.
/// The offsetChangeMap determines how offsets inside the old text are mapped to the new text.
/// This affects how the anchors and segments inside the replaced region behave.
- /// If you pass null (the default when using one of the other overloads), the offsets are changed as if
- /// the old text is removed first and the new text is inserted later (OffsetChangeMappingType.RemoveAndInsert).
- /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place.
- /// The offsetChangeMap must be a valid 'explanation' for the document change
+ /// If you pass null (the default when using one of the other overloads), the offsets are changed as
+ /// in OffsetChangeMappingType.Normal mode.
+ /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode).
+ /// The offsetChangeMap must be a valid 'explanation' for the document change. See .
+ ///
public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap)
{
if (text == null)
@@ -417,6 +439,12 @@ namespace ICSharpCode.AvalonEdit.Document
if (length == 0 && text.Length == 0)
return;
+ // trying to replace a single character in 'Normal' mode?
+ // for single characters, 'CharacterReplace' mode is equivalent, but more performant
+ // (we don't have to touch the anchorTree at all in 'CharacterReplace' mode)
+ if (length == 1 && text.Length == 1 && offsetChangeMap == null)
+ offsetChangeMap = OffsetChangeMap.Empty;
+
DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, length, text, offsetChangeMap);
// fire DocumentChanging event
@@ -446,12 +474,10 @@ namespace ICSharpCode.AvalonEdit.Document
// update text anchors
if (offsetChangeMap == null) {
- anchorTree.RemoveText(offset, length, delayedEvents);
- anchorTree.InsertText(offset, text.Length);
+ anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents);
} else {
foreach (OffsetChangeMapEntry entry in offsetChangeMap) {
- anchorTree.RemoveText(entry.Offset, entry.RemovalLength, delayedEvents);
- anchorTree.InsertText(entry.Offset, entry.InsertionLength);
+ anchorTree.HandleTextChange(entry, delayedEvents);
}
}
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs
index 7b71a0c5d2..99f2323775 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs
@@ -35,6 +35,9 @@ namespace ICSharpCode.AvalonEdit.Document
// Implementation: this is basically a mixture of an augmented interval tree
// and the TextAnchorTree.
+ // WARNING: you need to understand how 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
diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs
index b9d27259ef..90832ea91a 100644
--- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs
+++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs
@@ -25,13 +25,20 @@ namespace ICSharpCode.AvalonEdit.Utils
/// public VisualLineText(string text) : base(ThrowUtil.CheckNull(text, "text").Length)
///
///
- public static T CheckNull(T val, string parameterName) where T : class
+ public static T CheckNotNull(T val, string parameterName) where T : class
{
if (val == null)
throw new ArgumentNullException(parameterName);
return val;
}
+ public static int CheckNotNegative(int val, string parameterName)
+ {
+ if (val < 0)
+ throw new ArgumentOutOfRangeException(parameterName, val, "value must not be negative");
+ return val;
+ }
+
public static InvalidOperationException NoDocumentAssigned()
{
throw new InvalidOperationException("Document is null");
diff --git a/src/Main/Base/Project/Src/Editor/ITextEditor.cs b/src/Main/Base/Project/Src/Editor/ITextEditor.cs
index aa1ce55b9a..541a430547 100644
--- a/src/Main/Base/Project/Src/Editor/ITextEditor.cs
+++ b/src/Main/Base/Project/Src/Editor/ITextEditor.cs
@@ -51,7 +51,7 @@ namespace ICSharpCode.SharpDevelop.Editor
/// Sets the selection.
///
/// Start offset of the selection
- /// End offset of the selection
+ /// Length of the selection
void Select(int selectionStart, int selectionLength);
///