/// <summary> /// Replaces text. /// Runtime: /// for updating the text buffer: m=size of new text, d=distance to last change /// usual: O(m+d) /// rare: O(m+n) /// for updating the document lines: O(m*log n), m=number of changed lines /// </summary> public void Replace(int offset, int length, string text) { if (inDocumentChanging) { throw new InvalidOperationException("Cannot change document within another document change."); } BeginUpdate(); // protect document change against corruption by other changes inside the event handlers inDocumentChanging = true; try { VerifyRange(offset, length); if (text == null) { throw new ArgumentNullException("text"); } if (length == 0 && text.Length == 0) { return; } fireTextChanged = true; DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, length, text); // fire DocumentChanging event if (Changing != null) { Changing(this, args); } DelayedEvents delayedEvents = new DelayedEvents(); // now do the real work anchorTree.RemoveText(offset, length, delayedEvents); ReplaceInternal(offset, length, text); anchorTree.InsertText(offset, text.Length); delayedEvents.RaiseEvents(); // fire DocumentChanged event if (Changed != null) { Changed(this, args); } } finally { inDocumentChanging = false; EndUpdate(); } }
public void RemoveText(int offset, int length, DelayedEvents delayedEvents) { //Log("RemoveText(" + offset + ", " + length + ")"); if (length == 0 || root == null || offset >= root.totalLength) { return; } TextAnchorNode node = FindNode(ref offset); while (node != null && offset + length > node.length) { TextAnchor anchor = (TextAnchor)node.Target; if (anchor != null && anchor.SurviveDeletion) { // shorten node length -= node.length - offset; node.length = offset; offset = 0; UpdateAugmentedData(node); node = node.Successor; } else { // delete node TextAnchorNode s = node.Successor; length -= 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; } } if (node != null) { node.length -= length; UpdateAugmentedData(node); } DeleteMarkedNodes(); }
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)); } DocumentChangeEventArgs 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; DelayedEvents 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(); if (newText is RopeTextSource newRopeTextSource) { 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 if (newText is RopeTextSource newRopeTextSource) { 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 (OffsetChangeMapEntry 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); } }
internal void OnDeleted(DelayedEvents delayedEvents) { node = null; delayedEvents.DelayedRaise(Deleted, this, EventArgs.Empty); }
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 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); 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 || 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 TextAnchorNode 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(); }
void DoReplace(int offset, int length, string newText, OffsetChangeMap offsetChangeMap) { if (length == 0 && newText.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 && newText.Length == 1 && offsetChangeMap == null) { offsetChangeMap = OffsetChangeMap.Empty; } string removedText = rope.ToString(offset, length); DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap); // fire DocumentChanging event if (Changing != null) { Changing(this, args); } cachedText = null; // reset cache of complete document text fireTextChanged = true; DelayedEvents delayedEvents = new DelayedEvents(); lock (lockObject) { // create linked list of checkpoints, if required if (currentCheckpoint != null) { currentCheckpoint = currentCheckpoint.Append(args); } // now update the textBuffer and lineTree if (offset == 0 && length == rope.Length) { // optimize replacing the whole document rope.Clear(); rope.InsertText(0, newText); lineManager.Rebuild(); } else { rope.RemoveRange(offset, length); lineManager.Remove(offset, length); #if DEBUG lineTree.CheckProperties(); #endif rope.InsertText(offset, newText); lineManager.Insert(offset, newText); #if DEBUG lineTree.CheckProperties(); #endif } } // update text anchors if (offsetChangeMap == null) { anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents); } else { foreach (OffsetChangeMapEntry entry in offsetChangeMap) { anchorTree.HandleTextChange(entry, delayedEvents); } } // raise delayed events after our data structures are consistent again delayedEvents.RaiseEvents(); // fire DocumentChanged event if (Changed != null) { Changed(this, args); } }