// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Globalization; using System.Threading; using ICSharpCode.AvalonEdit.Utils; using ICSharpCode.NRefactory; using ICSharpCode.NRefactory.Editor; namespace ICSharpCode.AvalonEdit.Document { /// /// This class is the main class of the text model. Basically, it is a with events. /// /// /// Thread safety: /// /// However, there is a single method that is thread-safe: (and its overloads). /// public sealed class TextDocument : IDocument, INotifyPropertyChanged { #region Thread ownership readonly object lockObject = new object(); Thread owner = Thread.CurrentThread; /// /// Verifies that the current thread is the documents owner thread. /// Throws an if the wrong thread accesses the TextDocument. /// /// /// The TextDocument class is not thread-safe. A document instance expects to have a single owner thread /// and will throw an when accessed from another thread. /// It is possible to change the owner thread using the method. /// public void VerifyAccess() { if (Thread.CurrentThread != owner) throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it."); } /// /// 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. /// /// /// /// /// 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 . /// /// 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 rope; readonly DocumentLineTree lineTree; readonly LineManager lineManager; readonly TextAnchorTree anchorTree; readonly TextSourceVersionProvider versionProvider = new TextSourceVersionProvider(); /// /// Create an empty text document. /// public TextDocument() : this(string.Empty) { } /// /// Create a new text document with the specified initial text. /// public TextDocument(IEnumerable initialText) { if (initialText == null) throw new ArgumentNullException("initialText"); rope = new Rope(initialText); lineTree = new DocumentLineTree(this); lineManager = new LineManager(lineTree, this); lineTrackers.CollectionChanged += delegate { lineManager.UpdateListOfLineTrackers(); }; anchorTree = new TextAnchorTree(this); undoStack = new UndoStack(); FireChangeEvents(); } /// /// Create a new text document with the specified initial text. /// public TextDocument(ITextSource initialText) : this(GetTextFromTextSource(initialText)) { } // gets the text from a text source, directly retrieving the underlying rope where possible static IEnumerable GetTextFromTextSource(ITextSource textSource) { if (textSource == null) throw new ArgumentNullException("textSource"); #if NREFACTORY if (textSource is ReadOnlyDocument) textSource = textSource.CreateSnapshot(); // retrieve underlying text source, which might be a RopeTextSource #endif 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)); } } /// public string GetText(int offset, int length) { VerifyAccess(); return rope.ToString(offset, length); } /// /// Retrieves the text for a portion of the document. /// public string GetText(ISegment segment) { if (segment == null) throw new ArgumentNullException("segment"); return GetText(segment.Offset, segment.Length); } /// public int IndexOf(char c, int startIndex, int count) { DebugVerifyAccess(); return rope.IndexOf(c, startIndex, count); } /// public int LastIndexOf(char c, int startIndex, int count) { DebugVerifyAccess(); return rope.LastIndexOf(c, startIndex, count); } /// public int IndexOfAny(char[] anyOf, int startIndex, int count) { DebugVerifyAccess(); // frequently called (NewLineFinder), so must be fast in release builds return rope.IndexOfAny(anyOf, startIndex, count); } /// public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) { DebugVerifyAccess(); return rope.IndexOf(searchText, startIndex, count, comparisonType); } /// public int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) { DebugVerifyAccess(); return rope.LastIndexOf(searchText, startIndex, count, comparisonType); } /// public char GetCharAt(int offset) { DebugVerifyAccess(); // frequently called, so must be fast in release builds return rope[offset]; } WeakReference cachedText; /// /// Gets/Sets the text of the whole document. /// 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); } } /// /// public event EventHandler TextChanged; event EventHandler IDocument.ChangeCompleted { add { this.TextChanged += value; } remove { this.TextChanged -= value; } } /// public int TextLength { get { VerifyAccess(); return rope.Length; } } /// /// Is raised when the TextLength property changes. /// /// [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")] public event EventHandler TextLengthChanged; /// /// Is raised when one of the properties , , , /// changes. /// /// public event PropertyChangedEventHandler PropertyChanged; /// /// Is raised before the document changes. /// /// /// Here is the order in which events are raised during a document update: /// /// BeginUpdate() /// /// Start of change group (on undo stack) /// event is raised /// /// Insert() / Remove() / Replace() /// /// event is raised /// The document is changed /// TextAnchor.Deleted event is raised if anchors were /// in the deleted text portion /// event is raised /// /// EndUpdate() /// /// event is raised /// event is raised (for the Text, TextLength, LineCount properties, in that order) /// End of change group (on undo stack) /// event is raised /// /// /// /// If the insert/remove/replace methods are called without a call to BeginUpdate(), /// they will call BeginUpdate() and EndUpdate() to ensure no change happens outside of UpdateStarted/UpdateFinished. /// /// There can be multiple document changes between the BeginUpdate() and EndUpdate() calls. /// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done. /// /// The listens to the UpdateStarted and UpdateFinished events to group all changes into a single undo step. /// /// public event EventHandler Changing; // Unfortunately EventHandler is invariant, so we have to use two separate events private event EventHandler textChanging; event EventHandler IDocument.TextChanging { add { textChanging += value; } remove { textChanging -= value; } } /// /// Is raised after the document has changed. /// /// public event EventHandler Changed; private event EventHandler textChanged; event EventHandler IDocument.TextChanged { add { textChanged += value; } remove { textChanged -= value; } } /// /// Creates a snapshot of the current text. /// /// /// 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. /// /// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other /// classes implementing ITextSource.CreateSnapshot(). /// /// /// public ITextSource CreateSnapshot() { lock (lockObject) { return new RopeTextSource(rope, versionProvider.CurrentVersion); } } /// /// Creates a snapshot of a part of the current text. /// /// public ITextSource CreateSnapshot(int offset, int length) { lock (lockObject) { return new RopeTextSource(rope.GetRange(offset, length)); } } #if NREFACTORY /// public IDocument CreateDocumentSnapshot() { return new ReadOnlyDocument(this, fileName); } #endif /// public ITextSourceVersion Version { get { return versionProvider.CurrentVersion; } } /// public System.IO.TextReader CreateReader() { lock (lockObject) { return new RopeTextReader(rope); } } /// public System.IO.TextReader CreateReader(int offset, int length) { lock (lockObject) { return new RopeTextReader(rope.GetRange(offset, length)); } } /// public void WriteTextTo(System.IO.TextWriter writer) { VerifyAccess(); rope.WriteTo(writer, 0, rope.Length); } /// public void WriteTextTo(System.IO.TextWriter writer, int offset, int length) { VerifyAccess(); rope.WriteTo(writer, offset, length); } #endregion #region BeginUpdate / EndUpdate int beginUpdateCount; /// /// Gets if an update is running. /// /// public bool IsInUpdate { get { VerifyAccess(); return beginUpdateCount > 0; } } /// /// Immediately calls , /// and returns an IDisposable that calls . /// /// public IDisposable RunUpdate() { BeginUpdate(); return new CallbackOnDispose(EndUpdate); } /// /// Begins a group of document changes. /// Some events are suspended until EndUpdate is called, and the will /// group all changes into a single action. /// Calling BeginUpdate several times increments a counter, only after the appropriate number /// of EndUpdate calls the events resume their work. /// /// 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); } } /// /// Ends a group of document changes. /// /// 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; } } /// /// Occurs when a document change starts. /// /// public event EventHandler UpdateStarted; /// /// Occurs when a document change is finished. /// /// public event EventHandler UpdateFinished; void IDocument.StartUndoableAction() { BeginUpdate(); } void IDocument.EndUndoableAction() { EndUpdate(); } IDisposable IDocument.OpenUndoGroup() { return RunUpdate(); } #endregion #region Fire events after update int oldTextLength; int oldLineCount; bool fireTextChanged; /// /// Fires TextChanged, TextLengthChanged, LineCountChanged if required. /// 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 /// /// Inserts text. /// /// The offset at which the text is inserted. /// The new text. /// /// Anchors positioned exactly at the insertion offset will move according to their movement type. /// For AnchorMovementType.Default, they will move behind the inserted text. /// The caret will also move behind the inserted text. /// public void Insert(int offset, string text) { Replace(offset, 0, new StringTextSource(text), null); } /// /// Inserts text. /// /// The offset at which the text is inserted. /// The new text. /// /// Anchors positioned exactly at the insertion offset will move according to their movement type. /// For AnchorMovementType.Default, they will move behind the inserted text. /// The caret will also move behind the inserted text. /// public void Insert(int offset, ITextSource text) { Replace(offset, 0, text, null); } /// /// Inserts text. /// /// The offset at which the text is inserted. /// The new text. /// /// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type. /// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter. /// The caret will also move according to the parameter. /// public void Insert(int offset, string text, AnchorMovementType defaultAnchorMovementType) { if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion) { Replace(offset, 0, new StringTextSource(text), OffsetChangeMappingType.KeepAnchorBeforeInsertion); } else { Replace(offset, 0, new StringTextSource(text), null); } } /// /// Inserts text. /// /// The offset at which the text is inserted. /// The new text. /// /// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type. /// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter. /// The caret will also move according to the parameter. /// public void Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType) { if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion) { Replace(offset, 0, text, OffsetChangeMappingType.KeepAnchorBeforeInsertion); } else { Replace(offset, 0, text, null); } } /// /// Removes text. /// public void Remove(ISegment segment) { Replace(segment, string.Empty); } /// /// Removes text. /// /// Starting offset of the text to be removed. /// Length of the text to be removed. public void Remove(int offset, int length) { Replace(offset, length, StringTextSource.Empty); } internal bool inDocumentChanging; /// /// Replaces text. /// public void Replace(ISegment segment, string text) { if (segment == null) throw new ArgumentNullException("segment"); Replace(segment.Offset, segment.Length, new StringTextSource(text), null); } /// /// Replaces text. /// public void Replace(ISegment segment, ITextSource text) { if (segment == null) throw new ArgumentNullException("segment"); Replace(segment.Offset, segment.Length, text, null); } /// /// Replaces text. /// /// The starting offset of the text to be replaced. /// The length of the text to be replaced. /// The new text. public void Replace(int offset, int length, string text) { Replace(offset, length, new StringTextSource(text), null); } /// /// Replaces text. /// /// The starting offset of the text to be replaced. /// The length of the text to be replaced. /// The new text. public void Replace(int offset, int length, ITextSource text) { Replace(offset, length, text, null); } /// /// Replaces text. /// /// The starting offset of the text to be replaced. /// The length of the text to be replaced. /// The new text. /// 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. public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType) { Replace(offset, length, new StringTextSource(text), offsetChangeMappingType); } /// /// Replaces text. /// /// The starting offset of the text to be replaced. /// The length of the text to be replaced. /// The new text. /// 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. public void Replace(int offset, int length, ITextSource 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.TextLength, false, true))); break; case OffsetChangeMappingType.RemoveAndInsert: if (length == 0 || text.TextLength == 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.TextLength)); map.Freeze(); Replace(offset, length, text, map); } break; case OffsetChangeMappingType.CharacterReplace: if (length == 0 || text.TextLength == 0) { // only insertion or only removal? // OffsetChangeMappingType doesn't matter, just use Normal. Replace(offset, length, text, null); } else if (text.TextLength > 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.TextLength - length); Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry)); } else if (text.TextLength < length) { OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.TextLength, length - text.TextLength, 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"); } } /// /// Replaces text. /// /// The starting offset of the text to be replaced. /// The length of the text to be replaced. /// The new text. /// 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 . /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting /// DocumentChangeEventArgs instance. /// public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap) { Replace(offset, length, new StringTextSource(text), offsetChangeMap); } /// /// Replaces text. /// /// The starting offset of the text to be replaced. /// The length of the text to be replaced. /// The new text. /// 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 . /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting /// DocumentChangeEventArgs instance. /// public void Replace(int offset, int length, ITextSource text, OffsetChangeMap offsetChangeMap) { if (text == null) throw new ArgumentNullException("text"); text = text.CreateSnapshot(); 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, ITextSource newText, OffsetChangeMap offsetChangeMap) { if (length == 0 && newText.TextLength == 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.TextLength == 1 && offsetChangeMap == null) offsetChangeMap = OffsetChangeMap.Empty; ITextSource removedText; if (length == 0) { removedText = StringTextSource.Empty; } else if (length < 100) { removedText = new StringTextSource(rope.ToString(offset, length)); } else { // use a rope if the removed string is long removedText = new RopeTextSource(rope.GetRange(offset, length)); } DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap); // fire DocumentChanging event if (Changing != null) Changing(this, args); if (textChanging != null) textChanging(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 versionProvider.AppendChange(args); // now update the textBuffer and lineTree if (offset == 0 && length == rope.Length) { // optimize replacing the whole document rope.Clear(); var newRopeTextSource = newText as RopeTextSource; if (newRopeTextSource != null) rope.InsertRange(0, newRopeTextSource.GetRope()); else rope.InsertText(0, newText.Text); lineManager.Rebuild(); } else { rope.RemoveRange(offset, length); lineManager.Remove(offset, length); #if DEBUG lineTree.CheckProperties(); #endif var newRopeTextSource = newText as RopeTextSource; if (newRopeTextSource != null) rope.InsertRange(offset, newRopeTextSource.GetRope()); else rope.InsertText(offset, newText.Text); 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); } } lineManager.ChangeComplete(args); // raise delayed events after our data structures are consistent again delayedEvents.RaiseEvents(); // fire DocumentChanged event if (Changed != null) Changed(this, args); if (textChanged != null) textChanged(this, args); } #endregion #region GetLineBy... /// /// Gets a read-only list of lines. /// /// public IList Lines { get { return lineTree; } } /// /// Gets a line by the line number: O(log n) /// 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); } IDocumentLine IDocument.GetLineByNumber(int lineNumber) { return GetLineByNumber(lineNumber); } /// /// Gets a document lines by offset. /// Runtime: O(log n) /// [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); } IDocumentLine IDocument.GetLineByOffset(int offset) { return GetLineByOffset(offset); } #endregion #region GetOffset / GetLocation /// /// Gets the offset from a text location. /// /// public int GetOffset(TextLocation location) { return GetOffset(location.Line, location.Column); } /// /// Gets the offset from a text location. /// /// 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; } /// /// Gets the location from an offset. /// /// public TextLocation GetLocation(int offset) { DocumentLine line = GetLineByOffset(offset); return new TextLocation(line.LineNumber, offset - line.Offset + 1); } #endregion #region Line Trackers readonly ObservableCollection lineTrackers = new ObservableCollection(); /// /// Gets the list of s attached to this document. /// You can add custom line trackers to this list. /// public IList LineTrackers { get { VerifyAccess(); return lineTrackers; } } #endregion #region UndoStack UndoStack undoStack; /// /// Gets the of the document. /// /// This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents. 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"); } } } #endregion #region CreateAnchor /// /// Creates a new at the specified offset. /// /// 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); } ITextAnchor IDocument.CreateAnchor(int offset) { return CreateAnchor(offset); } #endregion #region LineCount /// /// Gets the total number of lines in the document. /// Runtime: O(1). /// public int LineCount { get { VerifyAccess(); return lineTree.LineCount; } } /// /// Is raised when the LineCount property changes. /// [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(); } /// /// Gets the document lines tree in string form. /// [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 } /// /// Gets the text anchor tree in string form. /// [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 #region Service Provider IServiceProvider serviceProvider; /// /// Gets/Sets the service provider associated with this document. /// By default, every TextDocument has its own ServiceContainer; and has the document itself /// registered as and . /// public IServiceProvider ServiceProvider { get { VerifyAccess(); if (serviceProvider == null) { var container = new ServiceContainer(); container.AddService(typeof(IDocument), this); container.AddService(typeof(TextDocument), this); serviceProvider = container; } return serviceProvider; } set { VerifyAccess(); if (value == null) throw new ArgumentNullException(); serviceProvider = value; } } object IServiceProvider.GetService(Type serviceType) { return this.ServiceProvider.GetService(serviceType); } #endregion #region FileName string fileName; /// public event EventHandler FileNameChanged; void OnFileNameChanged(EventArgs e) { EventHandler handler = this.FileNameChanged; if (handler != null) handler(this, e); } /// public string FileName { get { return fileName; } set { if (fileName != value) { fileName = value; OnFileNameChanged(EventArgs.Empty); } } } #endregion } }