// Copyright (c) 2010-2013 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.IO;
using System.Text;
using ICSharpCode.NRefactory.Utils;
namespace ICSharpCode.NRefactory.Editor
{
///
/// Document based on a string builder.
/// This class serves as a reference implementation for the IDocument interface.
///
public class StringBuilderDocument : IDocument
{
readonly StringBuilder b;
readonly TextSourceVersionProvider versionProvider = new TextSourceVersionProvider();
///
/// Creates a new StringBuilderDocument.
///
public StringBuilderDocument()
{
b = new StringBuilder();
}
///
/// Creates a new StringBuilderDocument with the specified initial text.
///
public StringBuilderDocument(string text)
{
if (text == null)
throw new ArgumentNullException("text");
b = new StringBuilder(text);
}
///
/// Creates a new StringBuilderDocument with the initial text copied from the specified text source.
///
public StringBuilderDocument(ITextSource textSource)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
b = new StringBuilder(textSource.TextLength);
textSource.WriteTextTo(new StringWriter(b));
}
///
public event EventHandler TextChanging;
///
public event EventHandler TextChanged;
///
public event EventHandler ChangeCompleted;
///
public ITextSourceVersion Version {
get { return versionProvider.CurrentVersion; }
}
#region Line<->Offset
///
public int LineCount {
get { return CreateDocumentSnapshot().LineCount; }
}
///
public IDocumentLine GetLineByNumber(int lineNumber)
{
return CreateDocumentSnapshot().GetLineByNumber(lineNumber);
}
///
public IDocumentLine GetLineByOffset(int offset)
{
return CreateDocumentSnapshot().GetLineByOffset(offset);
}
///
public int GetOffset(int line, int column)
{
return CreateDocumentSnapshot().GetOffset(line, column);
}
///
public int GetOffset(TextLocation location)
{
return CreateDocumentSnapshot().GetOffset(location);
}
///
public TextLocation GetLocation(int offset)
{
return CreateDocumentSnapshot().GetLocation(offset);
}
#endregion
#region Insert/Remove/Replace
///
public void Insert(int offset, string text)
{
Replace(offset, 0, text);
}
///
public void Insert(int offset, ITextSource text)
{
if (text == null)
throw new ArgumentNullException("text");
Replace(offset, 0, text.Text);
}
///
public void Insert(int offset, string text, AnchorMovementType defaultAnchorMovementType)
{
if (offset < 0 || offset > this.TextLength)
throw new ArgumentOutOfRangeException("offset");
if (text == null)
throw new ArgumentNullException("text");
if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion)
PerformChange(new InsertionWithMovementBefore(offset, text));
else
Replace(offset, 0, text);
}
///
public void Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType)
{
if (text == null)
throw new ArgumentNullException("text");
Insert(offset, text.Text, defaultAnchorMovementType);
}
[Serializable]
sealed class InsertionWithMovementBefore : TextChangeEventArgs
{
public InsertionWithMovementBefore(int offset, string newText) : base(offset, string.Empty, newText)
{
}
public override int GetNewOffset(int offset, AnchorMovementType movementType)
{
if (offset == this.Offset && movementType == AnchorMovementType.Default)
return offset;
else
return base.GetNewOffset(offset, movementType);
}
}
///
public void Remove(int offset, int length)
{
Replace(offset, length, string.Empty);
}
///
public void Replace(int offset, int length, string newText)
{
if (offset < 0 || offset > this.TextLength)
throw new ArgumentOutOfRangeException("offset");
if (length < 0 || length > this.TextLength - offset)
throw new ArgumentOutOfRangeException("length");
if (newText == null)
throw new ArgumentNullException("newText");
PerformChange(new TextChangeEventArgs(offset, b.ToString(offset, length), newText));
}
///
public void Replace(int offset, int length, ITextSource newText)
{
if (newText == null)
throw new ArgumentNullException("newText");
Replace(offset, length, newText.Text);
}
bool isInChange;
void PerformChange(TextChangeEventArgs change)
{
// Ensure that all changes take place inside an update group.
// Will also take care of throwing an exception if isInChange is set.
StartUndoableAction();
try {
isInChange = true;
try {
if (TextChanging != null)
TextChanging(this, change);
// Perform changes to document and Version property
documentSnapshot = null;
cachedText = null;
b.Remove(change.Offset, change.RemovalLength);
b.Insert(change.Offset, change.InsertedText.Text);
versionProvider.AppendChange(change);
// Update anchors and fire Deleted events
UpdateAnchors(change);
if (TextChanged != null)
TextChanged(this, change);
} finally {
isInChange = false;
}
} finally {
EndUndoableAction();
}
}
#endregion
#region Undo
int undoGroupNesting = 0;
///
public void StartUndoableAction()
{
// prevent changes from within the TextChanging/TextChanged event handlers
if (isInChange)
throw new InvalidOperationException();
undoGroupNesting++;
}
///
public void EndUndoableAction()
{
undoGroupNesting--;
if (undoGroupNesting == 0) {
if (ChangeCompleted != null)
ChangeCompleted(this, EventArgs.Empty);
}
}
///
public IDisposable OpenUndoGroup()
{
StartUndoableAction();
return new CallbackOnDispose(EndUndoableAction);
}
#endregion
#region CreateSnapshot/CreateReader
ReadOnlyDocument documentSnapshot;
///
public IDocument CreateDocumentSnapshot()
{
if (documentSnapshot == null)
documentSnapshot = new ReadOnlyDocument(this, this.FileName);
return documentSnapshot;
}
///
public ITextSource CreateSnapshot()
{
return new StringTextSource(this.Text, versionProvider.CurrentVersion);
}
///
public ITextSource CreateSnapshot(int offset, int length)
{
return new StringTextSource(GetText(offset, length));
}
///
public TextReader CreateReader()
{
return new StringReader(this.Text);
}
///
public TextReader CreateReader(int offset, int length)
{
return new StringReader(GetText(offset, length));
}
///
public void WriteTextTo(TextWriter writer)
{
if (writer == null)
throw new ArgumentNullException("writer");
writer.Write(this.Text);
}
///
public void WriteTextTo(TextWriter writer, int offset, int length)
{
if (writer == null)
throw new ArgumentNullException("writer");
writer.Write(GetText(offset, length));
}
#endregion
#region GetText / IndexOf
string cachedText;
///
public string Text {
get {
if (cachedText == null)
cachedText = b.ToString();
return cachedText;
}
set {
Replace(0, b.Length, value);
}
}
///
public int TextLength {
get { return b.Length; }
}
///
public char GetCharAt(int offset)
{
return b[offset];
}
///
public string GetText(int offset, int length)
{
return b.ToString(offset, length);
}
///
public string GetText(ISegment segment)
{
if (segment == null)
throw new ArgumentNullException("segment");
return b.ToString(segment.Offset, segment.Length);
}
///
public int IndexOf(char c, int startIndex, int count)
{
return this.Text.IndexOf(c, startIndex, count);
}
///
public int IndexOfAny(char[] anyOf, int startIndex, int count)
{
return this.Text.IndexOfAny(anyOf, startIndex, count);
}
///
public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType)
{
return this.Text.IndexOf(searchText, startIndex, count, comparisonType);
}
///
public int LastIndexOf(char c, int startIndex, int count)
{
return this.Text.LastIndexOf(c, startIndex + count - 1, count);
}
///
public int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType)
{
return this.Text.LastIndexOf(searchText, startIndex + count - 1, count, comparisonType);
}
#endregion
#region CreateAnchor
readonly List anchors = new List();
///
public ITextAnchor CreateAnchor(int offset)
{
var newAnchor = new SimpleAnchor(this, offset);
for (int i = 0; i < anchors.Count; i++) {
if (!anchors[i].IsAlive)
anchors[i] = new WeakReference(newAnchor);
}
anchors.Add(new WeakReference(newAnchor));
return newAnchor;
}
void UpdateAnchors(TextChangeEventArgs change)
{
// First update all anchors, then fire the deleted events.
List deletedAnchors = new List();
for (int i = 0; i < anchors.Count; i++) {
var anchor = anchors[i].Target as SimpleAnchor;
if (anchor != null) {
anchor.Update(change);
if (anchor.IsDeleted)
deletedAnchors.Add(i);
}
}
deletedAnchors.Reverse();
foreach (var index in deletedAnchors) {
var anchor = anchors[index].Target as SimpleAnchor;
if (anchor != null)
anchor.RaiseDeletedEvent();
anchors.RemoveAt(index);
}
}
sealed class SimpleAnchor : ITextAnchor
{
readonly StringBuilderDocument document;
int offset;
public SimpleAnchor(StringBuilderDocument document, int offset)
{
this.document = document;
this.offset = offset;
}
public event EventHandler Deleted;
public TextLocation Location {
get {
if (IsDeleted)
throw new InvalidOperationException();
return document.GetLocation(offset);
}
}
public int Offset {
get {
if (IsDeleted)
throw new InvalidOperationException();
return offset;
}
}
public AnchorMovementType MovementType { get; set; }
public bool SurviveDeletion { get; set; }
public bool IsDeleted {
get { return offset < 0; }
}
public void Update(TextChangeEventArgs change)
{
if (SurviveDeletion || offset <= change.Offset || offset >= change.Offset + change.RemovalLength) {
offset = change.GetNewOffset(offset, MovementType);
} else {
offset = -1;
}
}
public void RaiseDeletedEvent()
{
if (Deleted != null)
Deleted(this, EventArgs.Empty);
}
public int Line {
get { return this.Location.Line; }
}
public int Column {
get { return this.Location.Column; }
}
}
#endregion
///
public virtual object GetService(Type serviceType)
{
return null;
}
///
public virtual event EventHandler FileNameChanged { add {} remove {} }
///
public virtual string FileName {
get { return string.Empty; }
}
}
}