aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs
blob: a95d07fcf74c780c2d73a4abd56bfeb1886b723a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
' href='#n205'>205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Globalization;
using System.Linq.Expressions;
using System.Threading;
using Tango.Scripting.Editors.Utils;

namespace Tango.Scripting.Editors.Document
{
    /// <summary>
    /// This class is the main class of the text model. Basically, it is a <see cref="System.Text.StringBuilder"/> with events.
    /// </summary>
    /// <remarks>
    /// <b>Thread safety:</b>
    /// <inheritdoc cref="VerifyAccess"/>
    /// <para>However, there is a single method that is thread-safe: <see cref="CreateSnapshot()"/> (and its overloads).</para>
    /// </remarks>
    public sealed class TextDocument : ITextSource, INotifyPropertyChanged
    {
        #region Thread ownership
        readonly object lockObject = new object();
        Thread owner = Thread.CurrentThread;

        /// <summary>
        /// Verifies that the current thread is the documents owner thread.
        /// Throws an <see cref="InvalidOperationException"/> if the wrong thread accesses the TextDocument.
        /// </summary>
        /// <remarks>
        /// <para>The TextDocument class is not thread-safe. A document instance expects to have a single owner thread
        /// and will throw an <see cref="InvalidOperationException"/> when accessed from another thread.
        /// It is possible to change the owner thread using the <see cref="SetOwnerThread"/> method.</para>
        /// </remarks>
        public void VerifyAccess()
        {
            if (Thread.CurrentThread != owner)
                throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it.");
        }

        /// <summary>
        /// Transfers ownership of the document to another thread. This method can be used to load
        /// a file into a TextDocument on a background thread and then transfer ownership to the UI thread
        /// for displaying the document.
        /// </summary>
        /// <remarks>
        /// <inheritdoc cref="VerifyAccess"/>
        /// <para>
        /// The owner can be set to null, which means that no thread can access the document. But, if the document
        /// has no owner thread, any thread may take ownership by calling <see cref="SetOwnerThread"/>.
        /// </para>
        /// </remarks>
        public void SetOwnerThread(Thread newOwner)
        {
            // We need to lock here to ensure that in the null owner case,
            // only one thread succeeds in taking ownership.
            lock (lockObject)
            {
                if (owner != null)
                {
                    VerifyAccess();
                }
                owner = newOwner;
            }
        }
        #endregion

        #region Fields + Constructor
        readonly Rope<char> rope;
        readonly DocumentLineTree lineTree;
        readonly LineManager lineManager;
        readonly TextAnchorTree anchorTree;
        ChangeTrackingCheckpoint currentCheckpoint;

        /// <summary>
        /// Create an empty text document.
        /// </summary>
        public TextDocument()
            : this(string.Empty)
        {
        }

        /// <summary>
        /// Create a new text document with the specified initial text.
        /// </summary>
        public TextDocument(IEnumerable<char> initialText)
        {
            if (initialText == null)
                throw new ArgumentNullException("initialText");
            rope = new Rope<char>(initialText);
            lineTree = new DocumentLineTree(this);
            lineManager = new LineManager(lineTree, this);
            lineTrackers.CollectionChanged += delegate
            {
                lineManager.UpdateListOfLineTrackers();
            };

            anchorTree = new TextAnchorTree(this);
            undoStack = new UndoStack();
            FireChangeEvents();
        }

        /// <summary>
        /// Create a new text document with the specified initial text.
        /// </summary>
        public TextDocument(ITextSource initialText)
            : this(GetTextFromTextSource(initialText))
        {
        }

        // gets the text from a text source, directly retrieving the underlying rope where possible
        static IEnumerable<char> GetTextFromTextSource(ITextSource textSource)
        {
            if (textSource == null)
                throw new ArgumentNullException("textSource");

            RopeTextSource rts = textSource as RopeTextSource;
            if (rts != null)
                return rts.GetRope();

            TextDocument doc = textSource as TextDocument;
            if (doc != null)
                return doc.rope;

            return textSource.Text;
        }
        #endregion

        #region Text
        void ThrowIfRangeInvalid(int offset, int length)
        {
            if (offset < 0 || offset > rope.Length)
            {
                throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
            }
            if (length < 0 || offset + length > rope.Length)
            {
                throw new ArgumentOutOfRangeException("length", length, "0 <= length, offset(" + offset + ")+length <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
            }
        }

        /// <inheritdoc/>
        public string GetText(int offset, int length)
        {
            VerifyAccess();
            return rope.ToString(Math.Max(offset, 0), length);
        }

        /// <summary>
        /// Retrieves the text for a portion of the document.
        /// </summary>
        public string GetText(ISegment segment)
        {
            if (segment == null)
                throw new ArgumentNullException("segment");
            return GetText(segment.Offset, segment.Length);
        }

        int ITextSource.IndexOfAny(char[] anyOf, int startIndex, int count)
        {
            DebugVerifyAccess(); // frequently called (NewLineFinder), so must be fast in release builds
            return rope.IndexOfAny(anyOf, startIndex, count);
        }

        /// <inheritdoc/>
        public char GetCharAt(int offset)
        {
            DebugVerifyAccess(); // frequently called, so must be fast in release builds
            return rope[offset];
        }

        WeakReference cachedText;

        /// <summary>
        /// Gets/Sets the text of the whole document.
        /// </summary>
        public string Text
        {
            get
            {
                VerifyAccess();
                string completeText = cachedText != null ? (cachedText.Target as string) : null;
                if (completeText == null)
                {
                    completeText = rope.ToString();
                    cachedText = new WeakReference(completeText);
                }
                return completeText;
            }
            set
            {
                VerifyAccess();
                if (value == null)
                    throw new ArgumentNullException("value");
                Replace(0, rope.Length, value);
            }
        }

        /// <inheritdoc/>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public event EventHandler TextChanged;

        /// <inheritdoc/>
        public int TextLength
        {
            get
            {
                VerifyAccess();
                return rope.Length;
            }
        }

        /// <summary>
        /// Is raised when the TextLength property changes.
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")]
        public event EventHandler TextLengthChanged;

        /// <summary>
        /// Is raised when one of the properties <see cref="Text"/>, <see cref="TextLength"/>, <see cref="LineCount"/>,
        /// <see cref="UndoStack"/> changes.
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Is raised before the document changes.
        /// </summary>
        /// <remarks>
        /// <para>Here is the order in which events are raised during a document update:</para>
        /// <list type="bullet">
        /// <item><description><b><see cref="BeginUpdate">BeginUpdate()</see></b></description>
        ///   <list type="bullet">
        ///   <item><description>Start of change group (on undo stack)</description></item>
        ///   <item><description><see cref="UpdateStarted"/> event is raised</description></item>
        ///   </list></item>
        /// <item><description><b><see cref="Insert(int,string)">Insert()</see> / <see cref="Remove(int,int)">Remove()</see> / <see cref="Replace(int,int,string)">Replace()</see></b></description>
        ///   <list type="bullet">
        ///   <item><description><see cref="Changing"/> event is raised</description></item>
        ///   <item><description>The document is changed</description></item>
        ///   <item><description><see cref="TextAnchor.Deleted">TextAnchor.Deleted</see> event is raised if anchors were
        ///     in the deleted text portion</description></item>
        ///   <item><description><see cref="Changed"/> event is raised</description></item>
        ///   </list></item>
        /// <item><description><b><see cref="EndUpdate">EndUpdate()</see></b></description>
        ///   <list type="bullet">
        ///   <item><description><see cref="TextChanged"/> event is raised</description></item>
        ///   <item><description><see cref="PropertyChanged"/> event is raised (for the Text, TextLength, LineCount properties, in that order)</description></item>
        ///   <item><description>End of change group (on undo stack)</description></item>
        ///   <item><description><see cref="UpdateFinished"/> event is raised</description></item>
        ///   </list></item>
        /// </list>
        /// <para>
        /// If the insert/remove/replace methods are called without a call to <c>BeginUpdate()</c>,
        /// they will call <c>BeginUpdate()</c> and <c>EndUpdate()</c> to ensure no change happens outside of <c>UpdateStarted</c>/<c>UpdateFinished</c>.
        /// </para><para>
        /// There can be multiple document changes between the <c>BeginUpdate()</c> and <c>EndUpdate()</c> calls.
        /// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done.
        /// </para><para>
        /// The <see cref="UndoStack"/> listens to the <c>UpdateStarted</c> and <c>UpdateFinished</c> events to group all changes into a single undo step.
        /// </para>
        /// </remarks>
        public event EventHandler<DocumentChangeEventArgs> Changing;

        /// <summary>
        /// Is raised after the document has changed.
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public event EventHandler<DocumentChangeEventArgs> Changed;

        /// <summary>
        /// Creates a snapshot of the current text.
        /// </summary>
        /// <remarks>
        /// <para>This method returns an immutable snapshot of the document, and may be safely called even when
        /// the document's owner thread is concurrently modifying the document.
        /// </para><para>
        /// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other
        /// classes implementing ITextSource.CreateSnapshot().
        /// </para><para>
        /// </para>
        /// </remarks>
        public ITextSource CreateSnapshot()
        {
            lock (lockObject)
            {
                return new RopeTextSource(rope.Clone());
            }
        }

        /// <summary>
        /// Creates a snapshot of the current text.
        /// Additionally, creates a checkpoint that allows tracking document changes.
        /// </summary>
        /// <remarks><inheritdoc cref="CreateSnapshot()"/><inheritdoc cref="ChangeTrackingCheckpoint"/></remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "Need to return snapshot and checkpoint together to ensure thread-safety")]
        public ITextSource CreateSnapshot(out ChangeTrackingCheckpoint checkpoint)
        {
            lock (lockObject)
            {
                if (currentCheckpoint == null)
                    currentCheckpoint = new ChangeTrackingCheckpoint(lockObject);
                checkpoint = currentCheckpoint;
                return new RopeTextSource(rope.Clone());
            }
        }

        internal ChangeTrackingCheckpoint CreateChangeTrackingCheckpoint()
        {
            lock (lockObject)
            {
                if (currentCheckpoint == null)
                    currentCheckpoint = new ChangeTrackingCheckpoint(lockObject);
                return currentCheckpoint;
            }
        }

        /// <summary>
        /// Creates a snapshot of a part of the current text.
        /// </summary>
        /// <remarks><inheritdoc cref="CreateSnapshot()"/></remarks>
        public ITextSource CreateSnapshot(int offset, int length)
        {
            lock (lockObject)
            {
                return new RopeTextSource(rope.GetRange(offset, length));
            }
        }

        /// <inheritdoc/>
        public System.IO.TextReader CreateReader()
        {
            lock (lockObject)
            {
                return new RopeTextReader(rope);
            }
        }
        #endregion

        #region BeginUpdate / EndUpdate
        int beginUpdateCount;

        /// <summary>
        /// Gets if an update is running.
        /// </summary>
        /// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
        public bool IsInUpdate
        {
            get
            {
                VerifyAccess();
                return beginUpdateCount > 0;
            }
        }

        /// <summary>
        /// Immediately calls <see cref="BeginUpdate()"/>,
        /// and returns an IDisposable that calls <see cref="EndUpdate()"/>.
        /// </summary>
        /// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
        public IDisposable RunUpdate()
        {
            BeginUpdate();
            return new CallbackOnDispose(EndUpdate);
        }

        /// <summary>
        /// <para>Begins a group of document changes.</para>
        /// <para>Some events are suspended until EndUpdate is called, and the <see cref="UndoStack"/> will
        /// group all changes into a single action.</para>
        /// <para>Calling BeginUpdate several times increments a counter, only after the appropriate number
        /// of EndUpdate calls the events resume their work.</para>
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public void BeginUpdate()
        {
            VerifyAccess();
            if (inDocumentChanging)
                throw new InvalidOperationException("Cannot change document within another document change.");
            beginUpdateCount++;
            if (beginUpdateCount == 1)
            {
                undoStack.StartUndoGroup();
                if (UpdateStarted != null)
                    UpdateStarted(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Ends a group of document changes.
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public void EndUpdate()
        {
            VerifyAccess();
            if (inDocumentChanging)
                throw new InvalidOperationException("Cannot end update within document change.");
            if (beginUpdateCount == 0)
                throw new InvalidOperationException("No update is active.");
            if (beginUpdateCount == 1)
            {
                // fire change events inside the change group - event handlers might add additional
                // document changes to the change group
                FireChangeEvents();
                undoStack.EndUndoGroup();
                beginUpdateCount = 0;
                if (UpdateFinished != null)
                    UpdateFinished(this, EventArgs.Empty);
            }
            else
            {
                beginUpdateCount -= 1;
            }
        }

        /// <summary>
        /// Occurs when a document change starts.
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public event EventHandler UpdateStarted;

        /// <summary>
        /// Occurs when a document change is finished.
        /// </summary>
        /// <remarks><inheritdoc cref="Changing"/></remarks>
        public event EventHandler UpdateFinished;
        #endregion

        #region Fire events after update
        int oldTextLength;
        int oldLineCount;
        bool fireTextChanged;

        /// <summary>
        /// Fires TextChanged, TextLengthChanged, LineCountChanged if required.
        /// </summary>
        internal void FireChangeEvents()
        {
            // it may be necessary to fire the event multiple times if the document is changed
            // from inside the event handlers
            while (fireTextChanged)
            {
                fireTextChanged = false;
                if (TextChanged != null)
                    TextChanged(this, EventArgs.Empty);
                OnPropertyChanged("Text");

                int textLength = rope.Length;
                if (textLength != oldTextLength)
                {
                    oldTextLength = textLength;
                    if (TextLengthChanged != null)
                        TextLengthChanged(this, EventArgs.Empty);
                    OnPropertyChanged("TextLength");
                }
                int lineCount = lineTree.LineCount;
                if (lineCount != oldLineCount)
                {
                    oldLineCount = lineCount;
                    if (LineCountChanged != null)
                        LineCountChanged(this, EventArgs.Empty);
                    OnPropertyChanged("LineCount");
                }
            }
        }

        void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        #region Insert / Remove  / Replace
        /// <summary>
        /// Inserts text.
        /// </summary>
        public void Insert(int offset, string text)
        {
            Replace(offset, 0, text);
        }

        /// <summary>
        /// Removes text.
        /// </summary>
        public void Remove(ISegment segment)
        {
            Replace(segment, string.Empty);
        }

        /// <summary>
        /// Removes text.
        /// </summary>
        public void Remove(int offset, int length)
        {
            Replace(offset, length, string.Empty);
        }

        internal bool inDocumentChanging;

        /// <summary>
        /// Replaces text.
        /// </summary>
        public void Replace(ISegment segment, string text)
        {
            if (segment == null)
                throw new ArgumentNullException("segment");
            Replace(segment.Offset, segment.Length, text, null);
        }

        /// <summary>
        /// Replaces text.
        /// </summary>
        public void Replace(int offset, int length, string text)
        {
            Replace(offset, length, text, null);
        }

        /// <summary>
        /// Replaces text.
        /// </summary>
        /// <param name="offset">The starting offset of the text to be replaced.</param>
        /// <param name="length">The length of the text to be replaced.</param>
        /// <param name="text">The new text.</param>
        /// <param name="offsetChangeMappingType">The offsetChangeMappingType 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.</param>
        public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType)
        {
            if (text == null)
                throw new ArgumentNullException("text");
            // Please see OffsetChangeMappingType XML comments for details on how these modes work.
            switch (offsetChangeMappingType)
            {
                case OffsetChangeMappingType.Normal:
                    Replace(offset, length, text, null);
                    break;
                case OffsetChangeMappingType.KeepAnchorBeforeInsertion:
                    Replace(offset, length, text, OffsetChangeMap.FromSingleElement(
                        new OffsetChangeMapEntry(offset, length, text.Length, false, true)));
                    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));
                        map.Freeze();
                        Replace(offset, length, text, map);
                    }
                    break;
                case OffsetChangeMappingType.CharacterReplace:
                    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)
                    {
                        // look at OffsetChangeMappingType.CharacterReplace XML comments on why we need to replace
                        // the last character
                        OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.Length - length);
                        Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
                    }
                    else if (text.Length < length)
                    {
                        OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.Length, length - text.Length, 0, true, false);
                        Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
                    }
                    else
                    {
                        Replace(offset, length, text, OffsetChangeMap.Empty);
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException("offsetChangeMappingType", offsetChangeMappingType, "Invalid enum value");
            }
        }

        /// <summary>
        /// Replaces text.
        /// </summary>
        /// <param name="offset">The starting offset of the text to be replaced.</param>
        /// <param name="length">The length of the text to be replaced.</param>
        /// <param name="text">The new text.</param>
        /// <param name="offsetChangeMap">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
        /// 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 <see cref="OffsetChangeMap.IsValidForDocumentChange"/>.
        /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting
        /// DocumentChangeEventArgs instance.
        /// </param>
        public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap)
        {
            if (text == null)
                throw new ArgumentNullException("text");

            if (offsetChangeMap != null)
                offsetChangeMap.Freeze();

            // Ensure that all changes take place inside an update group.
            // Will also take care of throwing an exception if inDocumentChanging is set.
            BeginUpdate();
            try
            {
                // protect document change against corruption by other changes inside the event handlers
                inDocumentChanging = true;
                try
                {
                    // The range verification must wait until after the BeginUpdate() call because the document
                    // might be modified inside the UpdateStarted event.
                    ThrowIfRangeInvalid(offset, length);

                    DoReplace(offset, length, text, offsetChangeMap);
                }
                finally
                {
                    inDocumentChanging = false;
                }
            }
            finally
            {
                EndUpdate();
            }
        }

        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);

            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, 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);
        }
        #endregion

        #region GetLineBy...
        /// <summary>
        /// Gets a read-only list of lines.
        /// </summary>
        /// <remarks><inheritdoc cref="DocumentLine"/></remarks>
        public IList<DocumentLine> Lines
        {
            get { return lineTree; }
        }

        /// <summary>
        /// Gets a line by the line number: O(log n)
        /// </summary>
        public DocumentLine GetLineByNumber(int number)
        {
            VerifyAccess();
            if (number < 1 || number > lineTree.LineCount)
                throw new ArgumentOutOfRangeException("number", number, "Value must be between 1 and " + lineTree.LineCount);
            return lineTree.GetByNumber(number);
        }

        /// <summary>
        /// Gets a document lines by offset.
        /// Runtime: O(log n)
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
        public DocumentLine GetLineByOffset(int offset)
        {
            VerifyAccess();
            if (offset < 0 || offset > rope.Length)
            {
                throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString());
            }
            return lineTree.GetByOffset(offset);
        }
        #endregion

        /// <summary>
        /// Gets the offset from a text location.
        /// </summary>
        /// <seealso cref="GetLocation"/>
        public int GetOffset(TextLocation location)
        {
            return GetOffset(location.Line, location.Column);
        }

        /// <summary>
        /// Gets the offset from a text location.
        /// </summary>
        /// <seealso cref="GetLocation"/>
        public int GetOffset(int line, int column)
        {
            DocumentLine docLine = GetLineByNumber(line);
            if (column <= 0)
                return docLine.Offset;
            if (column > docLine.Length)
                return docLine.EndOffset;
            return docLine.Offset + column - 1;
        }

        /// <summary>
        /// Gets the location from an offset.
        /// </summary>
        /// <seealso cref="GetOffset(TextLocation)"/>
        public TextLocation GetLocation(int offset)
        {
            DocumentLine line = GetLineByOffset(offset);
            return new TextLocation(line.LineNumber, offset - line.Offset + 1);
        }

        readonly ObservableCollection<ILineTracker> lineTrackers = new ObservableCollection<ILineTracker>();

        /// <summary>
        /// Gets the list of <see cref="ILineTracker"/>s attached to this document.
        /// You can add custom line trackers to this list.
        /// </summary>
        public IList<ILineTracker> LineTrackers
        {
            get
            {
                VerifyAccess();
                return lineTrackers;
            }
        }

        UndoStack undoStack;

        /// <summary>
        /// Gets the <see cref="UndoStack"/> of the document.
        /// </summary>
        /// <remarks>This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents.</remarks>
        public UndoStack UndoStack
        {
            get { return undoStack; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException();
                if (value != undoStack)
                {
                    undoStack.ClearAll(); // first clear old undo stack, so that it can't be used to perform unexpected changes on this document
                                          // ClearAll() will also throw an exception when it's not safe to replace the undo stack (e.g. update is currently in progress)
                    undoStack = value;
                    OnPropertyChanged("UndoStack");
                }
            }
        }

        /// <summary>
        /// Creates a new <see cref="TextAnchor"/> at the specified offset.
        /// </summary>
        /// <inheritdoc cref="TextAnchor" select="remarks|example"/>
        public TextAnchor CreateAnchor(int offset)
        {
            VerifyAccess();
            if (offset < 0 || offset > rope.Length)
            {
                throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
            }
            return anchorTree.CreateAnchor(offset);
        }

        #region LineCount
        /// <summary>
        /// Gets the total number of lines in the document.
        /// Runtime: O(1).
        /// </summary>
        public int LineCount
        {
            get
            {
                VerifyAccess();
                return lineTree.LineCount;
            }
        }

        /// <summary>
        /// Is raised when the LineCount property changes.
        /// </summary>
        [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")]
        public event EventHandler LineCountChanged;
        #endregion

        #region Debugging
        [Conditional("DEBUG")]
        internal void DebugVerifyAccess()
        {
            VerifyAccess();
        }

        /// <summary>
        /// Gets the document lines tree in string form.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        internal string GetLineTreeAsString()
        {
#if DEBUG
            return lineTree.GetTreeAsString();
#else
			return "Not available in release build.";
#endif
        }

        /// <summary>
        /// Gets the text anchor tree in string form.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        internal string GetTextAnchorTreeAsString()
        {
#if DEBUG
            return anchorTree.GetTreeAsString();
#else
			return "Not available in release build.";
#endif
        }
        #endregion
    }
}