Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Document/TextDocument.cs @ 14419

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

#2077: created branch and added first version

File size: 38.6 KB
Line 
1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Collections.Generic;
21using System.Collections.ObjectModel;
22using System.ComponentModel;
23using System.ComponentModel.Design;
24using System.Diagnostics;
25using System.Globalization;
26using System.Threading;
27using ICSharpCode.AvalonEdit.Utils;
28using ICSharpCode.NRefactory;
29using ICSharpCode.NRefactory.Editor;
30
31namespace ICSharpCode.AvalonEdit.Document
32{
33  /// <summary>
34  /// This class is the main class of the text model. Basically, it is a <see cref="System.Text.StringBuilder"/> with events.
35  /// </summary>
36  /// <remarks>
37  /// <b>Thread safety:</b>
38  /// <inheritdoc cref="VerifyAccess"/>
39  /// <para>However, there is a single method that is thread-safe: <see cref="CreateSnapshot()"/> (and its overloads).</para>
40  /// </remarks>
41  public sealed class TextDocument : IDocument, INotifyPropertyChanged
42  {
43    #region Thread ownership
44    readonly object lockObject = new object();
45    Thread owner = Thread.CurrentThread;
46   
47    /// <summary>
48    /// Verifies that the current thread is the documents owner thread.
49    /// Throws an <see cref="InvalidOperationException"/> if the wrong thread accesses the TextDocument.
50    /// </summary>
51    /// <remarks>
52    /// <para>The TextDocument class is not thread-safe. A document instance expects to have a single owner thread
53    /// and will throw an <see cref="InvalidOperationException"/> when accessed from another thread.
54    /// It is possible to change the owner thread using the <see cref="SetOwnerThread"/> method.</para>
55    /// </remarks>
56    public void VerifyAccess()
57    {
58      if (Thread.CurrentThread != owner)
59        throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it.");
60    }
61   
62    /// <summary>
63    /// Transfers ownership of the document to another thread. This method can be used to load
64    /// a file into a TextDocument on a background thread and then transfer ownership to the UI thread
65    /// for displaying the document.
66    /// </summary>
67    /// <remarks>
68    /// <inheritdoc cref="VerifyAccess"/>
69    /// <para>
70    /// The owner can be set to null, which means that no thread can access the document. But, if the document
71    /// has no owner thread, any thread may take ownership by calling <see cref="SetOwnerThread"/>.
72    /// </para>
73    /// </remarks>
74    public void SetOwnerThread(Thread newOwner)
75    {
76      // We need to lock here to ensure that in the null owner case,
77      // only one thread succeeds in taking ownership.
78      lock (lockObject) {
79        if (owner != null) {
80          VerifyAccess();
81        }
82        owner = newOwner;
83      }
84    }
85    #endregion
86   
87    #region Fields + Constructor
88    readonly Rope<char> rope;
89    readonly DocumentLineTree lineTree;
90    readonly LineManager lineManager;
91    readonly TextAnchorTree anchorTree;
92    readonly TextSourceVersionProvider versionProvider = new TextSourceVersionProvider();
93   
94    /// <summary>
95    /// Create an empty text document.
96    /// </summary>
97    public TextDocument()
98      : this(string.Empty)
99    {
100    }
101   
102    /// <summary>
103    /// Create a new text document with the specified initial text.
104    /// </summary>
105    public TextDocument(IEnumerable<char> initialText)
106    {
107      if (initialText == null)
108        throw new ArgumentNullException("initialText");
109      rope = new Rope<char>(initialText);
110      lineTree = new DocumentLineTree(this);
111      lineManager = new LineManager(lineTree, this);
112      lineTrackers.CollectionChanged += delegate {
113        lineManager.UpdateListOfLineTrackers();
114      };
115     
116      anchorTree = new TextAnchorTree(this);
117      undoStack = new UndoStack();
118      FireChangeEvents();
119    }
120   
121    /// <summary>
122    /// Create a new text document with the specified initial text.
123    /// </summary>
124    public TextDocument(ITextSource initialText)
125      : this(GetTextFromTextSource(initialText))
126    {
127    }
128   
129    // gets the text from a text source, directly retrieving the underlying rope where possible
130    static IEnumerable<char> GetTextFromTextSource(ITextSource textSource)
131    {
132      if (textSource == null)
133        throw new ArgumentNullException("textSource");
134     
135      #if NREFACTORY
136      if (textSource is ReadOnlyDocument)
137        textSource = textSource.CreateSnapshot(); // retrieve underlying text source, which might be a RopeTextSource
138      #endif
139     
140      RopeTextSource rts = textSource as RopeTextSource;
141      if (rts != null)
142        return rts.GetRope();
143     
144      TextDocument doc = textSource as TextDocument;
145      if (doc != null)
146        return doc.rope;
147     
148      return textSource.Text;
149    }
150    #endregion
151   
152    #region Text
153    void ThrowIfRangeInvalid(int offset, int length)
154    {
155      if (offset < 0 || offset > rope.Length) {
156        throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
157      }
158      if (length < 0 || offset + length > rope.Length) {
159        throw new ArgumentOutOfRangeException("length", length, "0 <= length, offset(" + offset + ")+length <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
160      }
161    }
162   
163    /// <inheritdoc/>
164    public string GetText(int offset, int length)
165    {
166      VerifyAccess();
167      return rope.ToString(offset, length);
168    }
169   
170    /// <summary>
171    /// Retrieves the text for a portion of the document.
172    /// </summary>
173    public string GetText(ISegment segment)
174    {
175      if (segment == null)
176        throw new ArgumentNullException("segment");
177      return GetText(segment.Offset, segment.Length);
178    }
179   
180    /// <inheritdoc/>
181    public int IndexOf(char c, int startIndex, int count)
182    {
183      DebugVerifyAccess();
184      return rope.IndexOf(c, startIndex, count);
185    }
186   
187    /// <inheritdoc/>
188    public int LastIndexOf(char c, int startIndex, int count)
189    {
190      DebugVerifyAccess();
191      return rope.LastIndexOf(c, startIndex, count);
192    }
193   
194    /// <inheritdoc/>
195    public int IndexOfAny(char[] anyOf, int startIndex, int count)
196    {
197      DebugVerifyAccess(); // frequently called (NewLineFinder), so must be fast in release builds
198      return rope.IndexOfAny(anyOf, startIndex, count);
199    }
200   
201    /// <inheritdoc/>
202    public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType)
203    {
204      DebugVerifyAccess();
205      return rope.IndexOf(searchText, startIndex, count, comparisonType);
206    }
207   
208    /// <inheritdoc/>
209    public int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType)
210    {
211      DebugVerifyAccess();
212      return rope.LastIndexOf(searchText, startIndex, count, comparisonType);
213    }
214   
215    /// <inheritdoc/>
216    public char GetCharAt(int offset)
217    {
218      DebugVerifyAccess(); // frequently called, so must be fast in release builds
219      return rope[offset];
220    }
221   
222    WeakReference cachedText;
223   
224    /// <summary>
225    /// Gets/Sets the text of the whole document.
226    /// </summary>
227    public string Text {
228      get {
229        VerifyAccess();
230        string completeText = cachedText != null ? (cachedText.Target as string) : null;
231        if (completeText == null) {
232          completeText = rope.ToString();
233          cachedText = new WeakReference(completeText);
234        }
235        return completeText;
236      }
237      set {
238        VerifyAccess();
239        if (value == null)
240          throw new ArgumentNullException("value");
241        Replace(0, rope.Length, value);
242      }
243    }
244   
245    /// <inheritdoc/>
246    /// <remarks><inheritdoc cref="Changing"/></remarks>
247    public event EventHandler TextChanged;
248   
249    event EventHandler IDocument.ChangeCompleted {
250      add { this.TextChanged += value; }
251      remove { this.TextChanged -= value; }
252    }
253   
254    /// <inheritdoc/>
255    public int TextLength {
256      get {
257        VerifyAccess();
258        return rope.Length;
259      }
260    }
261   
262    /// <summary>
263    /// Is raised when the TextLength property changes.
264    /// </summary>
265    /// <remarks><inheritdoc cref="Changing"/></remarks>
266    [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")]
267    public event EventHandler TextLengthChanged;
268   
269    /// <summary>
270    /// Is raised when one of the properties <see cref="Text"/>, <see cref="TextLength"/>, <see cref="LineCount"/>,
271    /// <see cref="UndoStack"/> changes.
272    /// </summary>
273    /// <remarks><inheritdoc cref="Changing"/></remarks>
274    public event PropertyChangedEventHandler PropertyChanged;
275   
276    /// <summary>
277    /// Is raised before the document changes.
278    /// </summary>
279    /// <remarks>
280    /// <para>Here is the order in which events are raised during a document update:</para>
281    /// <list type="bullet">
282    /// <item><description><b><see cref="BeginUpdate">BeginUpdate()</see></b></description>
283    ///   <list type="bullet">
284    ///   <item><description>Start of change group (on undo stack)</description></item>
285    ///   <item><description><see cref="UpdateStarted"/> event is raised</description></item>
286    ///   </list></item>
287    /// <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>
288    ///   <list type="bullet">
289    ///   <item><description><see cref="Changing"/> event is raised</description></item>
290    ///   <item><description>The document is changed</description></item>
291    ///   <item><description><see cref="TextAnchor.Deleted">TextAnchor.Deleted</see> event is raised if anchors were
292    ///     in the deleted text portion</description></item>
293    ///   <item><description><see cref="Changed"/> event is raised</description></item>
294    ///   </list></item>
295    /// <item><description><b><see cref="EndUpdate">EndUpdate()</see></b></description>
296    ///   <list type="bullet">
297    ///   <item><description><see cref="TextChanged"/> event is raised</description></item>
298    ///   <item><description><see cref="PropertyChanged"/> event is raised (for the Text, TextLength, LineCount properties, in that order)</description></item>
299    ///   <item><description>End of change group (on undo stack)</description></item>
300    ///   <item><description><see cref="UpdateFinished"/> event is raised</description></item>
301    ///   </list></item>
302    /// </list>
303    /// <para>
304    /// If the insert/remove/replace methods are called without a call to <c>BeginUpdate()</c>,
305    /// they will call <c>BeginUpdate()</c> and <c>EndUpdate()</c> to ensure no change happens outside of <c>UpdateStarted</c>/<c>UpdateFinished</c>.
306    /// </para><para>
307    /// There can be multiple document changes between the <c>BeginUpdate()</c> and <c>EndUpdate()</c> calls.
308    /// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done.
309    /// </para><para>
310    /// The <see cref="UndoStack"/> listens to the <c>UpdateStarted</c> and <c>UpdateFinished</c> events to group all changes into a single undo step.
311    /// </para>
312    /// </remarks>
313    public event EventHandler<DocumentChangeEventArgs> Changing;
314   
315    // Unfortunately EventHandler<T> is invariant, so we have to use two separate events
316    private event EventHandler<TextChangeEventArgs> textChanging;
317   
318    event EventHandler<TextChangeEventArgs> IDocument.TextChanging {
319      add { textChanging += value; }
320      remove { textChanging -= value; }
321    }
322   
323    /// <summary>
324    /// Is raised after the document has changed.
325    /// </summary>
326    /// <remarks><inheritdoc cref="Changing"/></remarks>
327    public event EventHandler<DocumentChangeEventArgs> Changed;
328   
329    private event EventHandler<TextChangeEventArgs> textChanged;
330   
331    event EventHandler<TextChangeEventArgs> IDocument.TextChanged {
332      add { textChanged += value; }
333      remove { textChanged -= value; }
334    }
335   
336    /// <summary>
337    /// Creates a snapshot of the current text.
338    /// </summary>
339    /// <remarks>
340    /// <para>This method returns an immutable snapshot of the document, and may be safely called even when
341    /// the document's owner thread is concurrently modifying the document.
342    /// </para><para>
343    /// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other
344    /// classes implementing ITextSource.CreateSnapshot().
345    /// </para><para>
346    /// </para>
347    /// </remarks>
348    public ITextSource CreateSnapshot()
349    {
350      lock (lockObject) {
351        return new RopeTextSource(rope, versionProvider.CurrentVersion);
352      }
353    }
354   
355    /// <summary>
356    /// Creates a snapshot of a part of the current text.
357    /// </summary>
358    /// <remarks><inheritdoc cref="CreateSnapshot()"/></remarks>
359    public ITextSource CreateSnapshot(int offset, int length)
360    {
361      lock (lockObject) {
362        return new RopeTextSource(rope.GetRange(offset, length));
363      }
364    }
365   
366    #if NREFACTORY
367    /// <inheritdoc/>
368    public IDocument CreateDocumentSnapshot()
369    {
370      return new ReadOnlyDocument(this, fileName);
371    }
372    #endif
373   
374    /// <inheritdoc/>
375    public ITextSourceVersion Version {
376      get { return versionProvider.CurrentVersion; }
377    }
378   
379    /// <inheritdoc/>
380    public System.IO.TextReader CreateReader()
381    {
382      lock (lockObject) {
383        return new RopeTextReader(rope);
384      }
385    }
386   
387    /// <inheritdoc/>
388    public System.IO.TextReader CreateReader(int offset, int length)
389    {
390      lock (lockObject) {
391        return new RopeTextReader(rope.GetRange(offset, length));
392      }
393    }
394   
395    /// <inheritdoc/>
396    public void WriteTextTo(System.IO.TextWriter writer)
397    {
398      VerifyAccess();
399      rope.WriteTo(writer, 0, rope.Length);
400    }
401   
402    /// <inheritdoc/>
403    public void WriteTextTo(System.IO.TextWriter writer, int offset, int length)
404    {
405      VerifyAccess();
406      rope.WriteTo(writer, offset, length);
407    }
408    #endregion
409   
410    #region BeginUpdate / EndUpdate
411    int beginUpdateCount;
412   
413    /// <summary>
414    /// Gets if an update is running.
415    /// </summary>
416    /// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
417    public bool IsInUpdate {
418      get {
419        VerifyAccess();
420        return beginUpdateCount > 0;
421      }
422    }
423   
424    /// <summary>
425    /// Immediately calls <see cref="BeginUpdate()"/>,
426    /// and returns an IDisposable that calls <see cref="EndUpdate()"/>.
427    /// </summary>
428    /// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
429    public IDisposable RunUpdate()
430    {
431      BeginUpdate();
432      return new CallbackOnDispose(EndUpdate);
433    }
434   
435    /// <summary>
436    /// <para>Begins a group of document changes.</para>
437    /// <para>Some events are suspended until EndUpdate is called, and the <see cref="UndoStack"/> will
438    /// group all changes into a single action.</para>
439    /// <para>Calling BeginUpdate several times increments a counter, only after the appropriate number
440    /// of EndUpdate calls the events resume their work.</para>
441    /// </summary>
442    /// <remarks><inheritdoc cref="Changing"/></remarks>
443    public void BeginUpdate()
444    {
445      VerifyAccess();
446      if (inDocumentChanging)
447        throw new InvalidOperationException("Cannot change document within another document change.");
448      beginUpdateCount++;
449      if (beginUpdateCount == 1) {
450        undoStack.StartUndoGroup();
451        if (UpdateStarted != null)
452          UpdateStarted(this, EventArgs.Empty);
453      }
454    }
455   
456    /// <summary>
457    /// Ends a group of document changes.
458    /// </summary>
459    /// <remarks><inheritdoc cref="Changing"/></remarks>
460    public void EndUpdate()
461    {
462      VerifyAccess();
463      if (inDocumentChanging)
464        throw new InvalidOperationException("Cannot end update within document change.");
465      if (beginUpdateCount == 0)
466        throw new InvalidOperationException("No update is active.");
467      if (beginUpdateCount == 1) {
468        // fire change events inside the change group - event handlers might add additional
469        // document changes to the change group
470        FireChangeEvents();
471        undoStack.EndUndoGroup();
472        beginUpdateCount = 0;
473        if (UpdateFinished != null)
474          UpdateFinished(this, EventArgs.Empty);
475      } else {
476        beginUpdateCount -= 1;
477      }
478    }
479   
480    /// <summary>
481    /// Occurs when a document change starts.
482    /// </summary>
483    /// <remarks><inheritdoc cref="Changing"/></remarks>
484    public event EventHandler UpdateStarted;
485   
486    /// <summary>
487    /// Occurs when a document change is finished.
488    /// </summary>
489    /// <remarks><inheritdoc cref="Changing"/></remarks>
490    public event EventHandler UpdateFinished;
491   
492    void IDocument.StartUndoableAction()
493    {
494      BeginUpdate();
495    }
496   
497    void IDocument.EndUndoableAction()
498    {
499      EndUpdate();
500    }
501   
502    IDisposable IDocument.OpenUndoGroup()
503    {
504      return RunUpdate();
505    }
506    #endregion
507   
508    #region Fire events after update
509    int oldTextLength;
510    int oldLineCount;
511    bool fireTextChanged;
512   
513    /// <summary>
514    /// Fires TextChanged, TextLengthChanged, LineCountChanged if required.
515    /// </summary>
516    internal void FireChangeEvents()
517    {
518      // it may be necessary to fire the event multiple times if the document is changed
519      // from inside the event handlers
520      while (fireTextChanged) {
521        fireTextChanged = false;
522        if (TextChanged != null)
523          TextChanged(this, EventArgs.Empty);
524        OnPropertyChanged("Text");
525       
526        int textLength = rope.Length;
527        if (textLength != oldTextLength) {
528          oldTextLength = textLength;
529          if (TextLengthChanged != null)
530            TextLengthChanged(this, EventArgs.Empty);
531          OnPropertyChanged("TextLength");
532        }
533        int lineCount = lineTree.LineCount;
534        if (lineCount != oldLineCount) {
535          oldLineCount = lineCount;
536          if (LineCountChanged != null)
537            LineCountChanged(this, EventArgs.Empty);
538          OnPropertyChanged("LineCount");
539        }
540      }
541    }
542   
543    void OnPropertyChanged(string propertyName)
544    {
545      if (PropertyChanged != null)
546        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
547    }
548    #endregion
549   
550    #region Insert / Remove  / Replace
551    /// <summary>
552    /// Inserts text.
553    /// </summary>
554    /// <param name="offset">The offset at which the text is inserted.</param>
555    /// <param name="text">The new text.</param>
556    /// <remarks>
557    /// Anchors positioned exactly at the insertion offset will move according to their movement type.
558    /// For AnchorMovementType.Default, they will move behind the inserted text.
559    /// The caret will also move behind the inserted text.
560    /// </remarks>
561    public void Insert(int offset, string text)
562    {
563      Replace(offset, 0, new StringTextSource(text), null);
564    }
565   
566    /// <summary>
567    /// Inserts text.
568    /// </summary>
569    /// <param name="offset">The offset at which the text is inserted.</param>
570    /// <param name="text">The new text.</param>
571    /// <remarks>
572    /// Anchors positioned exactly at the insertion offset will move according to their movement type.
573    /// For AnchorMovementType.Default, they will move behind the inserted text.
574    /// The caret will also move behind the inserted text.
575    /// </remarks>
576    public void Insert(int offset, ITextSource text)
577    {
578      Replace(offset, 0, text, null);
579    }
580   
581    /// <summary>
582    /// Inserts text.
583    /// </summary>
584    /// <param name="offset">The offset at which the text is inserted.</param>
585    /// <param name="text">The new text.</param>
586    /// <param name="defaultAnchorMovementType">
587    /// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type.
588    /// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter.
589    /// The caret will also move according to the <paramref name="defaultAnchorMovementType"/> parameter.
590    /// </param>
591    public void Insert(int offset, string text, AnchorMovementType defaultAnchorMovementType)
592    {
593      if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion) {
594        Replace(offset, 0, new StringTextSource(text), OffsetChangeMappingType.KeepAnchorBeforeInsertion);
595      } else {
596        Replace(offset, 0, new StringTextSource(text), null);
597      }
598    }
599   
600    /// <summary>
601    /// Inserts text.
602    /// </summary>
603    /// <param name="offset">The offset at which the text is inserted.</param>
604    /// <param name="text">The new text.</param>
605    /// <param name="defaultAnchorMovementType">
606    /// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type.
607    /// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter.
608    /// The caret will also move according to the <paramref name="defaultAnchorMovementType"/> parameter.
609    /// </param>
610    public void Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType)
611    {
612      if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion) {
613        Replace(offset, 0, text, OffsetChangeMappingType.KeepAnchorBeforeInsertion);
614      } else {
615        Replace(offset, 0, text, null);
616      }
617    }
618   
619    /// <summary>
620    /// Removes text.
621    /// </summary>
622    public void Remove(ISegment segment)
623    {
624      Replace(segment, string.Empty);
625    }
626   
627    /// <summary>
628    /// Removes text.
629    /// </summary>
630    /// <param name="offset">Starting offset of the text to be removed.</param>
631    /// <param name="length">Length of the text to be removed.</param>
632    public void Remove(int offset, int length)
633    {
634      Replace(offset, length, StringTextSource.Empty);
635    }
636   
637    internal bool inDocumentChanging;
638   
639    /// <summary>
640    /// Replaces text.
641    /// </summary>
642    public void Replace(ISegment segment, string text)
643    {
644      if (segment == null)
645        throw new ArgumentNullException("segment");
646      Replace(segment.Offset, segment.Length, new StringTextSource(text), null);
647    }
648   
649    /// <summary>
650    /// Replaces text.
651    /// </summary>
652    public void Replace(ISegment segment, ITextSource text)
653    {
654      if (segment == null)
655        throw new ArgumentNullException("segment");
656      Replace(segment.Offset, segment.Length, text, null);
657    }
658   
659    /// <summary>
660    /// Replaces text.
661    /// </summary>
662    /// <param name="offset">The starting offset of the text to be replaced.</param>
663    /// <param name="length">The length of the text to be replaced.</param>
664    /// <param name="text">The new text.</param>
665    public void Replace(int offset, int length, string text)
666    {
667      Replace(offset, length, new StringTextSource(text), null);
668    }
669   
670    /// <summary>
671    /// Replaces text.
672    /// </summary>
673    /// <param name="offset">The starting offset of the text to be replaced.</param>
674    /// <param name="length">The length of the text to be replaced.</param>
675    /// <param name="text">The new text.</param>
676    public void Replace(int offset, int length, ITextSource text)
677    {
678      Replace(offset, length, text, null);
679    }
680   
681    /// <summary>
682    /// Replaces text.
683    /// </summary>
684    /// <param name="offset">The starting offset of the text to be replaced.</param>
685    /// <param name="length">The length of the text to be replaced.</param>
686    /// <param name="text">The new text.</param>
687    /// <param name="offsetChangeMappingType">The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text.
688    /// This affects how the anchors and segments inside the replaced region behave.</param>
689    public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType)
690    {
691      Replace(offset, length, new StringTextSource(text), offsetChangeMappingType);
692    }
693   
694    /// <summary>
695    /// Replaces text.
696    /// </summary>
697    /// <param name="offset">The starting offset of the text to be replaced.</param>
698    /// <param name="length">The length of the text to be replaced.</param>
699    /// <param name="text">The new text.</param>
700    /// <param name="offsetChangeMappingType">The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text.
701    /// This affects how the anchors and segments inside the replaced region behave.</param>
702    public void Replace(int offset, int length, ITextSource text, OffsetChangeMappingType offsetChangeMappingType)
703    {
704      if (text == null)
705        throw new ArgumentNullException("text");
706      // Please see OffsetChangeMappingType XML comments for details on how these modes work.
707      switch (offsetChangeMappingType) {
708        case OffsetChangeMappingType.Normal:
709          Replace(offset, length, text, null);
710          break;
711        case OffsetChangeMappingType.KeepAnchorBeforeInsertion:
712          Replace(offset, length, text, OffsetChangeMap.FromSingleElement(
713            new OffsetChangeMapEntry(offset, length, text.TextLength, false, true)));
714          break;
715        case OffsetChangeMappingType.RemoveAndInsert:
716          if (length == 0 || text.TextLength == 0) {
717            // only insertion or only removal?
718            // OffsetChangeMappingType doesn't matter, just use Normal.
719            Replace(offset, length, text, null);
720          } else {
721            OffsetChangeMap map = new OffsetChangeMap(2);
722            map.Add(new OffsetChangeMapEntry(offset, length, 0));
723            map.Add(new OffsetChangeMapEntry(offset, 0, text.TextLength));
724            map.Freeze();
725            Replace(offset, length, text, map);
726          }
727          break;
728        case OffsetChangeMappingType.CharacterReplace:
729          if (length == 0 || text.TextLength == 0) {
730            // only insertion or only removal?
731            // OffsetChangeMappingType doesn't matter, just use Normal.
732            Replace(offset, length, text, null);
733          } else if (text.TextLength > length) {
734            // look at OffsetChangeMappingType.CharacterReplace XML comments on why we need to replace
735            // the last character
736            OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.TextLength - length);
737            Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
738          } else if (text.TextLength < length) {
739            OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.TextLength, length - text.TextLength, 0, true, false);
740            Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
741          } else {
742            Replace(offset, length, text, OffsetChangeMap.Empty);
743          }
744          break;
745        default:
746          throw new ArgumentOutOfRangeException("offsetChangeMappingType", offsetChangeMappingType, "Invalid enum value");
747      }
748    }
749   
750    /// <summary>
751    /// Replaces text.
752    /// </summary>
753    /// <param name="offset">The starting offset of the text to be replaced.</param>
754    /// <param name="length">The length of the text to be replaced.</param>
755    /// <param name="text">The new text.</param>
756    /// <param name="offsetChangeMap">The offsetChangeMap determines how offsets inside the old text are mapped to the new text.
757    /// This affects how the anchors and segments inside the replaced region behave.
758    /// If you pass null (the default when using one of the other overloads), the offsets are changed as
759    /// in OffsetChangeMappingType.Normal mode.
760    /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode).
761    /// The offsetChangeMap must be a valid 'explanation' for the document change. See <see cref="OffsetChangeMap.IsValidForDocumentChange"/>.
762    /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting
763    /// DocumentChangeEventArgs instance.
764    /// </param>
765    public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap)
766    {
767      Replace(offset, length, new StringTextSource(text), offsetChangeMap);
768    }
769   
770    /// <summary>
771    /// Replaces text.
772    /// </summary>
773    /// <param name="offset">The starting offset of the text to be replaced.</param>
774    /// <param name="length">The length of the text to be replaced.</param>
775    /// <param name="text">The new text.</param>
776    /// <param name="offsetChangeMap">The offsetChangeMap determines how offsets inside the old text are mapped to the new text.
777    /// This affects how the anchors and segments inside the replaced region behave.
778    /// If you pass null (the default when using one of the other overloads), the offsets are changed as
779    /// in OffsetChangeMappingType.Normal mode.
780    /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode).
781    /// The offsetChangeMap must be a valid 'explanation' for the document change. See <see cref="OffsetChangeMap.IsValidForDocumentChange"/>.
782    /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting
783    /// DocumentChangeEventArgs instance.
784    /// </param>
785    public void Replace(int offset, int length, ITextSource text, OffsetChangeMap offsetChangeMap)
786    {
787      if (text == null)
788        throw new ArgumentNullException("text");
789      text = text.CreateSnapshot();
790      if (offsetChangeMap != null)
791        offsetChangeMap.Freeze();
792     
793      // Ensure that all changes take place inside an update group.
794      // Will also take care of throwing an exception if inDocumentChanging is set.
795      BeginUpdate();
796      try {
797        // protect document change against corruption by other changes inside the event handlers
798        inDocumentChanging = true;
799        try {
800          // The range verification must wait until after the BeginUpdate() call because the document
801          // might be modified inside the UpdateStarted event.
802          ThrowIfRangeInvalid(offset, length);
803         
804          DoReplace(offset, length, text, offsetChangeMap);
805        } finally {
806          inDocumentChanging = false;
807        }
808      } finally {
809        EndUpdate();
810      }
811    }
812   
813    void DoReplace(int offset, int length, ITextSource newText, OffsetChangeMap offsetChangeMap)
814    {
815      if (length == 0 && newText.TextLength == 0)
816        return;
817     
818      // trying to replace a single character in 'Normal' mode?
819      // for single characters, 'CharacterReplace' mode is equivalent, but more performant
820      // (we don't have to touch the anchorTree at all in 'CharacterReplace' mode)
821      if (length == 1 && newText.TextLength == 1 && offsetChangeMap == null)
822        offsetChangeMap = OffsetChangeMap.Empty;
823     
824      ITextSource removedText;
825      if (length == 0) {
826        removedText = StringTextSource.Empty;
827      } else if (length < 100) {
828        removedText = new StringTextSource(rope.ToString(offset, length));
829      } else {
830        // use a rope if the removed string is long
831        removedText = new RopeTextSource(rope.GetRange(offset, length));
832      }
833      DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap);
834     
835      // fire DocumentChanging event
836      if (Changing != null)
837        Changing(this, args);
838      if (textChanging != null)
839        textChanging(this, args);
840     
841      undoStack.Push(this, args);
842     
843      cachedText = null; // reset cache of complete document text
844      fireTextChanged = true;
845      DelayedEvents delayedEvents = new DelayedEvents();
846     
847      lock (lockObject) {
848        // create linked list of checkpoints
849        versionProvider.AppendChange(args);
850       
851        // now update the textBuffer and lineTree
852        if (offset == 0 && length == rope.Length) {
853          // optimize replacing the whole document
854          rope.Clear();
855          var newRopeTextSource = newText as RopeTextSource;
856          if (newRopeTextSource != null)
857            rope.InsertRange(0, newRopeTextSource.GetRope());
858          else
859            rope.InsertText(0, newText.Text);
860          lineManager.Rebuild();
861        } else {
862          rope.RemoveRange(offset, length);
863          lineManager.Remove(offset, length);
864          #if DEBUG
865          lineTree.CheckProperties();
866          #endif
867          var newRopeTextSource = newText as RopeTextSource;
868          if (newRopeTextSource != null)
869            rope.InsertRange(offset, newRopeTextSource.GetRope());
870          else
871            rope.InsertText(offset, newText.Text);
872          lineManager.Insert(offset, newText);
873          #if DEBUG
874          lineTree.CheckProperties();
875          #endif
876        }
877      }
878     
879      // update text anchors
880      if (offsetChangeMap == null) {
881        anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents);
882      } else {
883        foreach (OffsetChangeMapEntry entry in offsetChangeMap) {
884          anchorTree.HandleTextChange(entry, delayedEvents);
885        }
886      }
887     
888      lineManager.ChangeComplete(args);
889     
890      // raise delayed events after our data structures are consistent again
891      delayedEvents.RaiseEvents();
892     
893      // fire DocumentChanged event
894      if (Changed != null)
895        Changed(this, args);
896      if (textChanged != null)
897        textChanged(this, args);
898    }
899    #endregion
900   
901    #region GetLineBy...
902    /// <summary>
903    /// Gets a read-only list of lines.
904    /// </summary>
905    /// <remarks><inheritdoc cref="DocumentLine"/></remarks>
906    public IList<DocumentLine> Lines {
907      get { return lineTree; }
908    }
909   
910    /// <summary>
911    /// Gets a line by the line number: O(log n)
912    /// </summary>
913    public DocumentLine GetLineByNumber(int number)
914    {
915      VerifyAccess();
916      if (number < 1 || number > lineTree.LineCount)
917        throw new ArgumentOutOfRangeException("number", number, "Value must be between 1 and " + lineTree.LineCount);
918      return lineTree.GetByNumber(number);
919    }
920   
921    IDocumentLine IDocument.GetLineByNumber(int lineNumber)
922    {
923      return GetLineByNumber(lineNumber);
924    }
925   
926    /// <summary>
927    /// Gets a document lines by offset.
928    /// Runtime: O(log n)
929    /// </summary>
930    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
931    public DocumentLine GetLineByOffset(int offset)
932    {
933      VerifyAccess();
934      if (offset < 0 || offset > rope.Length) {
935        throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString());
936      }
937      return lineTree.GetByOffset(offset);
938    }
939   
940    IDocumentLine IDocument.GetLineByOffset(int offset)
941    {
942      return GetLineByOffset(offset);
943    }
944    #endregion
945   
946    #region GetOffset / GetLocation
947    /// <summary>
948    /// Gets the offset from a text location.
949    /// </summary>
950    /// <seealso cref="GetLocation"/>
951    public int GetOffset(TextLocation location)
952    {
953      return GetOffset(location.Line, location.Column);
954    }
955   
956    /// <summary>
957    /// Gets the offset from a text location.
958    /// </summary>
959    /// <seealso cref="GetLocation"/>
960    public int GetOffset(int line, int column)
961    {
962      DocumentLine docLine = GetLineByNumber(line);
963      if (column <= 0)
964        return docLine.Offset;
965      if (column > docLine.Length)
966        return docLine.EndOffset;
967      return docLine.Offset + column - 1;
968    }
969   
970    /// <summary>
971    /// Gets the location from an offset.
972    /// </summary>
973    /// <seealso cref="GetOffset(TextLocation)"/>
974    public TextLocation GetLocation(int offset)
975    {
976      DocumentLine line = GetLineByOffset(offset);
977      return new TextLocation(line.LineNumber, offset - line.Offset + 1);
978    }
979    #endregion
980   
981    #region Line Trackers
982    readonly ObservableCollection<ILineTracker> lineTrackers = new ObservableCollection<ILineTracker>();
983   
984    /// <summary>
985    /// Gets the list of <see cref="ILineTracker"/>s attached to this document.
986    /// You can add custom line trackers to this list.
987    /// </summary>
988    public IList<ILineTracker> LineTrackers {
989      get {
990        VerifyAccess();
991        return lineTrackers;
992      }
993    }
994    #endregion
995   
996    #region UndoStack
997    UndoStack undoStack;
998   
999    /// <summary>
1000    /// Gets the <see cref="UndoStack"/> of the document.
1001    /// </summary>
1002    /// <remarks>This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents.</remarks>
1003    public UndoStack UndoStack {
1004      get { return undoStack; }
1005      set {
1006        if (value == null)
1007          throw new ArgumentNullException();
1008        if (value != undoStack) {
1009          undoStack.ClearAll(); // first clear old undo stack, so that it can't be used to perform unexpected changes on this document
1010          // ClearAll() will also throw an exception when it's not safe to replace the undo stack (e.g. update is currently in progress)
1011          undoStack = value;
1012          OnPropertyChanged("UndoStack");
1013        }
1014      }
1015    }
1016    #endregion
1017   
1018    #region CreateAnchor
1019    /// <summary>
1020    /// Creates a new <see cref="TextAnchor"/> at the specified offset.
1021    /// </summary>
1022    /// <inheritdoc cref="TextAnchor" select="remarks|example"/>
1023    public TextAnchor CreateAnchor(int offset)
1024    {
1025      VerifyAccess();
1026      if (offset < 0 || offset > rope.Length) {
1027        throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
1028      }
1029      return anchorTree.CreateAnchor(offset);
1030    }
1031   
1032    ITextAnchor IDocument.CreateAnchor(int offset)
1033    {
1034      return CreateAnchor(offset);
1035    }
1036    #endregion
1037   
1038    #region LineCount
1039    /// <summary>
1040    /// Gets the total number of lines in the document.
1041    /// Runtime: O(1).
1042    /// </summary>
1043    public int LineCount {
1044      get {
1045        VerifyAccess();
1046        return lineTree.LineCount;
1047      }
1048    }
1049   
1050    /// <summary>
1051    /// Is raised when the LineCount property changes.
1052    /// </summary>
1053    [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")]
1054    public event EventHandler LineCountChanged;
1055    #endregion
1056   
1057    #region Debugging
1058    [Conditional("DEBUG")]
1059    internal void DebugVerifyAccess()
1060    {
1061      VerifyAccess();
1062    }
1063   
1064    /// <summary>
1065    /// Gets the document lines tree in string form.
1066    /// </summary>
1067    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
1068    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
1069    internal string GetLineTreeAsString()
1070    {
1071      #if DEBUG
1072      return lineTree.GetTreeAsString();
1073      #else
1074      return "Not available in release build.";
1075      #endif
1076    }
1077   
1078    /// <summary>
1079    /// Gets the text anchor tree in string form.
1080    /// </summary>
1081    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
1082    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
1083    internal string GetTextAnchorTreeAsString()
1084    {
1085      #if DEBUG
1086      return anchorTree.GetTreeAsString();
1087      #else
1088      return "Not available in release build.";
1089      #endif
1090    }
1091    #endregion
1092   
1093    #region Service Provider
1094    IServiceProvider serviceProvider;
1095   
1096    /// <summary>
1097    /// Gets/Sets the service provider associated with this document.
1098    /// By default, every TextDocument has its own ServiceContainer; and has the document itself
1099    /// registered as <see cref="IDocument"/> and <see cref="TextDocument"/>.
1100    /// </summary>
1101    public IServiceProvider ServiceProvider {
1102      get {
1103        VerifyAccess();
1104        if (serviceProvider == null) {
1105          var container = new ServiceContainer();
1106          container.AddService(typeof(IDocument), this);
1107          container.AddService(typeof(TextDocument), this);
1108          serviceProvider = container;
1109        }
1110        return serviceProvider;
1111      }
1112      set {
1113        VerifyAccess();
1114        if (value == null)
1115          throw new ArgumentNullException();
1116        serviceProvider = value;
1117      }
1118    }
1119   
1120    object IServiceProvider.GetService(Type serviceType)
1121    {
1122      return this.ServiceProvider.GetService(serviceType);
1123    }
1124    #endregion
1125   
1126    #region FileName
1127    string fileName;
1128   
1129    /// <inheritdoc/>
1130    public event EventHandler FileNameChanged;
1131   
1132    void OnFileNameChanged(EventArgs e)
1133    {
1134      EventHandler handler = this.FileNameChanged;
1135      if (handler != null)
1136        handler(this, e);
1137    }
1138   
1139    /// <inheritdoc/>
1140    public string FileName {
1141      get { return fileName; }
1142      set {
1143        if (fileName != value) {
1144          fileName = value;
1145          OnFileNameChanged(EventArgs.Empty);
1146        }
1147      }
1148    }
1149    #endregion
1150  }
1151}
Note: See TracBrowser for help on using the repository browser.