Free cookie consent management tool by TermsFeed Policy Generator

source: branches/PersistenceOverhaul/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Document/TextAnchorTree.cs @ 13325

Last change on this file since 13325 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 24.6 KB
Line 
1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Collections.Generic;
21using System.Diagnostics;
22using System.Text;
23using ICSharpCode.AvalonEdit.Utils;
24#if NREFACTORY
25using ICSharpCode.NRefactory.Editor;
26#endif
27
28namespace ICSharpCode.AvalonEdit.Document
29{
30  /// <summary>
31  /// A tree of TextAnchorNodes.
32  /// </summary>
33  sealed class TextAnchorTree
34  {
35    // The text anchor tree has difficult requirements:
36    // - it must QUICKLY update the offset in all anchors whenever there is a document change
37    // - it must not reference text anchors directly, using weak references instead
38   
39    // Clearly, we cannot afford updating an Offset property on all anchors (that would be O(N)).
40    // So instead, the anchors need to be able to calculate their offset from a data structure
41    // that can be efficiently updated.
42   
43    // This implementation is built using an augmented red-black-tree.
44    // There is a 'TextAnchorNode' for each text anchor.
45    // Such a node represents a section of text (just the length is stored) with a (weakly referenced) text anchor at the end.
46   
47    // Basically, you can imagine the list of text anchors as a sorted list of text anchors, where each anchor
48    // just stores the distance to the previous anchor.
49    // (next node = TextAnchorNode.Successor, distance = TextAnchorNode.length)
50    // Distances are never negative, so this representation means anchors are always sorted by offset
51    // (the order of anchors at the same offset is undefined)
52   
53    // Of course, a linked list of anchors would be way too slow (one would need to traverse the whole list
54    // every time the offset of an anchor is being looked up).
55    // Instead, we use a red-black-tree. We aren't actually using the tree for sorting - it's just a binary tree
56    // as storage format for what's conceptually a list, the red-black properties are used to keep the tree balanced.
57    // Other balanced binary trees would work, too.
58   
59    // What makes the tree-form efficient is that is augments the data by a 'totalLength'. Where 'length'
60    // represents the distance to the previous node, 'totalLength' is the sum of all 'length' values in the subtree
61    // under that node.
62    // This allows computing the Offset from an anchor by walking up the list of parent nodes instead of going
63    // through all predecessor nodes. So computing the Offset runs in O(log N).
64   
65    readonly TextDocument document;
66    readonly List<TextAnchorNode> nodesToDelete = new List<TextAnchorNode>();
67    TextAnchorNode root;
68   
69    public TextAnchorTree(TextDocument document)
70    {
71      this.document = document;
72    }
73   
74    [Conditional("DEBUG")]
75    static void Log(string text)
76    {
77      Debug.WriteLine("TextAnchorTree: " + text);
78    }
79   
80    #region Insert Text
81    void InsertText(int offset, int length, bool defaultAnchorMovementIsBeforeInsertion)
82    {
83      if (length == 0 || root == null || offset > root.totalLength)
84        return;
85     
86      // find the range of nodes that are placed exactly at offset
87      // beginNode is inclusive, endNode is exclusive
88      if (offset == root.totalLength) {
89        PerformInsertText(FindActualBeginNode(root.RightMost), null, length, defaultAnchorMovementIsBeforeInsertion);
90      } else {
91        TextAnchorNode endNode = FindNode(ref offset);
92        Debug.Assert(endNode.length > 0);
93       
94        if (offset > 0) {
95          // there are no nodes exactly at offset
96          endNode.length += length;
97          UpdateAugmentedData(endNode);
98        } else {
99          PerformInsertText(FindActualBeginNode(endNode.Predecessor), endNode, length, defaultAnchorMovementIsBeforeInsertion);
100        }
101      }
102      DeleteMarkedNodes();
103    }
104   
105    TextAnchorNode FindActualBeginNode(TextAnchorNode node)
106    {
107      // now find the actual beginNode
108      while (node != null && node.length == 0)
109        node = node.Predecessor;
110      if (node == null) {
111        // no predecessor = beginNode is first node in tree
112        node = root.LeftMost;
113      }
114      return node;
115    }
116   
117    // Sorts the nodes in the range [beginNode, endNode) by MovementType
118    // and inserts the length between the BeforeInsertion and the AfterInsertion nodes.
119    void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length, bool defaultAnchorMovementIsBeforeInsertion)
120    {
121      Debug.Assert(beginNode != null);
122      // endNode may be null at the end of the anchor tree
123     
124      // now we need to sort the nodes in the range [beginNode, endNode); putting those with
125      // MovementType.BeforeInsertion in front of those with MovementType.AfterInsertion
126      List<TextAnchorNode> beforeInsert = new List<TextAnchorNode>();
127      //List<TextAnchorNode> afterInsert = new List<TextAnchorNode>();
128      TextAnchorNode temp = beginNode;
129      while (temp != endNode) {
130        TextAnchor anchor = (TextAnchor)temp.Target;
131        if (anchor == null) {
132          // afterInsert.Add(temp);
133          MarkNodeForDelete(temp);
134        } else if (defaultAnchorMovementIsBeforeInsertion
135                   ? anchor.MovementType != AnchorMovementType.AfterInsertion
136                   : anchor.MovementType == AnchorMovementType.BeforeInsertion)
137        {
138          beforeInsert.Add(temp);
139//        } else {
140//          afterInsert.Add(temp);
141        }
142        temp = temp.Successor;
143      }
144      // now again go through the range and swap the nodes with those in the beforeInsert list
145      temp = beginNode;
146      foreach (TextAnchorNode node in beforeInsert) {
147        SwapAnchors(node, temp);
148        temp = temp.Successor;
149      }
150      // now temp is pointing to the first node that is afterInsert,
151      // or to endNode, if there is no afterInsert node at the offset
152      // So add the length to temp
153      if (temp == null) {
154        // temp might be null if endNode==null and no afterInserts
155        Debug.Assert(endNode == null);
156      } else {
157        temp.length += length;
158        UpdateAugmentedData(temp);
159      }
160    }
161   
162    /// <summary>
163    /// Swaps the anchors stored in the two nodes.
164    /// </summary>
165    void SwapAnchors(TextAnchorNode n1, TextAnchorNode n2)
166    {
167      if (n1 != n2) {
168        TextAnchor anchor1 = (TextAnchor)n1.Target;
169        TextAnchor anchor2 = (TextAnchor)n2.Target;
170        if (anchor1 == null && anchor2 == null) {
171          // -> no swap required
172          return;
173        }
174        n1.Target = anchor2;
175        n2.Target = anchor1;
176        if (anchor1 == null) {
177          // unmark n1 from deletion, mark n2 for deletion
178          nodesToDelete.Remove(n1);
179          MarkNodeForDelete(n2);
180          anchor2.node = n1;
181        } else if (anchor2 == null) {
182          // unmark n2 from deletion, mark n1 for deletion
183          nodesToDelete.Remove(n2);
184          MarkNodeForDelete(n1);
185          anchor1.node = n2;
186        } else {
187          anchor1.node = n2;
188          anchor2.node = n1;
189        }
190      }
191    }
192    #endregion
193   
194    #region Remove or Replace text
195    public void HandleTextChange(OffsetChangeMapEntry entry, DelayedEvents delayedEvents)
196    {
197      //Log("HandleTextChange(" + entry + ")");
198      if (entry.RemovalLength == 0) {
199        // This is a pure insertion.
200        // Unlike a replace with removal, a pure insertion can result in nodes at the same location
201        // to split depending on their MovementType.
202        // Thus, we handle this case on a separate code path
203        // (the code below looks like it does something similar, but it can only split
204        // the set of deletion survivors, not all nodes at an offset)
205        InsertText(entry.Offset, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion);
206        return;
207      }
208      // When handling a replacing text change, we need to:
209      // - find all anchors in the deleted segment and delete them / move them to the appropriate
210      //   surviving side.
211      // - adjust the segment size between the left and right side
212     
213      int offset = entry.Offset;
214      int remainingRemovalLength = entry.RemovalLength;
215      // if the text change is happening after the last anchor, we don't have to do anything
216      if (root == null || offset >= root.totalLength)
217        return;
218      TextAnchorNode node = FindNode(ref offset);
219      TextAnchorNode firstDeletionSurvivor = null;
220      // go forward through the tree and delete all nodes in the removal segment
221      while (node != null && offset + remainingRemovalLength > node.length) {
222        TextAnchor anchor = (TextAnchor)node.Target;
223        if (anchor != null && (anchor.SurviveDeletion || entry.RemovalNeverCausesAnchorDeletion)) {
224          if (firstDeletionSurvivor == null)
225            firstDeletionSurvivor = node;
226          // This node should be deleted, but it wants to survive.
227          // We'll just remove the deleted length segment, so the node will be positioned
228          // in front of the removed segment.
229          remainingRemovalLength -= node.length - offset;
230          node.length = offset;
231          offset = 0;
232          UpdateAugmentedData(node);
233          node = node.Successor;
234        } else {
235          // delete node
236          TextAnchorNode s = node.Successor;
237          remainingRemovalLength -= node.length;
238          RemoveNode(node);
239          // we already deleted the node, don't delete it twice
240          nodesToDelete.Remove(node);
241          if (anchor != null)
242            anchor.OnDeleted(delayedEvents);
243          node = s;
244        }
245      }
246      // 'node' now is the first anchor after the deleted segment.
247      // If there are no anchors after the deleted segment, 'node' is null.
248     
249      // firstDeletionSurvivor was set to the first node surviving deletion.
250      // Because all non-surviving nodes up to 'node' were deleted, the node range
251      // [firstDeletionSurvivor, node) now refers to the set of all deletion survivors.
252     
253      // do the remaining job of the removal
254      if (node != null) {
255        node.length -= remainingRemovalLength;
256        Debug.Assert(node.length >= 0);
257      }
258      if (entry.InsertionLength > 0) {
259        // we are performing a replacement
260        if (firstDeletionSurvivor != null) {
261          // We got deletion survivors which need to be split into BeforeInsertion
262          // and AfterInsertion groups.
263          // Take care that we don't regroup everything at offset, but only the deletion
264          // survivors - from firstDeletionSurvivor (inclusive) to node (exclusive).
265          // This ensures that nodes immediately before or after the replaced segment
266          // stay where they are (independent from their MovementType)
267          PerformInsertText(firstDeletionSurvivor, node, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion);
268        } else if (node != null) {
269          // No deletion survivors:
270          // just perform the insertion
271          node.length += entry.InsertionLength;
272        }
273      }
274      if (node != null) {
275        UpdateAugmentedData(node);
276      }
277      DeleteMarkedNodes();
278    }
279    #endregion
280   
281    #region Node removal when TextAnchor was GC'ed
282    void MarkNodeForDelete(TextAnchorNode node)
283    {
284      if (!nodesToDelete.Contains(node))
285        nodesToDelete.Add(node);
286    }
287   
288    void DeleteMarkedNodes()
289    {
290      CheckProperties();
291      while (nodesToDelete.Count > 0) {
292        int pos = nodesToDelete.Count - 1;
293        TextAnchorNode n = nodesToDelete[pos];
294        // combine section of n with the following section
295        TextAnchorNode s = n.Successor;
296        if (s != null) {
297          s.length += n.length;
298        }
299        RemoveNode(n);
300        if (s != null) {
301          UpdateAugmentedData(s);
302        }
303        nodesToDelete.RemoveAt(pos);
304        CheckProperties();
305      }
306      CheckProperties();
307    }
308    #endregion
309   
310    #region FindNode
311    /// <summary>
312    /// Finds the node at the specified offset.
313    /// After the method has run, offset is relative to the beginning of the returned node.
314    /// </summary>
315    TextAnchorNode FindNode(ref int offset)
316    {
317      TextAnchorNode n = root;
318      while (true) {
319        if (n.left != null) {
320          if (offset < n.left.totalLength) {
321            n = n.left; // descend into left subtree
322            continue;
323          } else {
324            offset -= n.left.totalLength; // skip left subtree
325          }
326        }
327        if (!n.IsAlive)
328          MarkNodeForDelete(n);
329        if (offset < n.length) {
330          return n; // found correct node
331        } else {
332          offset -= n.length; // skip this node
333        }
334        if (n.right != null) {
335          n = n.right; // descend into right subtree
336        } else {
337          // didn't find any node containing the offset
338          return null;
339        }
340      }
341    }
342    #endregion
343   
344    #region UpdateAugmentedData
345    void UpdateAugmentedData(TextAnchorNode n)
346    {
347      if (!n.IsAlive)
348        MarkNodeForDelete(n);
349     
350      int totalLength = n.length;
351      if (n.left != null)
352        totalLength += n.left.totalLength;
353      if (n.right != null)
354        totalLength += n.right.totalLength;
355      if (n.totalLength != totalLength) {
356        n.totalLength = totalLength;
357        if (n.parent != null)
358          UpdateAugmentedData(n.parent);
359      }
360    }
361    #endregion
362   
363    #region CreateAnchor
364    public TextAnchor CreateAnchor(int offset)
365    {
366      Log("CreateAnchor(" + offset + ")");
367      TextAnchor anchor = new TextAnchor(document);
368      anchor.node = new TextAnchorNode(anchor);
369      if (root == null) {
370        // creating the first text anchor
371        root = anchor.node;
372        root.totalLength = root.length = offset;
373      } else if (offset >= root.totalLength) {
374        // append anchor at end of tree
375        anchor.node.totalLength = anchor.node.length = offset - root.totalLength;
376        InsertAsRight(root.RightMost, anchor.node);
377      } else {
378        // insert anchor in middle of tree
379        TextAnchorNode n = FindNode(ref offset);
380        Debug.Assert(offset < n.length);
381        // split segment 'n' at offset
382        anchor.node.totalLength = anchor.node.length = offset;
383        n.length -= offset;
384        InsertBefore(n, anchor.node);
385      }
386      DeleteMarkedNodes();
387      return anchor;
388    }
389   
390    void InsertBefore(TextAnchorNode node, TextAnchorNode newNode)
391    {
392      if (node.left == null) {
393        InsertAsLeft(node, newNode);
394      } else {
395        InsertAsRight(node.left.RightMost, newNode);
396      }
397    }
398    #endregion
399   
400    #region Red/Black Tree
401    internal const bool RED = true;
402    internal const bool BLACK = false;
403   
404    void InsertAsLeft(TextAnchorNode parentNode, TextAnchorNode newNode)
405    {
406      Debug.Assert(parentNode.left == null);
407      parentNode.left = newNode;
408      newNode.parent = parentNode;
409      newNode.color = RED;
410      UpdateAugmentedData(parentNode);
411      FixTreeOnInsert(newNode);
412    }
413   
414    void InsertAsRight(TextAnchorNode parentNode, TextAnchorNode newNode)
415    {
416      Debug.Assert(parentNode.right == null);
417      parentNode.right = newNode;
418      newNode.parent = parentNode;
419      newNode.color = RED;
420      UpdateAugmentedData(parentNode);
421      FixTreeOnInsert(newNode);
422    }
423   
424    void FixTreeOnInsert(TextAnchorNode node)
425    {
426      Debug.Assert(node != null);
427      Debug.Assert(node.color == RED);
428      Debug.Assert(node.left == null || node.left.color == BLACK);
429      Debug.Assert(node.right == null || node.right.color == BLACK);
430     
431      TextAnchorNode parentNode = node.parent;
432      if (parentNode == null) {
433        // we inserted in the root -> the node must be black
434        // since this is a root node, making the node black increments the number of black nodes
435        // on all paths by one, so it is still the same for all paths.
436        node.color = BLACK;
437        return;
438      }
439      if (parentNode.color == BLACK) {
440        // if the parent node where we inserted was black, our red node is placed correctly.
441        // since we inserted a red node, the number of black nodes on each path is unchanged
442        // -> the tree is still balanced
443        return;
444      }
445      // parentNode is red, so there is a conflict here!
446     
447      // because the root is black, parentNode is not the root -> there is a grandparent node
448      TextAnchorNode grandparentNode = parentNode.parent;
449      TextAnchorNode uncleNode = Sibling(parentNode);
450      if (uncleNode != null && uncleNode.color == RED) {
451        parentNode.color = BLACK;
452        uncleNode.color = BLACK;
453        grandparentNode.color = RED;
454        FixTreeOnInsert(grandparentNode);
455        return;
456      }
457      // now we know: parent is red but uncle is black
458      // First rotation:
459      if (node == parentNode.right && parentNode == grandparentNode.left) {
460        RotateLeft(parentNode);
461        node = node.left;
462      } else if (node == parentNode.left && parentNode == grandparentNode.right) {
463        RotateRight(parentNode);
464        node = node.right;
465      }
466      // because node might have changed, reassign variables:
467      parentNode = node.parent;
468      grandparentNode = parentNode.parent;
469     
470      // Now recolor a bit:
471      parentNode.color = BLACK;
472      grandparentNode.color = RED;
473      // Second rotation:
474      if (node == parentNode.left && parentNode == grandparentNode.left) {
475        RotateRight(grandparentNode);
476      } else {
477        // because of the first rotation, this is guaranteed:
478        Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
479        RotateLeft(grandparentNode);
480      }
481    }
482   
483    void RemoveNode(TextAnchorNode removedNode)
484    {
485      if (removedNode.left != null && removedNode.right != null) {
486        // replace removedNode with it's in-order successor
487       
488        TextAnchorNode leftMost = removedNode.right.LeftMost;
489        RemoveNode(leftMost); // remove leftMost from its current location
490       
491        // and overwrite the removedNode with it
492        ReplaceNode(removedNode, leftMost);
493        leftMost.left = removedNode.left;
494        if (leftMost.left != null) leftMost.left.parent = leftMost;
495        leftMost.right = removedNode.right;
496        if (leftMost.right != null) leftMost.right.parent = leftMost;
497        leftMost.color = removedNode.color;
498       
499        UpdateAugmentedData(leftMost);
500        if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent);
501        return;
502      }
503     
504      // now either removedNode.left or removedNode.right is null
505      // get the remaining child
506      TextAnchorNode parentNode = removedNode.parent;
507      TextAnchorNode childNode = removedNode.left ?? removedNode.right;
508      ReplaceNode(removedNode, childNode);
509      if (parentNode != null) UpdateAugmentedData(parentNode);
510      if (removedNode.color == BLACK) {
511        if (childNode != null && childNode.color == RED) {
512          childNode.color = BLACK;
513        } else {
514          FixTreeOnDelete(childNode, parentNode);
515        }
516      }
517    }
518   
519    void FixTreeOnDelete(TextAnchorNode node, TextAnchorNode parentNode)
520    {
521      Debug.Assert(node == null || node.parent == parentNode);
522      if (parentNode == null)
523        return;
524     
525      // warning: node may be null
526      TextAnchorNode sibling = Sibling(node, parentNode);
527      if (sibling.color == RED) {
528        parentNode.color = RED;
529        sibling.color = BLACK;
530        if (node == parentNode.left) {
531          RotateLeft(parentNode);
532        } else {
533          RotateRight(parentNode);
534        }
535       
536        sibling = Sibling(node, parentNode); // update value of sibling after rotation
537      }
538     
539      if (parentNode.color == BLACK
540          && sibling.color == BLACK
541          && GetColor(sibling.left) == BLACK
542          && GetColor(sibling.right) == BLACK)
543      {
544        sibling.color = RED;
545        FixTreeOnDelete(parentNode, parentNode.parent);
546        return;
547      }
548     
549      if (parentNode.color == RED
550          && sibling.color == BLACK
551          && GetColor(sibling.left) == BLACK
552          && GetColor(sibling.right) == BLACK)
553      {
554        sibling.color = RED;
555        parentNode.color = BLACK;
556        return;
557      }
558     
559      if (node == parentNode.left &&
560          sibling.color == BLACK &&
561          GetColor(sibling.left) == RED &&
562          GetColor(sibling.right) == BLACK)
563      {
564        sibling.color = RED;
565        sibling.left.color = BLACK;
566        RotateRight(sibling);
567      }
568      else if (node == parentNode.right &&
569               sibling.color == BLACK &&
570               GetColor(sibling.right) == RED &&
571               GetColor(sibling.left) == BLACK)
572      {
573        sibling.color = RED;
574        sibling.right.color = BLACK;
575        RotateLeft(sibling);
576      }
577      sibling = Sibling(node, parentNode); // update value of sibling after rotation
578     
579      sibling.color = parentNode.color;
580      parentNode.color = BLACK;
581      if (node == parentNode.left) {
582        if (sibling.right != null) {
583          Debug.Assert(sibling.right.color == RED);
584          sibling.right.color = BLACK;
585        }
586        RotateLeft(parentNode);
587      } else {
588        if (sibling.left != null) {
589          Debug.Assert(sibling.left.color == RED);
590          sibling.left.color = BLACK;
591        }
592        RotateRight(parentNode);
593      }
594    }
595   
596    void ReplaceNode(TextAnchorNode replacedNode, TextAnchorNode newNode)
597    {
598      if (replacedNode.parent == null) {
599        Debug.Assert(replacedNode == root);
600        root = newNode;
601      } else {
602        if (replacedNode.parent.left == replacedNode)
603          replacedNode.parent.left = newNode;
604        else
605          replacedNode.parent.right = newNode;
606      }
607      if (newNode != null) {
608        newNode.parent = replacedNode.parent;
609      }
610      replacedNode.parent = null;
611    }
612   
613    void RotateLeft(TextAnchorNode p)
614    {
615      // let q be p's right child
616      TextAnchorNode q = p.right;
617      Debug.Assert(q != null);
618      Debug.Assert(q.parent == p);
619      // set q to be the new root
620      ReplaceNode(p, q);
621     
622      // set p's right child to be q's left child
623      p.right = q.left;
624      if (p.right != null) p.right.parent = p;
625      // set q's left child to be p
626      q.left = p;
627      p.parent = q;
628      UpdateAugmentedData(p);
629      UpdateAugmentedData(q);
630    }
631   
632    void RotateRight(TextAnchorNode p)
633    {
634      // let q be p's left child
635      TextAnchorNode q = p.left;
636      Debug.Assert(q != null);
637      Debug.Assert(q.parent == p);
638      // set q to be the new root
639      ReplaceNode(p, q);
640     
641      // set p's left child to be q's right child
642      p.left = q.right;
643      if (p.left != null) p.left.parent = p;
644      // set q's right child to be p
645      q.right = p;
646      p.parent = q;
647      UpdateAugmentedData(p);
648      UpdateAugmentedData(q);
649    }
650   
651    static TextAnchorNode Sibling(TextAnchorNode node)
652    {
653      if (node == node.parent.left)
654        return node.parent.right;
655      else
656        return node.parent.left;
657    }
658   
659    static TextAnchorNode Sibling(TextAnchorNode node, TextAnchorNode parentNode)
660    {
661      Debug.Assert(node == null || node.parent == parentNode);
662      if (node == parentNode.left)
663        return parentNode.right;
664      else
665        return parentNode.left;
666    }
667   
668    static bool GetColor(TextAnchorNode node)
669    {
670      return node != null ? node.color : BLACK;
671    }
672    #endregion
673   
674    #region CheckProperties
675    [Conditional("DATACONSISTENCYTEST")]
676    internal void CheckProperties()
677    {
678      #if DEBUG
679      if (root != null) {
680        CheckProperties(root);
681       
682        // check red-black property:
683        int blackCount = -1;
684        CheckNodeProperties(root, null, RED, 0, ref blackCount);
685      }
686      #endif
687    }
688   
689    #if DEBUG
690    void CheckProperties(TextAnchorNode node)
691    {
692      int totalLength = node.length;
693      if (node.left != null) {
694        CheckProperties(node.left);
695        totalLength += node.left.totalLength;
696      }
697      if (node.right != null) {
698        CheckProperties(node.right);
699        totalLength += node.right.totalLength;
700      }
701      Debug.Assert(node.totalLength == totalLength);
702    }
703   
704    /*
705    1. A node is either red or black.
706    2. The root is black.
707    3. All leaves are black. (The leaves are the NIL children.)
708    4. Both children of every red node are black. (So every red node must have a black parent.)
709    5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
710     */
711    void CheckNodeProperties(TextAnchorNode node, TextAnchorNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
712    {
713      if (node == null) return;
714     
715      Debug.Assert(node.parent == parentNode);
716     
717      if (parentColor == RED) {
718        Debug.Assert(node.color == BLACK);
719      }
720      if (node.color == BLACK) {
721        blackCount++;
722      }
723      if (node.left == null && node.right == null) {
724        // node is a leaf node:
725        if (expectedBlackCount == -1)
726          expectedBlackCount = blackCount;
727        else
728          Debug.Assert(expectedBlackCount == blackCount);
729      }
730      CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
731      CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
732    }
733    #endif
734    #endregion
735   
736    #region GetTreeAsString
737    #if DEBUG
738    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
739    public string GetTreeAsString()
740    {
741      if (root == null)
742        return "<empty tree>";
743      StringBuilder b = new StringBuilder();
744      AppendTreeToString(root, b, 0);
745      return b.ToString();
746    }
747   
748    static void AppendTreeToString(TextAnchorNode node, StringBuilder b, int indent)
749    {
750      if (node.color == RED)
751        b.Append("RED   ");
752      else
753        b.Append("BLACK ");
754      b.AppendLine(node.ToString());
755      indent += 2;
756      if (node.left != null) {
757        b.Append(' ', indent);
758        b.Append("L: ");
759        AppendTreeToString(node.left, b, indent);
760      }
761      if (node.right != null) {
762        b.Append(' ', indent);
763        b.Append("R: ");
764        AppendTreeToString(node.right, b, indent);
765      }
766    }
767    #endif
768    #endregion
769  }
770}
Note: See TracBrowser for help on using the repository browser.