aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/SideChains/ICSharpCode.AvalonEdit/Document/TextDocument.cs
blob: 23ed66e594804e72046d869a9cbb8146366fabbb (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
197
198
199
200
201
202
203
204
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
// 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 ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit.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(offset, 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
	}
}