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