The TextAnchor class references an offset (a position between two characters). It automatically updates the offset when text is inserted/removed in front of the anchor.

Use the Offset property to get the offset from a text anchor. Use the TextDocument.CreateAnchor method to create an anchor from an offset.

The document will automatically update all text anchors; and because it uses weak references to do so, the garbage collector can simply collect the anchor object when you don't need it anymore.

Moreover, the document is able to efficiently update a large number of anchors without having to look at each anchor object individually. Updating the offsets of all anchors usually only takes time logarithmic to the number of anchors. Retrieving the Offset property also runs in O(lg N).

If you want to track a segment, you can use the AnchorSegment class which implements ISegment using two text anchors.

 public TextAnchor CreateAnchor(int offset)
 {
     Log("CreateAnchor(" + offset + ")");
     TextAnchor anchor = new TextAnchor(document);
     anchor.node = new TextAnchorNode(anchor);
     if (root == null)
     {
         // creating the first text anchor
         root = anchor.node;
         root.totalLength = root.length = offset;
     }
     else if (offset >= root.totalLength)
     {
         // append anchor at end of tree
         anchor.node.totalLength = anchor.node.length = offset - root.totalLength;
         InsertAsRight(root.RightMost, anchor.node);
     }
     else
     {
         // insert anchor in middle of tree
         TextAnchorNode n = FindNode(ref offset);
         Debug.Assert(offset < n.length);
         // split segment 'n' at offset
         anchor.node.totalLength = anchor.node.length = offset;
         n.length -= offset;
         InsertBefore(n, anchor.node);
     }
     DeleteMarkedNodes();
     return anchor;
 }
		/// <summary>
		/// Called when this switch body element is inserted to the editor.
		/// </summary>
		public override void Insert(InsertionContext context)
		{
			this.context = context;
			this.context.Deactivated += new EventHandler<SnippetEventArgs>(InteractiveModeCompleted);
			this.anchor = SetUpAnchorAtInsertion(context);
			this.classFinderContext = new ClassFinder(ParserService.ParseCurrentViewContent(), Editor.Document.Text, Editor.Caret.Offset);
		}
		public void OnInsertionCompleted()
		{
			// anchors must be created in OnInsertionCompleted because they should move only
			// due to user insertions, not due to insertions of other snippet parts
			start = context.Document.CreateAnchor(startOffset);
			start.MovementType = AnchorMovementType.BeforeInsertion;
			end = context.Document.CreateAnchor(endOffset);
			end.MovementType = AnchorMovementType.AfterInsertion;
			start.Deleted += AnchorDeleted;
			end.Deleted += AnchorDeleted;
			
			// Be careful with references from the document to the editing/snippet layer - use weak events
			// to prevent memory leaks when the text area control gets dropped from the UI while the snippet is active.
			// The InsertionContext will keep us alive as long as the snippet is in interactive mode.
			TextDocumentWeakEventManager.TextChanged.AddListener(context.Document, this);
			
			background = new Renderer { Layer = KnownLayer.Background, element = this };
			foreground = new Renderer { Layer = KnownLayer.Text, element = this };
			context.TextArea.TextView.BackgroundRenderers.Add(background);
			context.TextArea.TextView.BackgroundRenderers.Add(foreground);
			context.TextArea.Caret.PositionChanged += Caret_PositionChanged;
			Caret_PositionChanged(null, null);
			
			this.Text = GetText();
		}
Example #4
0
		public void AnchorsSurviveDeletion()
		{
			document.Text = new string(' ', 10);
			TextAnchor[] a1 = new TextAnchor[11];
			TextAnchor[] a2 = new TextAnchor[11];
			for (int i = 0; i < 11; i++) {
				//Console.WriteLine("Insert first at i = " + i);
				a1[i] = document.CreateAnchor(i);
				a1[i].SurviveDeletion = true;
				//Console.WriteLine(document.GetTextAnchorTreeAsString());
				//Console.WriteLine("Insert second at i = " + i);
				a2[i] = document.CreateAnchor(i);
				a2[i].SurviveDeletion = false;
				//Console.WriteLine(document.GetTextAnchorTreeAsString());
			}
			for (int i = 0; i < 11; i++) {
				Assert.AreEqual(i, a1[i].Offset);
				Assert.AreEqual(i, a2[i].Offset);
			}
			document.Remove(1, 8);
			for (int i = 0; i < 11; i++) {
				if (i <= 1) {
					Assert.IsFalse(a1[i].IsDeleted);
					Assert.IsFalse(a2[i].IsDeleted);
					Assert.AreEqual(i, a1[i].Offset);
					Assert.AreEqual(i, a2[i].Offset);
				} else if (i <= 8) {
					Assert.IsFalse(a1[i].IsDeleted);
					Assert.IsTrue(a2[i].IsDeleted);
					Assert.AreEqual(1, a1[i].Offset);
				} else {
					Assert.IsFalse(a1[i].IsDeleted);
					Assert.IsFalse(a2[i].IsDeleted);
					Assert.AreEqual(i - 8, a1[i].Offset);
					Assert.AreEqual(i - 8, a2[i].Offset);
				}
			}
		}
			public AnchorAdapter(TextAnchor anchor)
			{
				this.anchor = anchor;
			}
Example #6
0
		internal int totalLength; // totalLength = length + left.totalLength + right.totalLength
		
		public TextAnchorNode(TextAnchor anchor) : base(anchor)
		{
		}
Example #7
0
		public Breakpoint(TextAnchor anchor)
		{
			Anchor = anchor;
			IsEnabled = true;
		}
Example #8
0
        internal int totalLength;         // totalLength = length + left.totalLength + right.totalLength

        public TextAnchorNode(TextAnchor anchor) : base(anchor)
        {
        }
        public void CreateAndMoveAnchors()
        {
            List <TextAnchor> anchors         = new List <TextAnchor>();
            List <int>        expectedOffsets = new List <int>();

            document.Text = new string(' ', 1000);
            for (int t = 0; t < 250; t++)
            {
                //Console.Write("t = " + t + " ");
                int c = rnd.Next(50);
                switch (rnd.Next(5))
                {
                case 0:
                    //Console.WriteLine("Add c=" + c + " anchors");
                    for (int i = 0; i < c; i++)
                    {
                        int        offset = rnd.Next(document.TextLength);
                        TextAnchor anchor = document.CreateAnchor(offset);
                        if (rnd.Next(2) == 0)
                        {
                            anchor.MovementType = AnchorMovementType.BeforeInsertion;
                        }
                        else
                        {
                            anchor.MovementType = AnchorMovementType.AfterInsertion;
                        }
                        anchor.SurviveDeletion = rnd.Next(2) == 0;
                        anchors.Add(anchor);
                        expectedOffsets.Add(offset);
                    }
                    break;

                case 1:
                    if (c <= anchors.Count)
                    {
                        //Console.WriteLine("Remove c=" + c + " anchors");
                        anchors.RemoveRange(0, c);
                        expectedOffsets.RemoveRange(0, c);
                        GC.Collect();
                    }
                    break;

                case 2:
                    int insertOffset = rnd.Next(document.TextLength);
                    int insertLength = rnd.Next(1000);
                    //Console.WriteLine("insertOffset=" + insertOffset + " insertLength="+insertLength);
                    document.Insert(insertOffset, new string(' ', insertLength));
                    for (int i = 0; i < anchors.Count; i++)
                    {
                        if (anchors[i].MovementType == AnchorMovementType.BeforeInsertion)
                        {
                            if (expectedOffsets[i] > insertOffset)
                            {
                                expectedOffsets[i] += insertLength;
                            }
                        }
                        else
                        {
                            if (expectedOffsets[i] >= insertOffset)
                            {
                                expectedOffsets[i] += insertLength;
                            }
                        }
                    }
                    break;

                case 3:
                    int removalOffset = rnd.Next(document.TextLength);
                    int removalLength = rnd.Next(document.TextLength - removalOffset);
                    //Console.WriteLine("RemovalOffset=" + removalOffset + " RemovalLength="+removalLength);
                    document.Remove(removalOffset, removalLength);
                    for (int i = anchors.Count - 1; i >= 0; i--)
                    {
                        if (expectedOffsets[i] > removalOffset && expectedOffsets[i] < removalOffset + removalLength)
                        {
                            if (anchors[i].SurviveDeletion)
                            {
                                expectedOffsets[i] = removalOffset;
                            }
                            else
                            {
                                Assert.IsTrue(anchors[i].IsDeleted);
                                anchors.RemoveAt(i);
                                expectedOffsets.RemoveAt(i);
                            }
                        }
                        else if (expectedOffsets[i] > removalOffset)
                        {
                            expectedOffsets[i] -= removalLength;
                        }
                    }
                    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++)
                {
                    Assert.AreEqual(expectedOffsets[j], anchors[j].Offset);
                }
            }
            GC.KeepAlive(anchors);
        }
Example #10
0
        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();
        }