Free cookie consent management tool by TermsFeed Policy Generator

source: branches/RemoveBackwardsCompatibility/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Document/OffsetChangeMap.cs @ 18242

Last change on this file since 18242 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 12.7 KB
Line 
1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Collections.Generic;
21using System.Collections.ObjectModel;
22using System.Diagnostics.CodeAnalysis;
23using ICSharpCode.AvalonEdit.Utils;
24#if NREFACTORY
25using ICSharpCode.NRefactory.Editor;
26#endif
27
28namespace ICSharpCode.AvalonEdit.Document
29{
30  /// <summary>
31  /// Contains predefined offset change mapping types.
32  /// </summary>
33  public enum OffsetChangeMappingType
34  {
35    /// <summary>
36    /// Normal replace.
37    /// Anchors in front of the replaced region will stay in front, anchors after the replaced region will stay after.
38    /// Anchors in the middle of the removed region will be deleted. If they survive deletion,
39    /// they move depending on their AnchorMovementType.
40    /// </summary>
41    /// <remarks>
42    /// This is the default implementation of DocumentChangeEventArgs when OffsetChangeMap is null,
43    /// so using this option usually works without creating an OffsetChangeMap instance.
44    /// This is equivalent to an OffsetChangeMap with a single entry describing the replace operation.
45    /// </remarks>
46    Normal,
47    /// <summary>
48    /// First the old text is removed, then the new text is inserted.
49    /// Anchors immediately in front (or after) the replaced region may move to the other side of the insertion,
50    /// depending on the AnchorMovementType.
51    /// </summary>
52    /// <remarks>
53    /// This is implemented as an OffsetChangeMap with two entries: the removal, and the insertion.
54    /// </remarks>
55    RemoveAndInsert,
56    /// <summary>
57    /// The text is replaced character-by-character.
58    /// Anchors keep their position inside the replaced text.
59    /// Anchors after the replaced region will move accordingly if the replacement text has a different length than the replaced text.
60    /// If the new text is shorter than the old text, anchors inside the old text that would end up behind the replacement text
61    /// will be moved so that they point to the end of the replacement text.
62    /// </summary>
63    /// <remarks>
64    /// On the OffsetChangeMap level, growing text is implemented by replacing the last character in the replaced text
65    /// with itself and the additional text segment. A simple insertion of the additional text would have the undesired
66    /// effect of moving anchors immediately after the replaced text into the replacement text if they used
67    /// AnchorMovementStyle.BeforeInsertion.
68    /// Shrinking text is implemented by removing the text segment that's too long; but in a special mode that
69    /// causes anchors to always survive irrespective of their <see cref="TextAnchor.SurviveDeletion"/> setting.
70    /// If the text keeps its old size, this is implemented as OffsetChangeMap.Empty.
71    /// </remarks>
72    CharacterReplace,
73    /// <summary>
74    /// Like 'Normal', but anchors with <see cref="TextAnchor.MovementType"/> = Default will stay in front of the
75    /// insertion instead of being moved behind it.
76    /// </summary>
77    KeepAnchorBeforeInsertion
78  }
79 
80  /// <summary>
81  /// Describes a series of offset changes.
82  /// </summary>
83  [Serializable]
84  [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
85                   Justification="It's a mapping old offsets -> new offsets")]
86  public sealed class OffsetChangeMap : Collection<OffsetChangeMapEntry>
87  {
88    /// <summary>
89    /// Immutable OffsetChangeMap that is empty.
90    /// </summary>
91    [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes",
92                     Justification="The Empty instance is immutable")]
93    public static readonly OffsetChangeMap Empty = new OffsetChangeMap(Empty<OffsetChangeMapEntry>.Array, true);
94   
95    /// <summary>
96    /// Creates a new OffsetChangeMap with a single element.
97    /// </summary>
98    /// <param name="entry">The entry.</param>
99    /// <returns>Returns a frozen OffsetChangeMap with a single entry.</returns>
100    public static OffsetChangeMap FromSingleElement(OffsetChangeMapEntry entry)
101    {
102      return new OffsetChangeMap(new OffsetChangeMapEntry[] { entry }, true);
103    }
104   
105    bool isFrozen;
106   
107    /// <summary>
108    /// Creates a new OffsetChangeMap instance.
109    /// </summary>
110    public OffsetChangeMap()
111    {
112    }
113   
114    internal OffsetChangeMap(int capacity)
115      : base(new List<OffsetChangeMapEntry>(capacity))
116    {
117    }
118   
119    private OffsetChangeMap(IList<OffsetChangeMapEntry> entries, bool isFrozen)
120      : base(entries)
121    {
122      this.isFrozen = isFrozen;
123    }
124   
125    /// <summary>
126    /// Gets the new offset where the specified offset moves after this document change.
127    /// </summary>
128    public int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default)
129    {
130      IList<OffsetChangeMapEntry> items = this.Items;
131      int count = items.Count;
132      for (int i = 0; i < count; i++) {
133        offset = items[i].GetNewOffset(offset, movementType);
134      }
135      return offset;
136    }
137   
138    /// <summary>
139    /// Gets whether this OffsetChangeMap is a valid explanation for the specified document change.
140    /// </summary>
141    public bool IsValidForDocumentChange(int offset, int removalLength, int insertionLength)
142    {
143      int endOffset = offset + removalLength;
144      foreach (OffsetChangeMapEntry entry in this) {
145        // check that ChangeMapEntry is in valid range for this document change
146        if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset)
147          return false;
148        endOffset += entry.InsertionLength - entry.RemovalLength;
149      }
150      // check that the total delta matches
151      return endOffset == offset + insertionLength;
152    }
153   
154    /// <summary>
155    /// Calculates the inverted OffsetChangeMap (used for the undo operation).
156    /// </summary>
157    public OffsetChangeMap Invert()
158    {
159      if (this == Empty)
160        return this;
161      OffsetChangeMap newMap = new OffsetChangeMap(this.Count);
162      for (int i = this.Count - 1; i >= 0; i--) {
163        OffsetChangeMapEntry entry = this[i];
164        // swap InsertionLength and RemovalLength
165        newMap.Add(new OffsetChangeMapEntry(entry.Offset, entry.InsertionLength, entry.RemovalLength));
166      }
167      return newMap;
168    }
169   
170    /// <inheritdoc/>
171    protected override void ClearItems()
172    {
173      CheckFrozen();
174      base.ClearItems();
175    }
176   
177    /// <inheritdoc/>
178    protected override void InsertItem(int index, OffsetChangeMapEntry item)
179    {
180      CheckFrozen();
181      base.InsertItem(index, item);
182    }
183   
184    /// <inheritdoc/>
185    protected override void RemoveItem(int index)
186    {
187      CheckFrozen();
188      base.RemoveItem(index);
189    }
190   
191    /// <inheritdoc/>
192    protected override void SetItem(int index, OffsetChangeMapEntry item)
193    {
194      CheckFrozen();
195      base.SetItem(index, item);
196    }
197   
198    void CheckFrozen()
199    {
200      if (isFrozen)
201        throw new InvalidOperationException("This instance is frozen and cannot be modified.");
202    }
203   
204    /// <summary>
205    /// Gets if this instance is frozen. Frozen instances are immutable and thus thread-safe.
206    /// </summary>
207    public bool IsFrozen {
208      get { return isFrozen; }
209    }
210   
211    /// <summary>
212    /// Freezes this instance.
213    /// </summary>
214    public void Freeze()
215    {
216      isFrozen = true;
217    }
218  }
219 
220  /// <summary>
221  /// An entry in the OffsetChangeMap.
222  /// This represents the offset of a document change (either insertion or removal, not both at once).
223  /// </summary>
224  [Serializable]
225  public struct OffsetChangeMapEntry : IEquatable<OffsetChangeMapEntry>
226  {
227    readonly int offset;
228   
229    // MSB: DefaultAnchorMovementIsBeforeInsertion
230    readonly uint insertionLengthWithMovementFlag;
231   
232    // MSB: RemovalNeverCausesAnchorDeletion; other 31 bits: RemovalLength
233    readonly uint removalLengthWithDeletionFlag;
234   
235    /// <summary>
236    /// The offset at which the change occurs.
237    /// </summary>
238    public int Offset {
239      get { return offset; }
240    }
241   
242    /// <summary>
243    /// The number of characters inserted.
244    /// Returns 0 if this entry represents a removal.
245    /// </summary>
246    public int InsertionLength {
247      get { return (int)(insertionLengthWithMovementFlag & 0x7fffffff); }
248    }
249   
250    /// <summary>
251    /// The number of characters removed.
252    /// Returns 0 if this entry represents an insertion.
253    /// </summary>
254    public int RemovalLength {
255      get { return (int)(removalLengthWithDeletionFlag & 0x7fffffff); }
256    }
257   
258    /// <summary>
259    /// Gets whether the removal should not cause any anchor deletions.
260    /// </summary>
261    public bool RemovalNeverCausesAnchorDeletion {
262      get { return (removalLengthWithDeletionFlag & 0x80000000) != 0; }
263    }
264   
265    /// <summary>
266    /// Gets whether default anchor movement causes the anchor to stay in front of the caret.
267    /// </summary>
268    public bool DefaultAnchorMovementIsBeforeInsertion {
269      get { return (insertionLengthWithMovementFlag & 0x80000000) != 0; }
270    }
271   
272    /// <summary>
273    /// Gets the new offset where the specified offset moves after this document change.
274    /// </summary>
275    public int GetNewOffset(int oldOffset, AnchorMovementType movementType = AnchorMovementType.Default)
276    {
277      int insertionLength = this.InsertionLength;
278      int removalLength = this.RemovalLength;
279      if (!(removalLength == 0 && oldOffset == offset)) {
280        // we're getting trouble (both if statements in here would apply)
281        // if there's no removal and we insert at the offset
282        // -> we'd need to disambiguate by movementType, which is handled after the if
283       
284        // offset is before start of change: no movement
285        if (oldOffset <= offset)
286          return oldOffset;
287        // offset is after end of change: movement by normal delta
288        if (oldOffset >= offset + removalLength)
289          return oldOffset + insertionLength - removalLength;
290      }
291      // we reach this point if
292      // a) the oldOffset is inside the deleted segment
293      // b) there was no removal and we insert at the caret position
294      if (movementType == AnchorMovementType.AfterInsertion)
295        return offset + insertionLength;
296      else if (movementType == AnchorMovementType.BeforeInsertion)
297        return offset;
298      else
299        return this.DefaultAnchorMovementIsBeforeInsertion ? offset : offset + insertionLength;
300    }
301   
302    /// <summary>
303    /// Creates a new OffsetChangeMapEntry instance.
304    /// </summary>
305    public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength)
306    {
307      ThrowUtil.CheckNotNegative(offset, "offset");
308      ThrowUtil.CheckNotNegative(removalLength, "removalLength");
309      ThrowUtil.CheckNotNegative(insertionLength, "insertionLength");
310     
311      this.offset = offset;
312      this.removalLengthWithDeletionFlag = (uint)removalLength;
313      this.insertionLengthWithMovementFlag = (uint)insertionLength;
314    }
315   
316    /// <summary>
317    /// Creates a new OffsetChangeMapEntry instance.
318    /// </summary>
319    public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength, bool removalNeverCausesAnchorDeletion, bool defaultAnchorMovementIsBeforeInsertion)
320      : this(offset, removalLength, insertionLength)
321    {
322      if (removalNeverCausesAnchorDeletion)
323        this.removalLengthWithDeletionFlag |= 0x80000000;
324      if (defaultAnchorMovementIsBeforeInsertion)
325        this.insertionLengthWithMovementFlag |= 0x80000000;
326    }
327   
328    /// <inheritdoc/>
329    public override int GetHashCode()
330    {
331      unchecked {
332        return offset + 3559 * (int)insertionLengthWithMovementFlag + 3571 * (int)removalLengthWithDeletionFlag;
333      }
334    }
335   
336    /// <inheritdoc/>
337    public override bool Equals(object obj)
338    {
339      return obj is OffsetChangeMapEntry && this.Equals((OffsetChangeMapEntry)obj);
340    }
341   
342    /// <inheritdoc/>
343    public bool Equals(OffsetChangeMapEntry other)
344    {
345      return offset == other.offset && insertionLengthWithMovementFlag == other.insertionLengthWithMovementFlag && removalLengthWithDeletionFlag == other.removalLengthWithDeletionFlag;
346    }
347   
348    /// <summary>
349    /// Tests the two entries for equality.
350    /// </summary>
351    public static bool operator ==(OffsetChangeMapEntry left, OffsetChangeMapEntry right)
352    {
353      return left.Equals(right);
354    }
355   
356    /// <summary>
357    /// Tests the two entries for inequality.
358    /// </summary>
359    public static bool operator !=(OffsetChangeMapEntry left, OffsetChangeMapEntry right)
360    {
361      return !left.Equals(right);
362    }
363  }
364}
Note: See TracBrowser for help on using the repository browser.