public void HandleTextChange(OffsetChangeMapEntry entry, DelayedEvents delayedEvents) { //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, entry.DefaultAnchorMovementIsBeforeInsertion); 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 var offset = entry.Offset; var 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; var node = FindNode(ref offset); TextAnchorNode firstDeletionSurvivor = null; // go forward through the tree and delete all nodes in the removal segment while (node != null && offset + remainingRemovalLength > node.length) { var anchor = (TextAnchor) node.Target; if (anchor != null && (anchor.SurviveDeletion || entry.RemovalNeverCausesAnchorDeletion)) { 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); node = node.Successor; } else { // delete node var s = node.Successor; remainingRemovalLength -= node.length; RemoveNode(node); // we already deleted the node, don't delete it twice nodesToDelete.Remove(node); if (anchor != null) anchor.OnDeleted(delayedEvents); 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, entry.DefaultAnchorMovementIsBeforeInsertion); } else if (node != null) { // No deletion survivors: // just perform the insertion node.length += entry.InsertionLength; } } if (node != null) { UpdateAugmentedData(node); } DeleteMarkedNodes(); }
internal void OnDeleted(DelayedEvents delayedEvents) { node = null; delayedEvents.DelayedRaise(Deleted, this, EventArgs.Empty); }
private void DoReplace(int offset, int length, ITextSource newText, OffsetChangeMap offsetChangeMap) { if (length == 0 && newText.TextLength == 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 && newText.TextLength == 1 && offsetChangeMap == null) offsetChangeMap = OffsetChangeMap.Empty; ITextSource removedText; if (length == 0) { removedText = StringTextSource.Empty; } else if (length < 100) { removedText = new StringTextSource(rope.ToString(offset, length)); } else { // use a rope if the removed string is long removedText = new RopeTextSource(rope.GetRange(offset, length)); } var args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap); // fire DocumentChanging event if (Changing != null) Changing(this, args); if (textChanging != null) textChanging(this, args); undoStack.Push(this, args); cachedText = null; // reset cache of complete document text fireTextChanged = true; var delayedEvents = new DelayedEvents(); lock (lockObject) { // create linked list of checkpoints versionProvider.AppendChange(args); // now update the textBuffer and lineTree if (offset == 0 && length == rope.Length) { // optimize replacing the whole document rope.Clear(); var newRopeTextSource = newText as RopeTextSource; if (newRopeTextSource != null) rope.InsertRange(0, newRopeTextSource.GetRope()); else rope.InsertText(0, newText.Text); lineManager.Rebuild(); } else { rope.RemoveRange(offset, length); lineManager.Remove(offset, length); #if DEBUG lineTree.CheckProperties(); #endif var newRopeTextSource = newText as RopeTextSource; if (newRopeTextSource != null) rope.InsertRange(offset, newRopeTextSource.GetRope()); else rope.InsertText(offset, newText.Text); lineManager.Insert(offset, newText); #if DEBUG lineTree.CheckProperties(); #endif } } // update text anchors if (offsetChangeMap == null) { anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents); } else { foreach (var entry in offsetChangeMap) { anchorTree.HandleTextChange(entry, delayedEvents); } } lineManager.ChangeComplete(args); // raise delayed events after our data structures are consistent again delayedEvents.RaiseEvents(); // fire DocumentChanged event if (Changed != null) Changed(this, args); if (textChanged != null) textChanged(this, args); }