129
|
1 /*******************************************************************************
|
|
2 * Copyright (c) 2000, 2008 IBM Corporation and others.
|
|
3 * All rights reserved. This program and the accompanying materials
|
|
4 * are made available under the terms of the Eclipse Public License v1.0
|
|
5 * which accompanies this distribution, and is available at
|
|
6 * http://www.eclipse.org/legal/epl-v10.html
|
|
7 *
|
|
8 * Contributors:
|
|
9 * IBM Corporation - initial API and implementation
|
|
10 * Port to the D programming language:
|
|
11 * Frank Benoit <benoit@tionex.de>
|
|
12 *******************************************************************************/
|
|
13 module dwtx.text.edits.TextEdit;
|
|
14
|
131
|
15 import dwtx.text.edits.MultiTextEdit; // packageimport
|
|
16 import dwtx.text.edits.CopySourceEdit; // packageimport
|
|
17 import dwtx.text.edits.MoveSourceEdit; // packageimport
|
|
18 import dwtx.text.edits.CopyingRangeMarker; // packageimport
|
|
19 import dwtx.text.edits.ReplaceEdit; // packageimport
|
|
20 import dwtx.text.edits.EditDocument; // packageimport
|
|
21 import dwtx.text.edits.UndoCollector; // packageimport
|
|
22 import dwtx.text.edits.DeleteEdit; // packageimport
|
|
23 import dwtx.text.edits.MoveTargetEdit; // packageimport
|
|
24 import dwtx.text.edits.CopyTargetEdit; // packageimport
|
|
25 import dwtx.text.edits.TextEditCopier; // packageimport
|
|
26 import dwtx.text.edits.ISourceModifier; // packageimport
|
|
27 import dwtx.text.edits.TextEditMessages; // packageimport
|
|
28 import dwtx.text.edits.TextEditProcessor; // packageimport
|
|
29 import dwtx.text.edits.MalformedTreeException; // packageimport
|
|
30 import dwtx.text.edits.TreeIterationInfo; // packageimport
|
|
31 import dwtx.text.edits.TextEditVisitor; // packageimport
|
|
32 import dwtx.text.edits.TextEditGroup; // packageimport
|
|
33 import dwtx.text.edits.RangeMarker; // packageimport
|
|
34 import dwtx.text.edits.UndoEdit; // packageimport
|
|
35 import dwtx.text.edits.InsertEdit; // packageimport
|
|
36
|
|
37
|
129
|
38 import dwt.dwthelper.utils;
|
|
39
|
|
40 import java.util.ArrayList;
|
|
41 import java.util.Collections;
|
|
42 import java.util.Comparator;
|
|
43 import java.util.Iterator;
|
|
44 import java.util.List;
|
|
45
|
|
46 import dwtx.core.runtime.Assert;
|
|
47 import dwtx.jface.text.BadLocationException;
|
|
48 import dwtx.jface.text.IDocument;
|
|
49 import dwtx.jface.text.IRegion;
|
|
50 import dwtx.jface.text.Region;
|
|
51
|
|
52
|
|
53 /**
|
|
54 * A text edit describes an elementary text manipulation operation. Edits are
|
|
55 * executed by applying them to a document (e.g. an instance of <code>IDocument
|
|
56 * </code>).
|
|
57 * <p>
|
|
58 * Text edits form a tree. Clients can navigate the tree upwards, from child to
|
|
59 * parent, as well as downwards. Newly created edits are un-parented. New edits
|
|
60 * are added to the tree by calling one of the <code>add</code> methods on a parent
|
|
61 * edit.
|
|
62 * </p>
|
|
63 * <p>
|
|
64 * An edit tree is well formed in the following sense:
|
|
65 * <ul>
|
|
66 * <li>a parent edit covers all its children</li>
|
|
67 * <li>children don't overlap</li>
|
|
68 * <li>an edit with length 0 can't have any children</li>
|
|
69 * </ul>
|
|
70 * Any manipulation of the tree that violates one of the above requirements results
|
|
71 * in a <code>MalformedTreeException</code>.
|
|
72 * </p>
|
|
73 * <p>
|
|
74 * Insert edits are represented by an edit of length 0. If more than one insert
|
|
75 * edit exists at the same offset then the edits are executed in the order in which
|
|
76 * they have been added to a parent. The following code example:
|
|
77 * <pre>
|
|
78 * IDocument document= new Document("org");
|
|
79 * MultiTextEdit edit= new MultiTextEdit();
|
|
80 * edit.addChild(new InsertEdit(0, "www."));
|
|
81 * edit.addChild(new InsertEdit(0, "eclipse."));
|
|
82 * edit.apply(document);
|
|
83 * </pre>
|
|
84 * therefore results in string: "www.eclipse.org".
|
|
85 * </p>
|
|
86 * <p>
|
|
87 * Text edits can be executed in a mode where the edit's region is updated to
|
|
88 * reflect the edit's position in the changed document. Region updating is enabled
|
|
89 * by default or can be requested by passing <code>UPDATE_REGIONS</code> to the
|
|
90 * {@link #apply(IDocument, int) apply(IDocument, int)} method. In the above example
|
|
91 * the region of the <code>InsertEdit(0, "eclipse.")</code> edit after executing
|
|
92 * the root edit is <code>[3, 8]</code>. If the region of an edit got deleted during
|
|
93 * change execution the region is set to <code>[-1, -1]</code> and the method {@link
|
|
94 * #isDeleted() isDeleted} returns <code>true</code>.
|
|
95 * </p>
|
|
96 * This class isn't intended to be subclassed outside of the edit framework. Clients
|
|
97 * are only allowed to subclass <code>MultiTextEdit</code>.
|
|
98 *
|
|
99 * @since 3.0
|
|
100 * @noextend This class is not intended to be subclassed by clients.
|
|
101 */
|
|
102 public abstract class TextEdit {
|
|
103
|
|
104 /**
|
|
105 * Flags indicating that neither <code>CREATE_UNDO</code> nor
|
|
106 * <code>UPDATE_REGIONS</code> is set.
|
|
107 */
|
|
108 public static final int NONE= 0;
|
|
109
|
|
110 /**
|
|
111 * Flags indicating that applying an edit tree to a document
|
|
112 * is supposed to create a corresponding undo edit. If not
|
|
113 * specified <code>null</code> is returned from method <code>
|
|
114 * apply</code>.
|
|
115 */
|
|
116 public static final int CREATE_UNDO= 1 << 0;
|
|
117
|
|
118 /**
|
|
119 * Flag indicating that the edit's region will be updated to
|
|
120 * reflect its position in the changed document. If not specified
|
|
121 * when applying an edit tree to a document the edit's region will
|
|
122 * be arbitrary. It is even not guaranteed that the tree is still
|
|
123 * well formed.
|
|
124 */
|
|
125 public static final int UPDATE_REGIONS= 1 << 1;
|
|
126
|
|
127 private static class InsertionComparator : Comparator {
|
|
128 public int compare(Object o1, Object o2) throws MalformedTreeException {
|
134
|
129 TextEdit edit1= cast(TextEdit)o1;
|
|
130 TextEdit edit2= cast(TextEdit)o2;
|
129
|
131
|
|
132 int offset1= edit1.getOffset();
|
|
133 int length1= edit1.getLength();
|
|
134
|
|
135 int offset2= edit2.getOffset();
|
|
136 int length2= edit2.getLength();
|
|
137
|
|
138 if (offset1 is offset2 && length1 is 0 && length2 is 0) {
|
|
139 return 0;
|
|
140 }
|
|
141 if (offset1 + length1 <= offset2) {
|
|
142 return -1;
|
|
143 }
|
|
144 if (offset2 + length2 <= offset1) {
|
|
145 return 1;
|
|
146 }
|
|
147 throw new MalformedTreeException(
|
|
148 null, edit1,
|
|
149 TextEditMessages.getString("TextEdit.overlapping")); //$NON-NLS-1$
|
|
150 }
|
|
151 }
|
|
152
|
|
153 private static final TextEdit[] EMPTY_ARRAY= new TextEdit[0];
|
|
154 private static final InsertionComparator INSERTION_COMPARATOR= new InsertionComparator();
|
|
155
|
|
156 private static final int DELETED_VALUE= -1;
|
|
157
|
|
158 private int fOffset;
|
|
159 private int fLength;
|
|
160
|
|
161 private TextEdit fParent;
|
|
162 private List fChildren;
|
|
163
|
|
164 int fDelta;
|
|
165
|
|
166 /**
|
|
167 * Create a new text edit. Parent is initialized to <code>
|
|
168 * null<code> and the edit doesn't have any children.
|
|
169 *
|
|
170 * @param offset the edit's offset
|
|
171 * @param length the edit's length
|
|
172 */
|
130
|
173 protected this(int offset, int length) {
|
129
|
174 Assert.isTrue(offset >= 0 && length >= 0);
|
|
175 fOffset= offset;
|
|
176 fLength= length;
|
|
177 fDelta= 0;
|
|
178 }
|
|
179
|
|
180 /**
|
|
181 * Copy constructor
|
|
182 *
|
|
183 * @param source the source to copy form
|
|
184 */
|
130
|
185 protected this(TextEdit source) {
|
129
|
186 fOffset= source.fOffset;
|
|
187 fLength= source.fLength;
|
|
188 fDelta= 0;
|
|
189 }
|
|
190
|
|
191 //---- Region management -----------------------------------------------
|
|
192
|
|
193 /**
|
|
194 * Returns the range that this edit is manipulating. The returned
|
|
195 * <code>IRegion</code> contains the edit's offset and length at
|
|
196 * the point in time when this call is made. Any subsequent changes
|
|
197 * to the edit's offset and length aren't reflected in the returned
|
|
198 * region object.
|
|
199 * <p>
|
|
200 * Creating a region for a deleted edit will result in an assertion
|
|
201 * failure.
|
|
202 *
|
|
203 * @return the manipulated region
|
|
204 */
|
|
205 public final IRegion getRegion() {
|
|
206 return new Region(getOffset(), getLength());
|
|
207 }
|
|
208
|
|
209 /**
|
|
210 * Returns the offset of the edit. An offset is a 0-based
|
|
211 * character index. Returns <code>-1</code> if the edit
|
|
212 * is marked as deleted.
|
|
213 *
|
|
214 * @return the offset of the edit
|
|
215 */
|
|
216 public int getOffset() {
|
|
217 return fOffset;
|
|
218 }
|
|
219
|
|
220 /**
|
|
221 * Returns the length of the edit. Returns <code>-1</code>
|
|
222 * if the edit is marked as deleted.
|
|
223 *
|
|
224 * @return the length of the edit
|
|
225 */
|
|
226 public int getLength() {
|
|
227 return fLength;
|
|
228 }
|
|
229
|
|
230 /**
|
|
231 * Returns the inclusive end position of this edit. The inclusive end
|
|
232 * position denotes the last character of the region manipulated by
|
|
233 * this edit. The returned value is the result of the following
|
|
234 * calculation:
|
|
235 * <pre>
|
|
236 * getOffset() + getLength() - 1;
|
|
237 * <pre>
|
|
238 *
|
|
239 * @return the inclusive end position
|
|
240 */
|
|
241 public final int getInclusiveEnd() {
|
|
242 return getOffset() + getLength() - 1;
|
|
243 }
|
|
244
|
|
245 /**
|
|
246 * Returns the exclusive end position of this edit. The exclusive end
|
|
247 * position denotes the next character of the region manipulated by
|
|
248 * this edit. The returned value is the result of the following
|
|
249 * calculation:
|
|
250 * <pre>
|
|
251 * getOffset() + getLength();
|
|
252 * </pre>
|
|
253 *
|
|
254 * @return the exclusive end position
|
|
255 */
|
|
256 public final int getExclusiveEnd() {
|
|
257 return getOffset() + getLength();
|
|
258 }
|
|
259
|
|
260 /**
|
|
261 * Returns whether this edit has been deleted or not.
|
|
262 *
|
|
263 * @return <code>true</code> if the edit has been
|
|
264 * deleted; otherwise <code>false</code> is returned.
|
|
265 */
|
|
266 public final bool isDeleted() {
|
|
267 return fOffset is DELETED_VALUE && fLength is DELETED_VALUE;
|
|
268 }
|
|
269
|
|
270 /**
|
|
271 * Move all offsets in the tree by the given delta. This node must be a
|
|
272 * root node. The resulting offsets must be greater or equal to zero.
|
|
273 *
|
|
274 * @param delta the delta
|
|
275 * @since 3.1
|
|
276 */
|
|
277 public final void moveTree(int delta) {
|
|
278 Assert.isTrue(fParent is null);
|
|
279 Assert.isTrue(getOffset() + delta >= 0);
|
|
280 internalMoveTree(delta);
|
|
281 }
|
|
282
|
|
283 /**
|
|
284 * Returns <code>true</code> if the edit covers the given edit
|
|
285 * <code>other</code>. It is up to the concrete text edit to
|
|
286 * decide if a edit of length zero can cover another edit.
|
|
287 *
|
|
288 * @param other the other edit
|
|
289 * @return <code>true<code> if the edit covers the other edit;
|
|
290 * otherwise <code>false</code> is returned.
|
|
291 */
|
|
292 public bool covers(TextEdit other) {
|
|
293 if (getLength() is 0 && !canZeroLengthCover())
|
|
294 return false;
|
|
295
|
|
296 if (!other.isDefined())
|
|
297 return true;
|
|
298
|
|
299 int thisOffset= getOffset();
|
|
300 int otherOffset= other.getOffset();
|
|
301 return thisOffset <= otherOffset && otherOffset + other.getLength() <= thisOffset + getLength();
|
|
302 }
|
|
303
|
|
304 /**
|
|
305 * Returns <code>true</code> if an edit with length zero can cover
|
|
306 * another edit. Returns <code>false</code> otherwise.
|
|
307 *
|
|
308 * @return whether an edit of length zero can cover another edit
|
|
309 */
|
|
310 protected bool canZeroLengthCover() {
|
|
311 return false;
|
|
312 }
|
|
313
|
|
314 /**
|
|
315 * Returns whether the region of this edit is defined or not.
|
|
316 *
|
|
317 * @return whether the region of this edit is defined or not
|
|
318 *
|
|
319 * @since 3.1
|
|
320 */
|
|
321 bool isDefined() {
|
|
322 return true;
|
|
323 }
|
|
324
|
|
325 //---- parent and children management -----------------------------
|
|
326
|
|
327 /**
|
|
328 * Returns the edit's parent. The method returns <code>null</code>
|
|
329 * if this edit hasn't been add to another edit.
|
|
330 *
|
|
331 * @return the edit's parent
|
|
332 */
|
|
333 public final TextEdit getParent() {
|
|
334 return fParent;
|
|
335 }
|
|
336
|
|
337 /**
|
|
338 * Returns the root edit of the edit tree.
|
|
339 *
|
|
340 * @return the root edit of the edit tree
|
|
341 * @since 3.1
|
|
342 */
|
|
343 public final TextEdit getRoot() {
|
|
344 TextEdit result= this;
|
|
345 while (result.fParent !is null) {
|
|
346 result= result.fParent;
|
|
347 }
|
|
348 return result;
|
|
349 }
|
|
350
|
|
351 /**
|
|
352 * Adds the given edit <code>child</code> to this edit.
|
|
353 *
|
|
354 * @param child the child edit to add
|
|
355 * @exception MalformedTreeException is thrown if the child
|
|
356 * edit can't be added to this edit. This is the case if the child
|
|
357 * overlaps with one of its siblings or if the child edit's region
|
|
358 * isn't fully covered by this edit.
|
|
359 */
|
|
360 public final void addChild(TextEdit child) throws MalformedTreeException {
|
|
361 internalAdd(child);
|
|
362 }
|
|
363
|
|
364 /**
|
|
365 * Adds all edits in <code>edits</code> to this edit.
|
|
366 *
|
|
367 * @param edits the text edits to add
|
|
368 * @exception MalformedTreeException is thrown if one of
|
|
369 * the given edits can't be added to this edit.
|
|
370 *
|
|
371 * @see #addChild(TextEdit)
|
|
372 */
|
|
373 public final void addChildren(TextEdit[] edits) throws MalformedTreeException {
|
|
374 for (int i= 0; i < edits.length; i++) {
|
|
375 internalAdd(edits[i]);
|
|
376 }
|
|
377 }
|
|
378
|
|
379 /**
|
|
380 * Removes the edit specified by the given index from the list
|
|
381 * of children. Returns the child edit that was removed from
|
|
382 * the list of children. The parent of the returned edit is
|
|
383 * set to <code>null</code>.
|
|
384 *
|
|
385 * @param index the index of the edit to remove
|
|
386 * @return the removed edit
|
|
387 * @exception IndexOutOfBoundsException if the index
|
|
388 * is out of range
|
|
389 */
|
|
390 public final TextEdit removeChild(int index) {
|
|
391 if (fChildren is null)
|
|
392 throw new IndexOutOfBoundsException("Index: " + index + " Size: 0"); //$NON-NLS-1$//$NON-NLS-2$
|
134
|
393 TextEdit result= cast(TextEdit)fChildren.remove(index);
|
129
|
394 result.internalSetParent(null);
|
|
395 if (fChildren.isEmpty())
|
|
396 fChildren= null;
|
|
397 return result;
|
|
398 }
|
|
399
|
|
400 /**
|
|
401 * Removes the first occurrence of the given child from the list
|
|
402 * of children.
|
|
403 *
|
|
404 * @param child the child to be removed
|
|
405 * @return <code>true</code> if the edit contained the given
|
|
406 * child; otherwise <code>false</code> is returned
|
|
407 */
|
|
408 public final bool removeChild(TextEdit child) {
|
|
409 Assert.isNotNull(child);
|
|
410 if (fChildren is null)
|
|
411 return false;
|
|
412 bool result= fChildren.remove(child);
|
|
413 if (result) {
|
|
414 child.internalSetParent(null);
|
|
415 if (fChildren.isEmpty())
|
|
416 fChildren= null;
|
|
417 }
|
|
418 return result;
|
|
419 }
|
|
420
|
|
421 /**
|
|
422 * Removes all child edits from and returns them. The parent
|
|
423 * of the removed edits is set to <code>null</code>.
|
|
424 *
|
|
425 * @return an array of the removed edits
|
|
426 */
|
|
427 public final TextEdit[] removeChildren() {
|
|
428 if (fChildren is null)
|
|
429 return EMPTY_ARRAY;
|
|
430 int size= fChildren.size();
|
|
431 TextEdit[] result= new TextEdit[size];
|
|
432 for (int i= 0; i < size; i++) {
|
134
|
433 result[i]= cast(TextEdit)fChildren.get(i);
|
129
|
434 result[i].internalSetParent(null);
|
|
435 }
|
|
436 fChildren= null;
|
|
437 return result;
|
|
438 }
|
|
439
|
|
440 /**
|
|
441 * Returns <code>true</code> if this edit has children. Otherwise
|
|
442 * <code>false</code> is returned.
|
|
443 *
|
|
444 * @return <code>true</code> if this edit has children; otherwise
|
|
445 * <code>false</code> is returned
|
|
446 */
|
|
447 public final bool hasChildren() {
|
|
448 return fChildren !is null && ! fChildren.isEmpty();
|
|
449 }
|
|
450
|
|
451 /**
|
|
452 * Returns the edit's children. If the edit doesn't have any
|
|
453 * children an empty array is returned.
|
|
454 *
|
|
455 * @return the edit's children
|
|
456 */
|
|
457 public final TextEdit[] getChildren() {
|
|
458 if (fChildren is null)
|
|
459 return EMPTY_ARRAY;
|
|
460 return (TextEdit[])fChildren.toArray(new TextEdit[fChildren.size()]);
|
|
461 }
|
|
462
|
|
463 /**
|
|
464 * Returns the size of the managed children.
|
|
465 *
|
|
466 * @return the size of the children
|
|
467 */
|
|
468 public final int getChildrenSize() {
|
|
469 if (fChildren is null)
|
|
470 return 0;
|
|
471 return fChildren.size();
|
|
472 }
|
|
473
|
|
474 /**
|
|
475 * Returns the text range spawned by the given array of text edits.
|
|
476 * The method requires that the given array contains at least one
|
|
477 * edit. If all edits passed are deleted the method returns <code>
|
|
478 * null</code>.
|
|
479 *
|
|
480 * @param edits an array of edits
|
|
481 * @return the text range spawned by the given array of edits or
|
|
482 * <code>null</code> if all edits are marked as deleted
|
|
483 */
|
|
484 public static IRegion getCoverage(TextEdit[] edits) {
|
|
485 Assert.isTrue(edits !is null && edits.length > 0);
|
|
486
|
|
487 int offset= Integer.MAX_VALUE;
|
|
488 int end= Integer.MIN_VALUE;
|
|
489 int deleted= 0;
|
|
490 for (int i= 0; i < edits.length; i++) {
|
|
491 TextEdit edit= edits[i];
|
|
492 if (edit.isDeleted()) {
|
|
493 deleted++;
|
|
494 } else {
|
|
495 offset= Math.min(offset, edit.getOffset());
|
|
496 end= Math.max(end, edit.getExclusiveEnd());
|
|
497 }
|
|
498 }
|
|
499 if (edits.length is deleted)
|
|
500 return null;
|
|
501
|
|
502 return new Region(offset, end - offset);
|
|
503 }
|
|
504
|
|
505 /*
|
|
506 * Hook called before this edit gets added to the passed
|
|
507 * parent.
|
|
508 */
|
|
509 void aboutToBeAdded(TextEdit parent) {
|
|
510 }
|
|
511
|
|
512 //---- Object methods ------------------------------------------------------
|
|
513
|
|
514 /**
|
|
515 * The <code>Edit</code> implementation of this <code>Object</code>
|
|
516 * method uses object identity (is).
|
|
517 *
|
|
518 * @param obj the other object
|
|
519 * @return <code>true</code> iff <code>this is obj</code>; otherwise
|
|
520 * <code>false</code> is returned
|
|
521 *
|
|
522 * @see Object#equals(java.lang.Object)
|
|
523 */
|
|
524 public final bool equals(Object obj) {
|
|
525 return this is obj; // equivalent to Object.equals
|
|
526 }
|
|
527
|
|
528 /**
|
|
529 * The <code>Edit</code> implementation of this <code>Object</code>
|
|
530 * method calls uses <code>Object#hashCode()</code> to compute its
|
|
531 * hash code.
|
|
532 *
|
|
533 * @return the object's hash code value
|
|
534 *
|
|
535 * @see Object#hashCode()
|
|
536 */
|
|
537 public final int hashCode() {
|
|
538 return super.hashCode();
|
|
539 }
|
|
540
|
|
541 /*
|
|
542 * @see java.lang.Object#toString()
|
|
543 */
|
|
544 public String toString() {
|
|
545 StringBuffer buffer= new StringBuffer();
|
|
546 toStringWithChildren(buffer, 0);
|
|
547 return buffer.toString();
|
|
548 }
|
|
549
|
|
550 /**
|
|
551 * Adds the string representation of this text edit without
|
|
552 * children information to the given string buffer.
|
130
|
553 *
|
129
|
554 * @param buffer the string buffer
|
|
555 * @param indent the indent level in number of spaces
|
|
556 * @since 3.3
|
|
557 */
|
|
558 void internalToString(StringBuffer buffer, int indent) {
|
|
559 for (int i= indent; i > 0; i--) {
|
|
560 buffer.append(" "); //$NON-NLS-1$
|
|
561 }
|
|
562 buffer.append("{"); //$NON-NLS-1$
|
|
563 String name= getClass().getName();
|
|
564 int index= name.lastIndexOf('.');
|
|
565 if (index !is -1) {
|
|
566 buffer.append(name.substring(index + 1));
|
|
567 } else {
|
|
568 buffer.append(name);
|
|
569 }
|
|
570 buffer.append("} "); //$NON-NLS-1$
|
|
571 if (isDeleted()) {
|
|
572 buffer.append("[deleted]"); //$NON-NLS-1$
|
|
573 } else {
|
|
574 buffer.append("["); //$NON-NLS-1$
|
|
575 buffer.append(getOffset());
|
|
576 buffer.append(","); //$NON-NLS-1$
|
|
577 buffer.append(getLength());
|
|
578 buffer.append("]"); //$NON-NLS-1$
|
|
579 }
|
|
580 }
|
|
581
|
|
582 /**
|
130
|
583 * Adds the string representation for this text edit
|
129
|
584 * and its children to the given string buffer.
|
130
|
585 *
|
129
|
586 * @param buffer the string buffer
|
|
587 * @param indent the indent level in number of spaces
|
|
588 * @since 3.3
|
|
589 */
|
|
590 private void toStringWithChildren(StringBuffer buffer, int indent) {
|
|
591 internalToString(buffer, indent);
|
|
592 if (fChildren !is null) {
|
|
593 for (Iterator iterator= fChildren.iterator(); iterator.hasNext();) {
|
134
|
594 TextEdit child= cast(TextEdit) iterator.next();
|
129
|
595 buffer.append('\n');
|
|
596 child.toStringWithChildren(buffer, indent + 1);
|
|
597 }
|
|
598 }
|
|
599 }
|
130
|
600
|
129
|
601 //---- Copying -------------------------------------------------------------
|
|
602
|
|
603 /**
|
|
604 * Creates a deep copy of the edit tree rooted at this
|
|
605 * edit.
|
|
606 *
|
|
607 * @return a deep copy of the edit tree
|
|
608 * @see #doCopy()
|
|
609 */
|
|
610 public final TextEdit copy() {
|
|
611 TextEditCopier copier= new TextEditCopier(this);
|
|
612 return copier.perform();
|
|
613 }
|
|
614
|
|
615 /**
|
|
616 * Creates and returns a copy of this edit. The copy method should be
|
|
617 * implemented in a way so that the copy can executed without causing
|
|
618 * any harm to the original edit. Implementors of this method are
|
|
619 * responsible for creating deep or shallow copies of referenced
|
|
620 * object to fulfill this requirement.
|
|
621 * <p>
|
|
622 * Implementers of this method should use the copy constructor <code>
|
|
623 * Edit#Edit(Edit source) to initialize the edit part of the copy.
|
|
624 * Implementors aren't responsible to actually copy the children or
|
|
625 * to set the right parent.
|
|
626 * <p>
|
|
627 * This method <b>should not be called</b> from outside the framework.
|
|
628 * Please use <code>copy</code> to create a copy of a edit tree.
|
|
629 *
|
|
630 * @return a copy of this edit.
|
|
631 * @see #copy()
|
|
632 * @see #postProcessCopy(TextEditCopier)
|
|
633 * @see TextEditCopier
|
|
634 */
|
|
635 protected abstract TextEdit doCopy();
|
|
636
|
|
637 /**
|
|
638 * This method is called on every edit of the copied tree to do some
|
|
639 * post-processing like connected an edit to a different edit in the tree.
|
|
640 * <p>
|
|
641 * This default implementation does nothing
|
|
642 *
|
|
643 * @param copier the copier that manages a map between original and
|
|
644 * copied edit.
|
|
645 * @see TextEditCopier
|
|
646 */
|
|
647 protected void postProcessCopy(TextEditCopier copier) {
|
|
648 }
|
|
649
|
|
650 //---- Visitor support -------------------------------------------------
|
|
651
|
|
652 /**
|
|
653 * Accepts the given visitor on a visit of the current edit.
|
|
654 *
|
|
655 * @param visitor the visitor object
|
|
656 * @exception IllegalArgumentException if the visitor is null
|
|
657 */
|
|
658 public final void accept(TextEditVisitor visitor) {
|
|
659 Assert.isNotNull(visitor);
|
|
660 // begin with the generic pre-visit
|
|
661 visitor.preVisit(this);
|
|
662 // dynamic dispatch to internal method for type-specific visit/endVisit
|
|
663 accept0(visitor);
|
|
664 // end with the generic post-visit
|
|
665 visitor.postVisit(this);
|
|
666 }
|
|
667
|
|
668 /**
|
|
669 * Accepts the given visitor on a type-specific visit of the current edit.
|
|
670 * This method must be implemented in all concrete text edits.
|
|
671 * <p>
|
|
672 * General template for implementation on each concrete TextEdit class:
|
|
673 * <pre>
|
|
674 * <code>
|
|
675 * bool visitChildren= visitor.visit(this);
|
|
676 * if (visitChildren) {
|
|
677 * acceptChildren(visitor);
|
|
678 * }
|
|
679 * </code>
|
|
680 * </pre>
|
|
681 * Note that the caller (<code>accept</code>) takes care of invoking
|
|
682 * <code>visitor.preVisit(this)</code> and <code>visitor.postVisit(this)</code>.
|
|
683 * </p>
|
|
684 *
|
|
685 * @param visitor the visitor object
|
|
686 */
|
|
687 protected abstract void accept0(TextEditVisitor visitor);
|
|
688
|
|
689
|
|
690 /**
|
|
691 * Accepts the given visitor on the edits children.
|
|
692 * <p>
|
|
693 * This method must be used by the concrete implementations of
|
|
694 * <code>accept</code> to traverse list-values properties; it
|
|
695 * encapsulates the proper handling of on-the-fly changes to the list.
|
|
696 * </p>
|
|
697 *
|
|
698 * @param visitor the visitor object
|
|
699 */
|
|
700 protected final void acceptChildren(TextEditVisitor visitor) {
|
|
701 if (fChildren is null)
|
|
702 return;
|
|
703 Iterator iterator= fChildren.iterator();
|
|
704 while (iterator.hasNext()) {
|
134
|
705 TextEdit curr= cast(TextEdit) iterator.next();
|
129
|
706 curr.accept(visitor);
|
|
707 }
|
|
708 }
|
|
709
|
|
710 //---- Execution -------------------------------------------------------
|
|
711
|
|
712 /**
|
|
713 * Applies the edit tree rooted by this edit to the given document. To check
|
|
714 * if the edit tree can be applied to the document either catch <code>
|
|
715 * MalformedTreeException</code> or use <code>TextEditProcessor</code> to
|
|
716 * execute an edit tree.
|
|
717 *
|
|
718 * @param document the document to be manipulated
|
|
719 * @param style flags controlling the execution of the edit tree. Valid
|
|
720 * flags are: <code>CREATE_UNDO</code> and </code>UPDATE_REGIONS</code>.
|
|
721 * @return a undo edit, if <code>CREATE_UNDO</code> is specified. Otherwise
|
|
722 * <code>null</code> is returned.
|
|
723 *
|
|
724 * @exception MalformedTreeException is thrown if the tree isn't
|
|
725 * in a valid state. This exception is thrown before any edit
|
|
726 * is executed. So the document is still in its original state.
|
|
727 * @exception BadLocationException is thrown if one of the edits
|
|
728 * in the tree can't be executed. The state of the document is
|
|
729 * undefined if this exception is thrown.
|
|
730 *
|
|
731 * @see TextEditProcessor#performEdits()
|
|
732 */
|
|
733 public final UndoEdit apply(IDocument document, int style) throws MalformedTreeException, BadLocationException {
|
|
734 try {
|
|
735 TextEditProcessor processor= new TextEditProcessor(document, this, style);
|
|
736 return processor.performEdits();
|
|
737 } finally {
|
|
738 // disconnect from text edit processor
|
|
739 fParent= null;
|
|
740 }
|
|
741 }
|
|
742
|
|
743 /**
|
|
744 * Applies the edit tree rooted by this edit to the given document. This
|
|
745 * method is a convenience method for <code>apply(document, CREATE_UNDO | UPDATE_REGIONS)
|
|
746 * </code>
|
|
747 *
|
|
748 * @param document the document to which to apply this edit
|
|
749 * @return a undo edit, if <code>CREATE_UNDO</code> is specified. Otherwise
|
|
750 * <code>null</code> is returned.
|
|
751 * @exception MalformedTreeException is thrown if the tree isn't
|
|
752 * in a valid state. This exception is thrown before any edit
|
|
753 * is executed. So the document is still in its original state.
|
|
754 * @exception BadLocationException is thrown if one of the edits
|
|
755 * in the tree can't be executed. The state of the document is
|
|
756 * undefined if this exception is thrown.
|
|
757 * @see #apply(IDocument, int)
|
|
758 */
|
|
759 public final UndoEdit apply(IDocument document) throws MalformedTreeException, BadLocationException {
|
|
760 return apply(document, CREATE_UNDO | UPDATE_REGIONS);
|
|
761 }
|
|
762
|
|
763 UndoEdit dispatchPerformEdits(TextEditProcessor processor) throws BadLocationException {
|
|
764 return processor.executeDo();
|
|
765 }
|
|
766
|
|
767 void dispatchCheckIntegrity(TextEditProcessor processor) throws MalformedTreeException {
|
|
768 processor.checkIntegrityDo();
|
|
769 }
|
|
770
|
|
771 //---- internal state accessors ----------------------------------------------------------
|
|
772
|
|
773 void internalSetParent(TextEdit parent) {
|
|
774 if (parent !is null)
|
|
775 Assert.isTrue(fParent is null);
|
|
776 fParent= parent;
|
|
777 }
|
|
778
|
|
779 void internalSetOffset(int offset) {
|
|
780 Assert.isTrue(offset >= 0);
|
|
781 fOffset= offset;
|
|
782 }
|
|
783
|
|
784 void internalSetLength(int length) {
|
|
785 Assert.isTrue(length >= 0);
|
|
786 fLength= length;
|
|
787 }
|
|
788
|
|
789 List internalGetChildren() {
|
|
790 return fChildren;
|
|
791 }
|
|
792
|
|
793 void internalSetChildren(List children) {
|
|
794 fChildren= children;
|
|
795 }
|
|
796
|
|
797 void internalAdd(TextEdit child) throws MalformedTreeException {
|
|
798 child.aboutToBeAdded(this);
|
|
799 if (child.isDeleted())
|
|
800 throw new MalformedTreeException(this, child, TextEditMessages.getString("TextEdit.deleted_edit")); //$NON-NLS-1$
|
|
801 if (!covers(child))
|
|
802 throw new MalformedTreeException(this, child, TextEditMessages.getString("TextEdit.range_outside")); //$NON-NLS-1$
|
|
803 if (fChildren is null) {
|
|
804 fChildren= new ArrayList(2);
|
|
805 }
|
|
806 int index= computeInsertionIndex(child);
|
|
807 fChildren.add(index, child);
|
|
808 child.internalSetParent(this);
|
|
809 }
|
|
810
|
|
811 private int computeInsertionIndex(TextEdit edit) throws MalformedTreeException {
|
|
812 int size= fChildren.size();
|
|
813 if (size is 0)
|
|
814 return 0;
|
|
815 int lastIndex= size - 1;
|
134
|
816 TextEdit last= cast(TextEdit)fChildren.get(lastIndex);
|
129
|
817 if (last.getExclusiveEnd() <= edit.getOffset())
|
|
818 return size;
|
|
819 try {
|
|
820
|
|
821 int index= Collections.binarySearch(fChildren, edit, INSERTION_COMPARATOR);
|
|
822
|
|
823 if (index < 0)
|
|
824 // edit is not in fChildren
|
|
825 return -index - 1;
|
|
826
|
|
827 // edit is already in fChildren
|
|
828 // make sure that multiple insertion points at the same offset are inserted last.
|
|
829 while (index < lastIndex && INSERTION_COMPARATOR.compare(fChildren.get(index), fChildren.get(index + 1)) is 0)
|
|
830 index++;
|
|
831
|
|
832 return index + 1;
|
|
833
|
|
834 } catch(MalformedTreeException e) {
|
|
835 e.setParent(this);
|
|
836 throw e;
|
|
837 }
|
|
838 }
|
|
839
|
|
840 //---- Offset & Length updating -------------------------------------------------
|
|
841
|
|
842 /**
|
|
843 * Adjusts the edits offset according to the given
|
|
844 * delta. This method doesn't update any children.
|
|
845 *
|
|
846 * @param delta the delta of the text replace operation
|
|
847 */
|
|
848 void adjustOffset(int delta) {
|
|
849 if (isDeleted())
|
|
850 return;
|
|
851 fOffset+= delta;
|
|
852 Assert.isTrue(fOffset >= 0);
|
|
853 }
|
|
854
|
|
855 /**
|
|
856 * Adjusts the edits length according to the given
|
|
857 * delta. This method doesn't update any children.
|
|
858 *
|
|
859 * @param delta the delta of the text replace operation
|
|
860 */
|
|
861 void adjustLength(int delta) {
|
|
862 if (isDeleted())
|
|
863 return;
|
|
864 fLength+= delta;
|
|
865 Assert.isTrue(fLength >= 0);
|
|
866 }
|
|
867
|
|
868 /**
|
|
869 * Marks the edit as deleted. This method doesn't update
|
|
870 * any children.
|
|
871 */
|
|
872 void markAsDeleted() {
|
|
873 fOffset= DELETED_VALUE;
|
|
874 fLength= DELETED_VALUE;
|
|
875 }
|
|
876
|
|
877 //---- Edit processing ----------------------------------------------
|
|
878
|
|
879 /**
|
|
880 * Traverses the edit tree to perform the consistency check.
|
|
881 *
|
|
882 * @param processor the text edit processor
|
|
883 * @param document the document to be manipulated
|
|
884 * @param sourceEdits the list of source edits to be performed before
|
|
885 * the actual tree is applied to the document
|
|
886 *
|
|
887 * @return the number of indirect move or copy target edit children
|
|
888 */
|
|
889 int traverseConsistencyCheck(TextEditProcessor processor, IDocument document, List sourceEdits) {
|
|
890 int result= 0;
|
|
891 if (fChildren !is null) {
|
|
892 for (int i= fChildren.size() - 1; i >= 0; i--) {
|
134
|
893 TextEdit child= cast(TextEdit)fChildren.get(i);
|
129
|
894 result= Math.max(result, child.traverseConsistencyCheck(processor, document, sourceEdits));
|
|
895 }
|
|
896 }
|
|
897 if (processor.considerEdit(this)) {
|
|
898 performConsistencyCheck(processor, document);
|
|
899 }
|
|
900 return result;
|
|
901 }
|
|
902
|
|
903 void performConsistencyCheck(TextEditProcessor processor, IDocument document) {
|
|
904 }
|
|
905
|
|
906 void traverseSourceComputation(TextEditProcessor processor, IDocument document) {
|
|
907 }
|
|
908
|
|
909 void performSourceComputation(TextEditProcessor processor, IDocument document) {
|
|
910 }
|
|
911
|
|
912 int traverseDocumentUpdating(TextEditProcessor processor, IDocument document) throws BadLocationException {
|
|
913 int delta= 0;
|
|
914 if (fChildren !is null) {
|
|
915 for (int i= fChildren.size() - 1; i >= 0; i--) {
|
134
|
916 TextEdit child= cast(TextEdit)fChildren.get(i);
|
129
|
917 delta+= child.traverseDocumentUpdating(processor, document);
|
|
918 childDocumentUpdated();
|
|
919 }
|
|
920 }
|
|
921 if (processor.considerEdit(this)) {
|
|
922 if (delta !is 0)
|
|
923 adjustLength(delta);
|
|
924 int r= performDocumentUpdating(document);
|
|
925 if (r !is 0)
|
|
926 adjustLength(r);
|
|
927 delta+= r;
|
|
928 }
|
|
929 return delta;
|
|
930 }
|
|
931
|
|
932 /**
|
|
933 * Hook method called when the document updating of a child edit has been
|
|
934 * completed. When a client calls {@link #apply(IDocument)} or
|
|
935 * {@link #apply(IDocument, int)} this method is called
|
|
936 * {@link #getChildrenSize()} times.
|
|
937 * <p>
|
|
938 * May be overridden by subclasses of {@link MultiTextEdit}.
|
|
939 *
|
|
940 * @since 3.1
|
|
941 */
|
|
942 protected void childDocumentUpdated() {
|
|
943 }
|
|
944
|
|
945 abstract int performDocumentUpdating(IDocument document) throws BadLocationException;
|
|
946
|
|
947 int traverseRegionUpdating(TextEditProcessor processor, IDocument document, int accumulatedDelta, bool delete) {
|
|
948 performRegionUpdating(accumulatedDelta, delete);
|
|
949 if (fChildren !is null) {
|
|
950 bool childDelete= delete || deleteChildren();
|
|
951 for (Iterator iter= fChildren.iterator(); iter.hasNext();) {
|
134
|
952 TextEdit child= cast(TextEdit)iter.next();
|
129
|
953 accumulatedDelta= child.traverseRegionUpdating(processor, document, accumulatedDelta, childDelete);
|
|
954 childRegionUpdated();
|
|
955 }
|
|
956 }
|
|
957 return accumulatedDelta + fDelta;
|
|
958 }
|
|
959
|
|
960 /**
|
|
961 * Hook method called when the region updating of a child edit has been
|
|
962 * completed. When a client calls {@link #apply(IDocument)} this method is
|
|
963 * called {@link #getChildrenSize()} times. When calling
|
|
964 * {@link #apply(IDocument, int)} this method is called
|
|
965 * {@link #getChildrenSize()} times, when the style parameter contains the
|
|
966 * {@link #UPDATE_REGIONS} flag.
|
|
967 * <p>
|
|
968 * May be overridden by subclasses of {@link MultiTextEdit}.
|
|
969 *
|
|
970 * @since 3.1
|
|
971 */
|
|
972 protected void childRegionUpdated() {
|
|
973 }
|
|
974
|
|
975 void performRegionUpdating(int accumulatedDelta, bool delete) {
|
|
976 if (delete)
|
|
977 markAsDeleted();
|
|
978 else
|
|
979 adjustOffset(accumulatedDelta);
|
|
980 }
|
|
981
|
|
982 abstract bool deleteChildren();
|
|
983
|
|
984 void internalMoveTree(int delta) {
|
|
985 adjustOffset(delta);
|
|
986 if (fChildren !is null) {
|
|
987 for (Iterator iter= fChildren.iterator(); iter.hasNext();) {
|
134
|
988 (cast(TextEdit)iter.next()).internalMoveTree(delta);
|
129
|
989 }
|
|
990 }
|
|
991 }
|
|
992
|
|
993 void deleteTree() {
|
|
994 markAsDeleted();
|
|
995 if (fChildren !is null) {
|
|
996 for (Iterator iter= fChildren.iterator(); iter.hasNext();) {
|
134
|
997 TextEdit child= cast(TextEdit)iter.next();
|
129
|
998 child.deleteTree();
|
|
999 }
|
|
1000 }
|
|
1001 }
|
|
1002 }
|
|
1003
|