Browse Source

Implemented updating text anchors on text replace operations.

git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4197 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61
shortcuts
Daniel Grunwald 17 years ago
parent
commit
fc543e2a99
  1. 49
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs
  2. 30
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
  3. 87
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs
  4. 2
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs
  5. 126
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs
  6. 52
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs
  7. 3
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs
  8. 9
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs
  9. 2
      src/Main/Base/Project/Src/Editor/ITextEditor.cs

49
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs

@ -130,6 +130,28 @@ namespace ICSharpCode.AvalonEdit.Document.Tests @@ -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 @@ -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 @@ -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++) {

30
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
// <version>$Revision$</version>
// </file>
using ICSharpCode.AvalonEdit.Utils;
using System;
namespace ICSharpCode.AvalonEdit.Document
@ -47,17 +48,19 @@ 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);
}
/// <summary>
/// Gets the OffsetChangeMap, or null if the default offset map (=removal followed by insertion) is being used.
/// </summary>
@ -74,16 +77,8 @@ namespace ICSharpCode.AvalonEdit.Document @@ -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);
}
/// <summary>
@ -99,8 +94,9 @@ namespace ICSharpCode.AvalonEdit.Document @@ -99,8 +94,9 @@ namespace ICSharpCode.AvalonEdit.Document
/// </summary>
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;

87
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
// <version>$Revision$</version>
// </file>
using ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -16,15 +17,41 @@ namespace ICSharpCode.AvalonEdit.Document @@ -16,15 +17,41 @@ namespace ICSharpCode.AvalonEdit.Document
/// </summary>
public enum OffsetChangeMappingType
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
Normal,
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This is implemented as an OffsetChangeMap with two entries: the removal, and the insertion.
/// </remarks>
RemoveAndInsert,
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
CharacterReplace
}
@ -83,7 +110,7 @@ namespace ICSharpCode.AvalonEdit.Document @@ -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 @@ -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 @@ -113,7 +141,8 @@ namespace ICSharpCode.AvalonEdit.Document
public struct OffsetChangeMapEntry
{
readonly int offset;
readonly int delta;
readonly int removalLength;
readonly int insertionLength;
/// <summary>
/// The offset at which the change occurs.
@ -122,21 +151,12 @@ namespace ICSharpCode.AvalonEdit.Document @@ -122,21 +151,12 @@ namespace ICSharpCode.AvalonEdit.Document
get { return offset; }
}
/// <summary>
/// The change delta. If positive, it is equal to InsertionLength; if negative, it is equal to RemovalLength.
/// </summary>
public int Delta {
get { return delta; }
}
/// <summary>
/// The number of characters removed.
/// Returns 0 if this entry represents an insertion.
/// </summary>
public int RemovalLength {
get {
return delta < 0 ? -delta : 0;
}
get { return removalLength; }
}
/// <summary>
@ -144,9 +164,7 @@ namespace ICSharpCode.AvalonEdit.Document @@ -144,9 +164,7 @@ namespace ICSharpCode.AvalonEdit.Document
/// Returns 0 if this entry represents a removal.
/// </summary>
public int InsertionLength {
get {
return delta > 0 ? delta : 0;
}
get { return insertionLength; }
}
/// <summary>
@ -154,24 +172,39 @@ namespace ICSharpCode.AvalonEdit.Document @@ -154,24 +172,39 @@ namespace ICSharpCode.AvalonEdit.Document
/// </summary>
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;
}
/// <summary>
/// Creates a new OffsetChangeMapEntry instance.
/// </summary>
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;
}
}
}

2
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs

@ -12,7 +12,7 @@ namespace ICSharpCode.AvalonEdit.Document @@ -12,7 +12,7 @@ namespace ICSharpCode.AvalonEdit.Document
/// <summary>
/// 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)
/// </summary>
sealed class TextAnchorNode : WeakReference
{

126
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs

@ -19,6 +19,36 @@ namespace ICSharpCode.AvalonEdit.Document @@ -19,6 +19,36 @@ namespace ICSharpCode.AvalonEdit.Document
/// </summary>
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<TextAnchorNode> nodesToDelete = new List<TextAnchorNode>();
TextAnchorNode root;
@ -35,16 +65,15 @@ namespace ICSharpCode.AvalonEdit.Document @@ -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 @@ -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<TextAnchorNode> beforeInsert = new List<TextAnchorNode>();
@ -136,18 +175,42 @@ namespace ICSharpCode.AvalonEdit.Document @@ -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 @@ -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 @@ -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();

52
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs

@ -334,7 +334,7 @@ namespace ICSharpCode.AvalonEdit.Document @@ -334,7 +334,7 @@ namespace ICSharpCode.AvalonEdit.Document
Replace(offset, length, text, null);
}
/// <summary>
/// <summary>
/// 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 @@ -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 @@ -384,10 +405,11 @@ namespace ICSharpCode.AvalonEdit.Document
/// <param name="text">The new text.</param>
/// <param name="offsetChangeMap">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</param>
/// 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 <see cref="OffsetChangeMap.IsValidForDocumentChange"/>.
/// </param>
public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap)
{
if (text == null)
@ -417,6 +439,12 @@ namespace ICSharpCode.AvalonEdit.Document @@ -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 @@ -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);
}
}

3
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs

@ -35,6 +35,9 @@ namespace ICSharpCode.AvalonEdit.Document @@ -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

9
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs

@ -25,13 +25,20 @@ namespace ICSharpCode.AvalonEdit.Utils @@ -25,13 +25,20 @@ namespace ICSharpCode.AvalonEdit.Utils
/// public VisualLineText(string text) : base(ThrowUtil.CheckNull(text, "text").Length)
/// </code>
/// </example>
public static T CheckNull<T>(T val, string parameterName) where T : class
public static T CheckNotNull<T>(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");

2
src/Main/Base/Project/Src/Editor/ITextEditor.cs

@ -51,7 +51,7 @@ namespace ICSharpCode.SharpDevelop.Editor @@ -51,7 +51,7 @@ namespace ICSharpCode.SharpDevelop.Editor
/// Sets the selection.
/// </summary>
/// <param name="selectionStart">Start offset of the selection</param>
/// <param name="selectionLength">End offset of the selection</param>
/// <param name="selectionLength">Length of the selection</param>
void Select(int selectionStart, int selectionLength);
/// <summary>

Loading…
Cancel
Save