aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ChangeTrackingCheckpoint.cs
blob: 283731da89389f98050da185b29cc98b91064f91 (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
// 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 Tango.Scripting.Editors.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Tango.Scripting.Editors.Document
{
	/// <summary>
	/// <para>A checkpoint that allows tracking changes to a TextDocument.</para>
	/// <para>
	/// Use <see cref="TextDocument.CreateSnapshot(out ChangeTrackingCheckpoint)"/> to create a checkpoint.
	/// </para>
	/// </summary>
	/// <remarks>
	/// <para>The <see cref="ChangeTrackingCheckpoint"/> class allows tracking document changes, even from background threads.</para>
	/// <para>Once you have two checkpoints, you can call <see cref="ChangeTrackingCheckpoint.GetChangesTo"/> to retrieve the complete list
	/// of document changes that happened between those versions of the document.</para>
	/// </remarks>
	public sealed class ChangeTrackingCheckpoint
	{
		// Object that is unique per document.
		// Used to determine if two checkpoints belong to the same document.
		// We don't use a reference to the document itself to allow the GC to reclaim the document memory
		// even if there are still references to checkpoints.
		readonly object documentIdentifier;
		
		// 'value' is the change from the previous checkpoint to this checkpoint
		// TODO: store the change in the older checkpoint instead - if only a reference to the
		// newest document version exists, the GC should be able to collect all DocumentChangeEventArgs.
		readonly DocumentChangeEventArgs value;
		readonly int id;
		ChangeTrackingCheckpoint next;
		
		internal ChangeTrackingCheckpoint(object documentIdentifier)
		{
			this.documentIdentifier = documentIdentifier;
		}
		
		internal ChangeTrackingCheckpoint(object documentIdentifier, DocumentChangeEventArgs value, int id)
		{
			this.documentIdentifier = documentIdentifier;
			this.value = value;
			this.id = id;
		}
		
		internal ChangeTrackingCheckpoint Append(DocumentChangeEventArgs change)
		{
			Debug.Assert(this.next == null);
			this.next = new ChangeTrackingCheckpoint(this.documentIdentifier, change, unchecked( this.id + 1 ));
			return this.next;
		}
		
		/// <summary>
		/// Creates a change tracking checkpoint for the specified document.
		/// This method is thread-safe.
		/// If you need a ChangeTrackingCheckpoint that's consistent with a snapshot of the document,
		/// use <see cref="TextDocument.CreateSnapshot(out ChangeTrackingCheckpoint)"/>.
		/// </summary>
		public static ChangeTrackingCheckpoint Create(TextDocument document)
		{
			if (document == null)
				throw new ArgumentNullException("document");
			return document.CreateChangeTrackingCheckpoint();
		}
		
		/// <summary>
		/// Gets whether this checkpoint belongs to the same document as the other checkpoint.
		/// </summary>
		public bool BelongsToSameDocumentAs(ChangeTrackingCheckpoint other)
		{
			if (other == null)
				throw new ArgumentNullException("other");
			return documentIdentifier == other.documentIdentifier;
		}
		
		/// <summary>
		/// Compares the age of this checkpoint to the other checkpoint.
		/// </summary>
		/// <remarks>This method is thread-safe.</remarks>
		/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
		/// <returns>-1 if this checkpoint is older than <paramref name="other"/>.
		/// 0 if <c>this</c>==<paramref name="other"/>.
		/// 1 if this checkpoint is newer than <paramref name="other"/>.</returns>
		public int CompareAge(ChangeTrackingCheckpoint other)
		{
			if (other == null)
				throw new ArgumentNullException("other");
			if (other.documentIdentifier != this.documentIdentifier)
				throw new ArgumentException("Checkpoints do not belong to the same document.");
			// We will allow overflows, but assume that the maximum distance between checkpoints is 2^31-1.
			// This is guaranteed on x86 because so many checkpoints don't fit into memory.
			return Math.Sign(unchecked( this.id - other.id ));
		}
		
		/// <summary>
		/// Gets the changes from this checkpoint to the other checkpoint.
		/// If 'other' is older than this checkpoint, reverse changes are calculated.
		/// </summary>
		/// <remarks>This method is thread-safe.</remarks>
		/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
		public IEnumerable<DocumentChangeEventArgs> GetChangesTo(ChangeTrackingCheckpoint other)
		{
			int result = CompareAge(other);
			if (result < 0)
				return GetForwardChanges(other);
			else if (result > 0)
				return other.GetForwardChanges(this).Reverse().Select(change => change.Invert());
			else
				return Empty<DocumentChangeEventArgs>.Array;
		}
		
		IEnumerable<DocumentChangeEventArgs> GetForwardChanges(ChangeTrackingCheckpoint other)
		{
			// Return changes from this(exclusive) to other(inclusive).
			ChangeTrackingCheckpoint node = this;
			do {
				node = node.next;
				yield return node.value;
			} while (node != other);
		}
		
		/// <summary>
		/// Calculates where the offset has moved in the other buffer version.
		/// </summary>
		/// <remarks>This method is thread-safe.</remarks>
		/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
		public int MoveOffsetTo(ChangeTrackingCheckpoint other, int oldOffset, AnchorMovementType movement)
		{
			int offset = oldOffset;
			foreach (DocumentChangeEventArgs e in GetChangesTo(other)) {
				offset = e.GetNewOffset(offset, movement);
			}
			return offset;
		}
	}
}
light .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
// 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
    }
}