// 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
}
}