From 2e63fcd5bf54035956b2c2974242b1470e1f9868 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Thu, 7 Jun 2012 13:57:37 +0200 Subject: [PATCH] Change TextChangeEventArgs.InsertedText and RemovedText from string to ITextSource. --- .../Document/DocumentChangeEventArgs.cs | 20 ++- .../Document/LineManager.cs | 9 +- .../Document/RopeTextSource.cs | 1 + .../Document/TextDocument.cs | 136 +++++++++++++++--- .../Editor/IDocument.cs | 32 +++++ .../Editor/ReadOnlyDocument.cs | 15 ++ .../Editor/StringBuilderDocument.cs | 26 ++++ .../Editor/StringTextSource.cs | 7 +- .../Editor/TextChangeEventArgs.cs | 30 ++-- 9 files changed, 242 insertions(+), 34 deletions(-) diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs index 431d6ab4db..02cbe5ac71 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs @@ -71,14 +71,24 @@ namespace ICSharpCode.AvalonEdit.Document public DocumentChangeEventArgs(int offset, string removedText, string insertedText, OffsetChangeMap offsetChangeMap) : base(offset, removedText, insertedText) { - ThrowUtil.CheckNotNegative(offset, "offset"); - ThrowUtil.CheckNotNull(removedText, "removedText"); - ThrowUtil.CheckNotNull(insertedText, "insertedText"); - + SetOffsetChangeMap(offsetChangeMap); + } + + /// + /// Creates a new DocumentChangeEventArgs object. + /// + public DocumentChangeEventArgs(int offset, ITextSource removedText, ITextSource insertedText, OffsetChangeMap offsetChangeMap) + : base(offset, removedText, insertedText) + { + SetOffsetChangeMap(offsetChangeMap); + } + + void SetOffsetChangeMap(OffsetChangeMap offsetChangeMap) + { if (offsetChangeMap != null) { if (!offsetChangeMap.IsFrozen) throw new ArgumentException("The OffsetChangeMap must be frozen before it can be used in DocumentChangeEventArgs"); - if (!offsetChangeMap.IsValidForDocumentChange(offset, removedText.Length, insertedText.Length)) + if (!offsetChangeMap.IsValidForDocumentChange(this.Offset, this.RemovalLength, this.InsertionLength)) throw new ArgumentException("OffsetChangeMap is not valid for this document change", "offsetChangeMap"); this.offsetChangeMap = offsetChangeMap; } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineManager.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineManager.cs index 0325fef44b..3f05979544 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineManager.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using ICSharpCode.NRefactory.Editor; namespace ICSharpCode.AvalonEdit.Document { @@ -181,7 +182,7 @@ namespace ICSharpCode.AvalonEdit.Document #endregion #region Insert - public void Insert(int offset, string text) + public void Insert(int offset, ITextSource text) { DocumentLine line = documentLineTree.GetByOffset(offset); int lineOffset = line.Offset; @@ -202,7 +203,7 @@ namespace ICSharpCode.AvalonEdit.Document if (ds == SimpleSegment.Invalid) { // no newline is being inserted, all text is inserted in a single line //line.InsertedLinePart(offset - line.Offset, text.Length); - SetLineLength(line, line.TotalLength + text.Length); + SetLineLength(line, line.TotalLength + text.TextLength); return; } //DocumentLine firstLine = line; @@ -224,9 +225,9 @@ namespace ICSharpCode.AvalonEdit.Document } //firstLine.SplitTo(line); // insert rest after last delimiter - if (lastDelimiterEnd != text.Length) { + if (lastDelimiterEnd != text.TextLength) { //line.InsertedLinePart(0, text.Length - lastDelimiterEnd); - SetLineLength(line, line.TotalLength + text.Length - lastDelimiterEnd); + SetLineLength(line, line.TotalLength + text.TextLength - lastDelimiterEnd); } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/RopeTextSource.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/RopeTextSource.cs index fdc1935066..a33e1cf19d 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/RopeTextSource.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/RopeTextSource.cs @@ -11,6 +11,7 @@ namespace ICSharpCode.AvalonEdit.Document /// /// Implements the ITextSource interface using a rope. /// + [Serializable] public sealed class RopeTextSource : ITextSource { readonly Rope rope; diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs index d2e41154a6..454124df12 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs @@ -524,6 +524,21 @@ namespace ICSharpCode.AvalonEdit.Document /// 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); } @@ -539,6 +554,25 @@ namespace ICSharpCode.AvalonEdit.Document /// 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); @@ -562,7 +596,7 @@ namespace ICSharpCode.AvalonEdit.Document /// Length of the text to be removed. public void Remove(int offset, int length) { - Replace(offset, length, string.Empty); + Replace(offset, length, StringTextSource.Empty); } internal bool inDocumentChanging; @@ -571,6 +605,16 @@ namespace ICSharpCode.AvalonEdit.Document /// 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"); @@ -584,6 +628,17 @@ namespace ICSharpCode.AvalonEdit.Document /// 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); } @@ -597,6 +652,19 @@ namespace ICSharpCode.AvalonEdit.Document /// 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"); @@ -607,33 +675,33 @@ namespace ICSharpCode.AvalonEdit.Document break; case OffsetChangeMappingType.KeepAnchorBeforeInsertion: Replace(offset, length, text, OffsetChangeMap.FromSingleElement( - new OffsetChangeMapEntry(offset, length, text.Length, false, true))); + new OffsetChangeMapEntry(offset, length, text.TextLength, false, true))); break; case OffsetChangeMappingType.RemoveAndInsert: - if (length == 0 || text.Length == 0) { + 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.Length)); + map.Add(new OffsetChangeMapEntry(offset, 0, text.TextLength)); map.Freeze(); Replace(offset, length, text, map); } break; case OffsetChangeMappingType.CharacterReplace: - if (length == 0 || text.Length == 0) { + 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.Length > length) { + } 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.Length - length); + OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.TextLength - length); Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry)); - } else if (text.Length < length) { - OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.Length, length - text.Length, 0, true, false); + } 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); @@ -660,10 +728,30 @@ namespace ICSharpCode.AvalonEdit.Document /// 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(); @@ -687,18 +775,26 @@ namespace ICSharpCode.AvalonEdit.Document } } - void DoReplace(int offset, int length, string newText, OffsetChangeMap offsetChangeMap) + void DoReplace(int offset, int length, ITextSource newText, OffsetChangeMap offsetChangeMap) { - if (length == 0 && newText.Length == 0) + 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.Length == 1 && offsetChangeMap == null) + if (length == 1 && newText.TextLength == 1 && offsetChangeMap == null) offsetChangeMap = OffsetChangeMap.Empty; - string removedText = rope.ToString(offset, length); + 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 @@ -721,7 +817,11 @@ namespace ICSharpCode.AvalonEdit.Document if (offset == 0 && length == rope.Length) { // optimize replacing the whole document rope.Clear(); - rope.InsertText(0, newText); + 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); @@ -729,7 +829,11 @@ namespace ICSharpCode.AvalonEdit.Document #if DEBUG lineTree.CheckProperties(); #endif - rope.InsertText(offset, newText); + 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(); diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/IDocument.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/IDocument.cs index 4201ce4484..f9a793b911 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/IDocument.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/IDocument.cs @@ -109,6 +109,18 @@ namespace ICSharpCode.NRefactory.Editor /// void Insert(int offset, string text); + /// + /// 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. + /// + void Insert(int offset, ITextSource text); + /// /// Inserts text. /// @@ -121,6 +133,18 @@ namespace ICSharpCode.NRefactory.Editor /// void Insert(int offset, string text, AnchorMovementType defaultAnchorMovementType); + /// + /// 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. + /// + void Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType); + /// /// Removes text. /// @@ -136,6 +160,14 @@ namespace ICSharpCode.NRefactory.Editor /// The new text. void Replace(int offset, int length, string newText); + /// + /// Replaces text. + /// + /// The starting offset of the text to be replaced. + /// The length of the text to be replaced. + /// The new text. + void Replace(int offset, int length, ITextSource newText); + /// /// Make the document combine the following actions into a single /// action for undo purposes. diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/ReadOnlyDocument.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/ReadOnlyDocument.cs index a4d76a8394..2a36ccda33 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/ReadOnlyDocument.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/ReadOnlyDocument.cs @@ -257,6 +257,21 @@ namespace ICSharpCode.NRefactory.Editor throw new NotSupportedException(); } + void IDocument.Insert(int offset, ITextSource text) + { + throw new NotImplementedException(); + } + + void IDocument.Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType) + { + throw new NotImplementedException(); + } + + void IDocument.Replace(int offset, int length, ITextSource newText) + { + throw new NotImplementedException(); + } + void IDocument.StartUndoableAction() { } diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringBuilderDocument.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringBuilderDocument.cs index 79553a4136..fad9dfcc5b 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringBuilderDocument.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringBuilderDocument.cs @@ -104,17 +104,35 @@ namespace ICSharpCode.NRefactory.Editor 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 { @@ -149,6 +167,14 @@ namespace ICSharpCode.NRefactory.Editor 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) diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringTextSource.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringTextSource.cs index 3a24db3646..36780954bd 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringTextSource.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/StringTextSource.cs @@ -27,6 +27,11 @@ namespace ICSharpCode.NRefactory.Editor [Serializable] public class StringTextSource : ITextSource { + /// + /// Gets a text source containing the empty string. + /// + public static readonly StringTextSource Empty = new StringTextSource(string.Empty); + readonly string text; readonly ITextSourceVersion version; @@ -69,7 +74,7 @@ namespace ICSharpCode.NRefactory.Editor /// public ITextSource CreateSnapshot() { - return this; // StringTextBuffer is immutable + return this; // StringTextSource is immutable } /// diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/TextChangeEventArgs.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/TextChangeEventArgs.cs index 936de37aa4..f1eeb4d27f 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/TextChangeEventArgs.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory/Editor/TextChangeEventArgs.cs @@ -28,8 +28,8 @@ namespace ICSharpCode.NRefactory.Editor public class TextChangeEventArgs : EventArgs { readonly int offset; - readonly string removedText; - readonly string insertedText; + readonly ITextSource removedText; + readonly ITextSource insertedText; /// /// The offset at which the change occurs. @@ -41,7 +41,7 @@ namespace ICSharpCode.NRefactory.Editor /// /// The text that was removed. /// - public string RemovedText { + public ITextSource RemovedText { get { return removedText; } } @@ -49,13 +49,13 @@ namespace ICSharpCode.NRefactory.Editor /// The number of characters removed. /// public int RemovalLength { - get { return removedText.Length; } + get { return removedText.TextLength; } } /// /// The text that was inserted. /// - public string InsertedText { + public ITextSource InsertedText { get { return insertedText; } } @@ -63,7 +63,7 @@ namespace ICSharpCode.NRefactory.Editor /// The number of characters inserted. /// public int InsertionLength { - get { return insertedText.Length; } + get { return insertedText.TextLength; } } /// @@ -71,9 +71,23 @@ namespace ICSharpCode.NRefactory.Editor /// public TextChangeEventArgs(int offset, string removedText, string insertedText) { + if (offset < 0) + throw new ArgumentOutOfRangeException("offset", offset, "offset must not be negative"); this.offset = offset; - this.removedText = removedText ?? string.Empty; - this.insertedText = insertedText ?? string.Empty; + this.removedText = removedText != null ? new StringTextSource(removedText) : StringTextSource.Empty; + this.insertedText = insertedText != null ? new StringTextSource(insertedText) : StringTextSource.Empty; + } + + /// + /// Creates a new TextChangeEventArgs object. + /// + public TextChangeEventArgs(int offset, ITextSource removedText, ITextSource insertedText) + { + if (offset < 0) + throw new ArgumentOutOfRangeException("offset", offset, "offset must not be negative"); + this.offset = offset; + this.removedText = removedText ?? StringTextSource.Empty; + this.insertedText = insertedText ?? StringTextSource.Empty; } ///