mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
494 lines
13 KiB
494 lines
13 KiB
// 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.Decompiler.CSharp.Syntax; |
|
using ICSharpCode.Decompiler.Util; |
|
|
|
namespace ICSharpCode.NRefactory.Editor |
|
{ |
|
/// <summary> |
|
/// Document based on a string builder. |
|
/// This class serves as a reference implementation for the IDocument interface. |
|
/// </summary> |
|
public class StringBuilderDocument : IDocument |
|
{ |
|
readonly StringBuilder b; |
|
readonly TextSourceVersionProvider versionProvider = new TextSourceVersionProvider(); |
|
|
|
/// <summary> |
|
/// Creates a new StringBuilderDocument. |
|
/// </summary> |
|
public StringBuilderDocument() |
|
{ |
|
b = new StringBuilder(); |
|
} |
|
|
|
/// <summary> |
|
/// Creates a new StringBuilderDocument with the specified initial text. |
|
/// </summary> |
|
public StringBuilderDocument(string text) |
|
{ |
|
if (text == null) |
|
throw new ArgumentNullException("text"); |
|
b = new StringBuilder(text); |
|
} |
|
|
|
/// <summary> |
|
/// Creates a new StringBuilderDocument with the initial text copied from the specified text source. |
|
/// </summary> |
|
public StringBuilderDocument(ITextSource textSource) |
|
{ |
|
if (textSource == null) |
|
throw new ArgumentNullException("textSource"); |
|
b = new StringBuilder(textSource.TextLength); |
|
textSource.WriteTextTo(new StringWriter(b)); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public event EventHandler<TextChangeEventArgs> TextChanging; |
|
|
|
/// <inheritdoc/> |
|
public event EventHandler<TextChangeEventArgs> TextChanged; |
|
|
|
/// <inheritdoc/> |
|
public event EventHandler ChangeCompleted; |
|
|
|
/// <inheritdoc/> |
|
public ITextSourceVersion Version { |
|
get { return versionProvider.CurrentVersion; } |
|
} |
|
|
|
#region Line<->Offset |
|
/// <inheritdoc/> |
|
public int LineCount { |
|
get { return CreateDocumentSnapshot().LineCount; } |
|
} |
|
|
|
/// <inheritdoc/> |
|
public IDocumentLine GetLineByNumber(int lineNumber) |
|
{ |
|
return CreateDocumentSnapshot().GetLineByNumber(lineNumber); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public IDocumentLine GetLineByOffset(int offset) |
|
{ |
|
return CreateDocumentSnapshot().GetLineByOffset(offset); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int GetOffset(int line, int column) |
|
{ |
|
return CreateDocumentSnapshot().GetOffset(line, column); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int GetOffset(TextLocation location) |
|
{ |
|
return CreateDocumentSnapshot().GetOffset(location); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public TextLocation GetLocation(int offset) |
|
{ |
|
return CreateDocumentSnapshot().GetLocation(offset); |
|
} |
|
#endregion |
|
|
|
#region Insert/Remove/Replace |
|
/// <inheritdoc/> |
|
public void Insert(int offset, string text) |
|
{ |
|
Replace(offset, 0, text); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void Insert(int offset, ITextSource text) |
|
{ |
|
if (text == null) |
|
throw new ArgumentNullException("text"); |
|
Replace(offset, 0, text.Text); |
|
} |
|
|
|
/// <inheritdoc/> |
|
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); |
|
} |
|
|
|
/// <inheritdoc/> |
|
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); |
|
} |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void Remove(int offset, int length) |
|
{ |
|
Replace(offset, length, string.Empty); |
|
} |
|
|
|
/// <inheritdoc/> |
|
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)); |
|
} |
|
|
|
/// <inheritdoc/> |
|
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; |
|
|
|
/// <inheritdoc/> |
|
public void StartUndoableAction() |
|
{ |
|
// prevent changes from within the TextChanging/TextChanged event handlers |
|
if (isInChange) |
|
throw new InvalidOperationException(); |
|
undoGroupNesting++; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void EndUndoableAction() |
|
{ |
|
undoGroupNesting--; |
|
if (undoGroupNesting == 0) { |
|
if (ChangeCompleted != null) |
|
ChangeCompleted(this, EventArgs.Empty); |
|
} |
|
} |
|
|
|
/// <inheritdoc/> |
|
public IDisposable OpenUndoGroup() |
|
{ |
|
StartUndoableAction(); |
|
return new CallbackOnDispose(EndUndoableAction); |
|
} |
|
#endregion |
|
|
|
#region CreateSnapshot/CreateReader |
|
ReadOnlyDocument documentSnapshot; |
|
|
|
/// <inheritdoc/> |
|
public IDocument CreateDocumentSnapshot() |
|
{ |
|
if (documentSnapshot == null) |
|
documentSnapshot = new ReadOnlyDocument(this, this.FileName); |
|
return documentSnapshot; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public ITextSource CreateSnapshot() |
|
{ |
|
return new StringTextSource(this.Text, versionProvider.CurrentVersion); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public ITextSource CreateSnapshot(int offset, int length) |
|
{ |
|
return new StringTextSource(GetText(offset, length)); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public TextReader CreateReader() |
|
{ |
|
return new StringReader(this.Text); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public TextReader CreateReader(int offset, int length) |
|
{ |
|
return new StringReader(GetText(offset, length)); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void WriteTextTo(TextWriter writer) |
|
{ |
|
if (writer == null) |
|
throw new ArgumentNullException("writer"); |
|
writer.Write(this.Text); |
|
} |
|
|
|
/// <inheritdoc/> |
|
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; |
|
|
|
/// <inheritdoc/> |
|
public string Text { |
|
get { |
|
if (cachedText == null) |
|
cachedText = b.ToString(); |
|
return cachedText; |
|
} |
|
set { |
|
Replace(0, b.Length, value); |
|
} |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int TextLength { |
|
get { return b.Length; } |
|
} |
|
|
|
/// <inheritdoc/> |
|
public char GetCharAt(int offset) |
|
{ |
|
return b[offset]; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public string GetText(int offset, int length) |
|
{ |
|
return b.ToString(offset, length); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public string GetText(ISegment segment) |
|
{ |
|
if (segment == null) |
|
throw new ArgumentNullException("segment"); |
|
return b.ToString(segment.Offset, segment.Length); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int IndexOf(char c, int startIndex, int count) |
|
{ |
|
return this.Text.IndexOf(c, startIndex, count); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int IndexOfAny(char[] anyOf, int startIndex, int count) |
|
{ |
|
return this.Text.IndexOfAny(anyOf, startIndex, count); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
|
{ |
|
return this.Text.IndexOf(searchText, startIndex, count, comparisonType); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public int LastIndexOf(char c, int startIndex, int count) |
|
{ |
|
return this.Text.LastIndexOf(c, startIndex + count - 1, count); |
|
} |
|
|
|
/// <inheritdoc/> |
|
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<WeakReference> anchors = new List<WeakReference>(); |
|
|
|
/// <inheritdoc/> |
|
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<int> deletedAnchors = new List<int>(); |
|
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 |
|
|
|
/// <inheritdoc/> |
|
public virtual object GetService(Type serviceType) |
|
{ |
|
return null; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public virtual event EventHandler FileNameChanged { add {} remove {} } |
|
|
|
/// <inheritdoc/> |
|
public virtual string FileName { |
|
get { return string.Empty; } |
|
} |
|
} |
|
}
|
|
|