From fc543e2a997dc91b976d9f551cc8ade4bf1b7b6c Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 1 Jun 2009 23:25:49 +0000 Subject: [PATCH] Implemented updating text anchors on text replace operations. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4197 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../Document/TextAnchorTest.cs | 49 ++++++- .../Document/DocumentChangeEventArgs.cs | 30 ++--- .../Document/OffsetChangeMap.cs | 87 ++++++++---- .../Document/TextAnchorNode.cs | 2 +- .../Document/TextAnchorTree.cs | 126 +++++++++++++++--- .../Document/TextDocument.cs | 52 ++++++-- .../Document/TextSegmentCollection.cs | 3 + .../ICSharpCode.AvalonEdit/Utils/ThrowUtil.cs | 9 +- .../Base/Project/Src/Editor/ITextEditor.cs | 2 +- 9 files changed, 281 insertions(+), 79 deletions(-) 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); ///