1 /*******************************************************************************
2  * Copyright (c) 2000, 2015 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.text.edits;
15 
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 
23 import org.eclipse.core.runtime.Assert;
24 
25 import org.eclipse.jface.text.BadLocationException;
26 import org.eclipse.jface.text.IDocument;
27 import org.eclipse.jface.text.IRegion;
28 import org.eclipse.jface.text.Region;
29 
30 /**
31  * A move source edit denotes the source of a move operation. Move
32  * source edits are only valid inside an edit tree if they have a
33  * corresponding target edit. Furthermore the corresponding target
34  * edit can't be a direct or indirect child of the source edit.
35  * Violating one of two requirements will result in a <code>
36  * MalformedTreeException</code> when executing the edit tree.
37  * <p>
38  * A move source edit can manage an optional source modifier. A
39  * source modifier can provide a set of replace edits which will
40  * to applied to the source before it gets inserted at the target
41  * position.
42  *
43  * @see org.eclipse.text.edits.MoveTargetEdit
44  * @see org.eclipse.text.edits.CopySourceEdit
45  *
46  * @since 3.0
47  */
48 public final class MoveSourceEdit extends TextEdit {
49 
50 	private MoveTargetEdit fTarget;
51 	private ISourceModifier fModifier;
52 
53 	private String fSourceContent;
54 	private MultiTextEdit fSourceRoot;
55 
56 	/**
57 	 * Constructs a new move source edit.
58 	 *
59 	 * @param offset the edit's offset
60 	 * @param length the edit's length
61 	 */
MoveSourceEdit(int offset, int length)62 	public MoveSourceEdit(int offset, int length) {
63 		super(offset, length);
64 	}
65 
66 	/**
67 	 * Constructs a new copy source edit.
68 	 *
69 	 * @param offset the edit's offset
70 	 * @param length the edit's length
71 	 * @param target the edit's target
72 	 */
MoveSourceEdit(int offset, int length, MoveTargetEdit target)73 	public MoveSourceEdit(int offset, int length, MoveTargetEdit target) {
74 		this(offset, length);
75 		setTargetEdit(target);
76 	}
77 
78 	/*
79 	 * Copy constructor
80 	 */
MoveSourceEdit(MoveSourceEdit other)81 	private MoveSourceEdit(MoveSourceEdit other) {
82 		super(other);
83 		if (other.fModifier != null)
84 			fModifier= other.fModifier.copy();
85 	}
86 
87 	/**
88 	 * Returns the associated target edit or <code>null</code>
89 	 * if no target edit is associated yet.
90 	 *
91 	 * @return the target edit or <code>null</code>
92 	 */
getTargetEdit()93 	public MoveTargetEdit getTargetEdit() {
94 		return fTarget;
95 	}
96 
97 	/**
98 	 * Sets the target edit.
99 	 *
100 	 * @param edit the new target edit.
101 	 *
102 	 * @exception MalformedTreeException is thrown if the target edit
103 	 *  is a direct or indirect child of the source edit
104 	 */
setTargetEdit(MoveTargetEdit edit)105 	public void setTargetEdit(MoveTargetEdit edit) {
106 		fTarget= edit;
107 		fTarget.setSourceEdit(this);
108 	}
109 
110 	/**
111 	 * Returns the current source modifier or <code>null</code>
112 	 * if no source modifier is set.
113 	 *
114 	 * @return the source modifier
115 	 */
getSourceModifier()116 	public ISourceModifier getSourceModifier() {
117 		return fModifier;
118 	}
119 
120 	/**
121 	 * Sets the optional source modifier.
122 	 *
123 	 * @param modifier the source modifier or <code>null</code>
124 	 *  if no source modification is need.
125 	 */
setSourceModifier(ISourceModifier modifier)126 	public void setSourceModifier(ISourceModifier modifier) {
127 		fModifier= modifier;
128 	}
129 
130 	//---- API for MoveTargetEdit ---------------------------------------------
131 
getContent()132 	String getContent() {
133 		// The source content can be null if the edit wasn't executed
134 		// due to an exclusion list of the text edit processor. Return
135 		// the empty string which can be moved without any harm.
136 		if (fSourceContent == null)
137 			return ""; //$NON-NLS-1$
138 		return fSourceContent;
139 	}
140 
getSourceRoot()141 	MultiTextEdit getSourceRoot() {
142 		return fSourceRoot;
143 	}
144 
clearContent()145 	void clearContent() {
146 		fSourceContent= null;
147 		fSourceRoot= null;
148 	}
149 
150 	//---- Copying -------------------------------------------------------------
151 
152 	@Override
doCopy()153 	protected TextEdit doCopy() {
154 		return new MoveSourceEdit(this);
155 	}
156 
157 	@Override
postProcessCopy(TextEditCopier copier)158 	protected void postProcessCopy(TextEditCopier copier) {
159 		if (fTarget != null) {
160 			MoveSourceEdit source= (MoveSourceEdit)copier.getCopy(this);
161 			MoveTargetEdit target= (MoveTargetEdit)copier.getCopy(fTarget);
162 			if (source != null && target != null)
163 				source.setTargetEdit(target);
164 		}
165 	}
166 
167 	//---- Visitor -------------------------------------------------------------
168 
169 	@Override
accept0(TextEditVisitor visitor)170 	protected void accept0(TextEditVisitor visitor) {
171 		boolean visitChildren= visitor.visit(this);
172 		if (visitChildren) {
173 			acceptChildren(visitor);
174 		}
175 	}
176 
177 	//---- consistency check ----------------------------------------------------------------
178 
179 	@Override
traverseConsistencyCheck(TextEditProcessor processor, IDocument document, List<List<TextEdit>> sourceEdits)180 	int traverseConsistencyCheck(TextEditProcessor processor, IDocument document, List<List<TextEdit>> sourceEdits) {
181 		int result= super.traverseConsistencyCheck(processor, document, sourceEdits);
182 		// Since source computation takes place in a recursive fashion (see
183 		// performSourceComputation) we only do something if we don't have a
184 		// computed source already.
185 		if (fSourceContent == null) {
186 			if (sourceEdits.size() <= result) {
187 				List<TextEdit> list= new ArrayList<>();
188 				list.add(this);
189 				for (int i= sourceEdits.size(); i < result; i++)
190 					sourceEdits.add(null);
191 				sourceEdits.add(list);
192 			} else {
193 				List<TextEdit> list= sourceEdits.get(result);
194 				if (list == null) {
195 					list= new ArrayList<>();
196 					sourceEdits.add(result, list);
197 				}
198 				list.add(this);
199 			}
200 		}
201 		return result;
202 	}
203 
204 	@Override
performConsistencyCheck(TextEditProcessor processor, IDocument document)205 	void performConsistencyCheck(TextEditProcessor processor, IDocument document) throws MalformedTreeException {
206 		if (fTarget == null)
207 			throw new MalformedTreeException(getParent(), this, TextEditMessages.getString("MoveSourceEdit.no_target")); //$NON-NLS-1$
208 		if (fTarget.getSourceEdit() != this)
209 			throw new MalformedTreeException(getParent(), this, TextEditMessages.getString("MoveSourceEdit.different_source"));  //$NON-NLS-1$
210 		/* Causes AST rewrite to fail
211 		if (getRoot() != fTarget.getRoot())
212 			throw new MalformedTreeException(getParent(), this, TextEditMessages.getString("MoveSourceEdit.different_tree")); //$NON-NLS-1$
213 		*/
214 	}
215 
216 	//---- source computation --------------------------------------------------------------
217 
218 	@Override
traverseSourceComputation(TextEditProcessor processor, IDocument document)219 	void traverseSourceComputation(TextEditProcessor processor, IDocument document) {
220 		// always perform source computation independent of processor.considerEdit
221 		// The target might need the source and the source is computed in a
222 		// temporary buffer.
223 		performSourceComputation(processor, document);
224 	}
225 
226 	@Override
performSourceComputation(TextEditProcessor processor, IDocument document)227 	void performSourceComputation(TextEditProcessor processor, IDocument document) {
228 		try {
229 			TextEdit[] children= removeChildren();
230 			if (children.length > 0) {
231 				String content= document.get(getOffset(), getLength());
232 				EditDocument subDocument= new EditDocument(content);
233 				fSourceRoot= new MultiTextEdit(getOffset(), getLength());
234 				fSourceRoot.addChildren(children);
235 				fSourceRoot.internalMoveTree(-getOffset());
236 				int processingStyle= getStyle(processor);
237 				TextEditProcessor subProcessor= TextEditProcessor.createSourceComputationProcessor(subDocument, fSourceRoot, processingStyle);
238 				subProcessor.performEdits();
239 				if (needsTransformation())
240 					applyTransformation(subDocument, processingStyle);
241 				fSourceContent= subDocument.get();
242 			} else {
243 				fSourceContent= document.get(getOffset(), getLength());
244 				if (needsTransformation()) {
245 					EditDocument subDocument= new EditDocument(fSourceContent);
246 					applyTransformation(subDocument, getStyle(processor));
247 					fSourceContent= subDocument.get();
248 				}
249 			}
250 		} catch (BadLocationException cannotHappen) {
251 			Assert.isTrue(false);
252 		}
253 	}
254 
getStyle(TextEditProcessor processor)255 	private int getStyle(TextEditProcessor processor) {
256 		// we never need undo while performing local edits.
257 		if ((processor.getStyle() & TextEdit.UPDATE_REGIONS) != 0)
258 			return TextEdit.UPDATE_REGIONS;
259 		return TextEdit.NONE;
260 	}
261 
262 	//---- document updating ----------------------------------------------------------------
263 
264 	@Override
performDocumentUpdating(IDocument document)265 	int performDocumentUpdating(IDocument document) throws BadLocationException {
266 		document.replace(getOffset(), getLength(), ""); //$NON-NLS-1$
267 		fDelta= -getLength();
268 		return fDelta;
269 	}
270 
271 	//---- region updating --------------------------------------------------------------
272 
273 	@Override
deleteChildren()274 	boolean deleteChildren() {
275 		return false;
276 	}
277 
278 	//---- content transformation --------------------------------------------------
279 
needsTransformation()280 	private boolean needsTransformation() {
281 		return fModifier != null;
282 	}
283 
applyTransformation(IDocument document, int style)284 	private void applyTransformation(IDocument document, int style) throws MalformedTreeException {
285 		if ((style & TextEdit.UPDATE_REGIONS) != 0 && fSourceRoot != null) {
286 			Map<TextEdit, TextEdit> editMap= new HashMap<>();
287 			TextEdit newEdit= createEdit(editMap);
288 			List<ReplaceEdit> replaces= new ArrayList<>(Arrays.asList(fModifier.getModifications(document.get())));
289 			insertEdits(newEdit, replaces);
290 			try {
291 				newEdit.apply(document, style);
292 			} catch (BadLocationException cannotHappen) {
293 				Assert.isTrue(false);
294 			}
295 			restorePositions(editMap);
296 		} else {
297 			MultiTextEdit newEdit= new MultiTextEdit(0, document.getLength());
298 			TextEdit[] replaces= fModifier.getModifications(document.get());
299 			for (TextEdit replace : replaces) {
300 				newEdit.addChild(replace);
301 			}
302 			try {
303 				newEdit.apply(document, style);
304 			} catch (BadLocationException cannotHappen) {
305 				Assert.isTrue(false);
306 			}
307 		}
308 	}
309 
createEdit(Map<TextEdit, TextEdit> editMap)310 	private TextEdit createEdit(Map<TextEdit, TextEdit> editMap) {
311 		MultiTextEdit result= new MultiTextEdit(0, fSourceRoot.getLength());
312 		editMap.put(result, fSourceRoot);
313 		createEdit(fSourceRoot, result, editMap);
314 		return result;
315 	}
316 
createEdit(TextEdit source, TextEdit target, Map<TextEdit, TextEdit> editMap)317 	private static void createEdit(TextEdit source, TextEdit target, Map<TextEdit, TextEdit> editMap) {
318 		TextEdit[] children= source.getChildren();
319 		for (TextEdit child : children) {
320 			// a deleted child remains deleted even if the temporary buffer
321 			// gets modified.
322 			if (child.isDeleted())
323 				continue;
324 			RangeMarker marker= new RangeMarker(child.getOffset(), child.getLength());
325 			target.addChild(marker);
326 			editMap.put(marker, child);
327 			createEdit(child, marker, editMap);
328 		}
329 	}
330 
insertEdits(TextEdit root, List<ReplaceEdit> edits)331 	private void insertEdits(TextEdit root, List<ReplaceEdit> edits) {
332 		while(!edits.isEmpty()) {
333 			ReplaceEdit edit= edits.remove(0);
334 			insert(root, edit, edits);
335 		}
336 	}
insert(TextEdit parent, ReplaceEdit edit, List<ReplaceEdit> edits)337 	private static void insert(TextEdit parent, ReplaceEdit edit, List<ReplaceEdit> edits) {
338 		if (!parent.hasChildren()) {
339 			parent.addChild(edit);
340 			return;
341 		}
342 		TextEdit[] children= parent.getChildren();
343 		// First dive down to find the right parent.
344 		int removed= 0;
345 		for (int i= 0; i < children.length; i++) {
346 			TextEdit child= children[i];
347 			if (child.covers(edit)) {
348 				insert(child, edit, edits);
349 				return;
350 			} else if (edit.covers(child)) {
351 				parent.removeChild(i - removed++);
352 				edit.addChild(child);
353 			} else {
354 				IRegion intersect= intersect(edit, child);
355 				if (intersect != null) {
356 					ReplaceEdit[] splits= splitEdit(edit, intersect);
357 					insert(child, splits[0], edits);
358 					edits.add(splits[1]);
359 					return;
360 				}
361 			}
362 		}
363 		parent.addChild(edit);
364 	}
365 
intersect(TextEdit op1, TextEdit op2)366 	public static IRegion intersect(TextEdit op1, TextEdit op2) {
367 		int offset1= op1.getOffset();
368 		int length1= op1.getLength();
369 		int end1= offset1 + length1 - 1;
370 		int offset2= op2.getOffset();
371 		if (end1 < offset2)
372 			return null;
373 		int length2= op2.getLength();
374 		int end2= offset2 + length2 - 1;
375 		if (end2 < offset1)
376 			return null;
377 
378 		int end= Math.min(end1, end2);
379 		if (offset1 < offset2) {
380 			return new Region(offset2, end - offset2 + 1);
381 		}
382 		return new Region(offset1, end - offset1 + 1);
383 	}
384 
splitEdit(ReplaceEdit edit, IRegion intersect)385 	private static ReplaceEdit[] splitEdit(ReplaceEdit edit, IRegion intersect) {
386 		if (edit.getOffset() != intersect.getOffset())
387 			return splitIntersectRight(edit, intersect);
388 		return splitIntersectLeft(edit, intersect);
389 	}
390 
splitIntersectRight(ReplaceEdit edit, IRegion intersect)391 	private static ReplaceEdit[] splitIntersectRight(ReplaceEdit edit, IRegion intersect) {
392 		ReplaceEdit[] result= new ReplaceEdit[2];
393 		// this is the actual delete. We use replace to only deal with one type
394 		result[0]= new ReplaceEdit(intersect.getOffset(), intersect.getLength(), ""); //$NON-NLS-1$
395 		result[1]= new ReplaceEdit(
396 							edit.getOffset(),
397 							intersect.getOffset() - edit.getOffset(),
398 							edit.getText());
399 		return result;
400 	}
401 
splitIntersectLeft(ReplaceEdit edit, IRegion intersect)402 	private static ReplaceEdit[] splitIntersectLeft(ReplaceEdit edit, IRegion intersect) {
403 		ReplaceEdit[] result= new ReplaceEdit[2];
404 		result[0]= new ReplaceEdit(intersect.getOffset(), intersect.getLength(), edit.getText());
405 		result[1]= new ReplaceEdit(	// this is the actual delete. We use replace to only deal with one type
406 							intersect.getOffset() + intersect.getLength(),
407 							edit.getLength() - intersect.getLength(),
408 							""); //$NON-NLS-1$
409 		return result;
410 	}
411 
restorePositions(Map<TextEdit, TextEdit> editMap)412 	private static void restorePositions(Map<TextEdit, TextEdit> editMap) {
413 		for (Entry<TextEdit, TextEdit> entry: editMap.entrySet()) {
414 			TextEdit marker = entry.getKey();
415 			TextEdit edit= entry.getValue();
416 			if (marker.isDeleted()) {
417 				edit.markAsDeleted();
418 			} else {
419 				edit.adjustOffset(marker.getOffset() - edit.getOffset());
420 				edit.adjustLength(marker.getLength() - edit.getLength());
421 			}
422 		}
423 	}
424 }
425