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