1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; 17 18 import static com.android.SdkConstants.ANDROID_NS_NAME; 19 import static com.android.SdkConstants.ANDROID_URI; 20 import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; 21 import static com.android.SdkConstants.ATTR_ID; 22 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; 23 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; 24 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; 25 import static com.android.SdkConstants.ID_PREFIX; 26 import static com.android.SdkConstants.NEW_ID_PREFIX; 27 import static com.android.SdkConstants.XMLNS; 28 import static com.android.SdkConstants.XMLNS_PREFIX; 29 30 import com.android.annotations.NonNull; 31 import com.android.annotations.VisibleForTesting; 32 import com.android.ide.common.xml.XmlFormatStyle; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 35 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; 36 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; 37 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 38 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; 39 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 40 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; 41 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 43 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 44 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 45 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 46 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 47 import com.android.utils.Pair; 48 49 import org.eclipse.core.resources.IFile; 50 import org.eclipse.core.resources.IProject; 51 import org.eclipse.core.resources.ResourcesPlugin; 52 import org.eclipse.core.runtime.CoreException; 53 import org.eclipse.core.runtime.IPath; 54 import org.eclipse.core.runtime.IProgressMonitor; 55 import org.eclipse.core.runtime.OperationCanceledException; 56 import org.eclipse.core.runtime.Path; 57 import org.eclipse.jface.text.BadLocationException; 58 import org.eclipse.jface.text.IDocument; 59 import org.eclipse.jface.text.IRegion; 60 import org.eclipse.jface.text.ITextSelection; 61 import org.eclipse.jface.viewers.ITreeSelection; 62 import org.eclipse.jface.viewers.TreePath; 63 import org.eclipse.ltk.core.refactoring.Change; 64 import org.eclipse.ltk.core.refactoring.ChangeDescriptor; 65 import org.eclipse.ltk.core.refactoring.CompositeChange; 66 import org.eclipse.ltk.core.refactoring.Refactoring; 67 import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor; 68 import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; 69 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 70 import org.eclipse.text.edits.DeleteEdit; 71 import org.eclipse.text.edits.InsertEdit; 72 import org.eclipse.text.edits.MalformedTreeException; 73 import org.eclipse.text.edits.MultiTextEdit; 74 import org.eclipse.text.edits.ReplaceEdit; 75 import org.eclipse.text.edits.TextEdit; 76 import org.eclipse.ui.IEditorPart; 77 import org.eclipse.ui.PartInitException; 78 import org.eclipse.ui.ide.IDE; 79 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 80 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 81 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 82 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; 83 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; 84 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; 85 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; 86 import org.w3c.dom.Attr; 87 import org.w3c.dom.Document; 88 import org.w3c.dom.Element; 89 import org.w3c.dom.NamedNodeMap; 90 import org.w3c.dom.Node; 91 92 import java.util.ArrayList; 93 import java.util.Collections; 94 import java.util.Comparator; 95 import java.util.HashMap; 96 import java.util.HashSet; 97 import java.util.List; 98 import java.util.Locale; 99 import java.util.Map; 100 import java.util.Set; 101 102 /** 103 * Parent class for the various visual refactoring operations; contains shared 104 * implementations needed by most of them 105 */ 106 @SuppressWarnings("restriction") // XML model 107 public abstract class VisualRefactoring extends Refactoring { 108 private static final String KEY_FILE = "file"; //$NON-NLS-1$ 109 private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$ 110 private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$ 111 private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$ 112 113 protected final IFile mFile; 114 protected final LayoutEditorDelegate mDelegate; 115 protected final IProject mProject; 116 protected int mSelectionStart = -1; 117 protected int mSelectionEnd = -1; 118 protected final List<Element> mElements; 119 protected final ITreeSelection mTreeSelection; 120 protected final ITextSelection mSelection; 121 /** Same as {@link #mSelectionStart} but not adjusted to element edges */ 122 protected int mOriginalSelectionStart = -1; 123 /** Same as {@link #mSelectionEnd} but not adjusted to element edges */ 124 protected int mOriginalSelectionEnd = -1; 125 126 protected final Map<Element, String> mGeneratedIdMap = new HashMap<Element, String>(); 127 protected final Set<String> mGeneratedIds = new HashSet<String>(); 128 129 protected List<Change> mChanges; 130 private String mAndroidNamespacePrefix; 131 132 /** 133 * This constructor is solely used by {@link VisualRefactoringDescriptor}, 134 * to replay a previous refactoring. 135 * @param arguments argument map created by #createArgumentMap. 136 */ VisualRefactoring(Map<String, String> arguments)137 VisualRefactoring(Map<String, String> arguments) { 138 IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT)); 139 mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path); 140 path = Path.fromPortableString(arguments.get(KEY_FILE)); 141 mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path); 142 mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START)); 143 mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END)); 144 mOriginalSelectionStart = mSelectionStart; 145 mOriginalSelectionEnd = mSelectionEnd; 146 mDelegate = null; 147 mElements = null; 148 mSelection = null; 149 mTreeSelection = null; 150 } 151 152 @VisibleForTesting VisualRefactoring(List<Element> elements, LayoutEditorDelegate delegate)153 VisualRefactoring(List<Element> elements, LayoutEditorDelegate delegate) { 154 mElements = elements; 155 mDelegate = delegate; 156 157 mFile = delegate != null ? delegate.getEditor().getInputFile() : null; 158 mProject = delegate != null ? delegate.getEditor().getProject() : null; 159 mSelectionStart = 0; 160 mSelectionEnd = 0; 161 mOriginalSelectionStart = 0; 162 mOriginalSelectionEnd = 0; 163 mSelection = null; 164 mTreeSelection = null; 165 166 int end = Integer.MIN_VALUE; 167 int start = Integer.MAX_VALUE; 168 for (Element element : elements) { 169 if (element instanceof IndexedRegion) { 170 IndexedRegion region = (IndexedRegion) element; 171 start = Math.min(start, region.getStartOffset()); 172 end = Math.max(end, region.getEndOffset()); 173 } 174 } 175 if (start >= 0) { 176 mSelectionStart = start; 177 mSelectionEnd = end; 178 mOriginalSelectionStart = start; 179 mOriginalSelectionEnd = end; 180 } 181 } 182 VisualRefactoring(IFile file, LayoutEditorDelegate editor, ITextSelection selection, ITreeSelection treeSelection)183 public VisualRefactoring(IFile file, LayoutEditorDelegate editor, ITextSelection selection, 184 ITreeSelection treeSelection) { 185 mFile = file; 186 mDelegate = editor; 187 mProject = file.getProject(); 188 mSelection = selection; 189 mTreeSelection = treeSelection; 190 191 // Initialize mSelectionStart and mSelectionEnd based on the selection context, which 192 // is either a treeSelection (when invoked from the layout editor or the outline), or 193 // a selection (when invoked from an XML editor) 194 if (treeSelection != null) { 195 int end = Integer.MIN_VALUE; 196 int start = Integer.MAX_VALUE; 197 for (TreePath path : treeSelection.getPaths()) { 198 Object lastSegment = path.getLastSegment(); 199 if (lastSegment instanceof CanvasViewInfo) { 200 CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment; 201 UiViewElementNode uiNode = viewInfo.getUiViewNode(); 202 if (uiNode == null) { 203 continue; 204 } 205 Node xmlNode = uiNode.getXmlNode(); 206 if (xmlNode instanceof IndexedRegion) { 207 IndexedRegion region = (IndexedRegion) xmlNode; 208 209 start = Math.min(start, region.getStartOffset()); 210 end = Math.max(end, region.getEndOffset()); 211 } 212 } 213 } 214 if (start >= 0) { 215 mSelectionStart = start; 216 mSelectionEnd = end; 217 mOriginalSelectionStart = mSelectionStart; 218 mOriginalSelectionEnd = mSelectionEnd; 219 } 220 if (selection != null) { 221 mOriginalSelectionStart = selection.getOffset(); 222 mOriginalSelectionEnd = mOriginalSelectionStart + selection.getLength(); 223 } 224 } else if (selection != null) { 225 // TODO: update selection to boundaries! 226 mSelectionStart = selection.getOffset(); 227 mSelectionEnd = mSelectionStart + selection.getLength(); 228 mOriginalSelectionStart = mSelectionStart; 229 mOriginalSelectionEnd = mSelectionEnd; 230 } 231 232 mElements = initElements(); 233 } 234 235 @NonNull computeChanges(IProgressMonitor monitor)236 protected abstract List<Change> computeChanges(IProgressMonitor monitor); 237 238 @Override checkFinalConditions(IProgressMonitor monitor)239 public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) throws CoreException, 240 OperationCanceledException { 241 RefactoringStatus status = new RefactoringStatus(); 242 mChanges = new ArrayList<Change>(); 243 try { 244 monitor.beginTask("Checking post-conditions...", 5); 245 246 // Reset state for each computeChanges call, in case the user goes back 247 // and forth in the refactoring wizard 248 mGeneratedIdMap.clear(); 249 mGeneratedIds.clear(); 250 List<Change> changes = computeChanges(monitor); 251 mChanges.addAll(changes); 252 253 monitor.worked(1); 254 } finally { 255 monitor.done(); 256 } 257 258 return status; 259 } 260 261 @Override createChange(IProgressMonitor monitor)262 public Change createChange(IProgressMonitor monitor) throws CoreException, 263 OperationCanceledException { 264 try { 265 monitor.beginTask("Applying changes...", 1); 266 267 CompositeChange change = new CompositeChange( 268 getName(), 269 mChanges.toArray(new Change[mChanges.size()])) { 270 @Override 271 public ChangeDescriptor getDescriptor() { 272 VisualRefactoringDescriptor desc = createDescriptor(); 273 return new RefactoringChangeDescriptor(desc); 274 } 275 }; 276 277 monitor.worked(1); 278 return change; 279 280 } finally { 281 monitor.done(); 282 } 283 } 284 createDescriptor()285 protected abstract VisualRefactoringDescriptor createDescriptor(); 286 createArgumentMap()287 protected Map<String, String> createArgumentMap() { 288 HashMap<String, String> args = new HashMap<String, String>(); 289 args.put(KEY_PROJECT, mProject.getFullPath().toPortableString()); 290 args.put(KEY_FILE, mFile.getFullPath().toPortableString()); 291 args.put(KEY_SEL_START, Integer.toString(mSelectionStart)); 292 args.put(KEY_SEL_END, Integer.toString(mSelectionEnd)); 293 294 return args; 295 } 296 getFile()297 IFile getFile() { 298 return mFile; 299 } 300 301 // ---- Shared functionality ---- 302 303 openFile(IFile file)304 protected void openFile(IFile file) { 305 GraphicalEditorPart graphicalEditor = mDelegate.getGraphicalEditor(); 306 IFile leavingFile = graphicalEditor.getEditedFile(); 307 308 try { 309 // Duplicate the current state into the newly created file 310 String state = ConfigurationDescription.getDescription(leavingFile); 311 312 // TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current 313 // theme to show. 314 315 file.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state); 316 } catch (CoreException e) { 317 // pass 318 } 319 320 /* TBD: "Show Included In" if supported. 321 * Not sure if this is a good idea. 322 if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) { 323 try { 324 Reference include = Reference.create(graphicalEditor.getEditedFile()); 325 file.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, include); 326 } catch (CoreException e) { 327 // pass - worst that can happen is that we don't start with inclusion 328 } 329 } 330 */ 331 332 try { 333 IEditorPart part = 334 IDE.openEditor(mDelegate.getEditor().getEditorSite().getPage(), file); 335 if (part instanceof AndroidXmlEditor && AdtPrefs.getPrefs().getFormatGuiXml()) { 336 AndroidXmlEditor newEditor = (AndroidXmlEditor) part; 337 newEditor.reformatDocument(); 338 } 339 } catch (PartInitException e) { 340 AdtPlugin.log(e, "Can't open new included layout"); 341 } 342 } 343 344 345 /** Produce a list of edits to replace references to the given id with the given new id */ replaceIds(String androidNamePrefix, IStructuredDocument doc, int skipStart, int skipEnd, String rootId, String referenceId)346 protected static List<TextEdit> replaceIds(String androidNamePrefix, 347 IStructuredDocument doc, int skipStart, int skipEnd, 348 String rootId, String referenceId) { 349 if (rootId == null) { 350 return Collections.emptyList(); 351 } 352 353 // We need to search for either @+id/ or @id/ 354 String match1 = rootId; 355 String match2; 356 if (match1.startsWith(ID_PREFIX)) { 357 match2 = '"' + NEW_ID_PREFIX + match1.substring(ID_PREFIX.length()) + '"'; 358 match1 = '"' + match1 + '"'; 359 } else if (match1.startsWith(NEW_ID_PREFIX)) { 360 match2 = '"' + ID_PREFIX + match1.substring(NEW_ID_PREFIX.length()) + '"'; 361 match1 = '"' + match1 + '"'; 362 } else { 363 return Collections.emptyList(); 364 } 365 366 String namePrefix = androidNamePrefix + ':' + ATTR_LAYOUT_RESOURCE_PREFIX; 367 List<TextEdit> edits = new ArrayList<TextEdit>(); 368 369 IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion(); 370 for (; region != null; region = region.getNext()) { 371 ITextRegionList list = region.getRegions(); 372 int regionStart = region.getStart(); 373 374 // Look at all attribute values and look for an id reference match 375 String attributeName = ""; //$NON-NLS-1$ 376 for (int j = 0; j < region.getNumberOfRegions(); j++) { 377 ITextRegion subRegion = list.get(j); 378 String type = subRegion.getType(); 379 if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { 380 attributeName = region.getText(subRegion); 381 } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { 382 // Only replace references in layout attributes 383 if (!attributeName.startsWith(namePrefix)) { 384 continue; 385 } 386 // Skip occurrences in the given skip range 387 int subRegionStart = regionStart + subRegion.getStart(); 388 if (subRegionStart >= skipStart && subRegionStart <= skipEnd) { 389 continue; 390 } 391 392 String attributeValue = region.getText(subRegion); 393 if (attributeValue.equals(match1) || attributeValue.equals(match2)) { 394 int start = subRegionStart + 1; // skip quote 395 int end = start + rootId.length(); 396 397 edits.add(new ReplaceEdit(start, end - start, referenceId)); 398 } 399 } 400 } 401 } 402 403 return edits; 404 } 405 406 /** Get the id of the root selected element, if any */ getRootId()407 protected String getRootId() { 408 Element primary = getPrimaryElement(); 409 if (primary != null) { 410 String oldId = primary.getAttributeNS(ANDROID_URI, ATTR_ID); 411 // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378 412 if (oldId != null && oldId.length() > 0) { 413 return oldId; 414 } 415 } 416 417 return null; 418 } 419 getAndroidNamespacePrefix()420 protected String getAndroidNamespacePrefix() { 421 if (mAndroidNamespacePrefix == null) { 422 List<Attr> attributeNodes = findNamespaceAttributes(); 423 for (Node attributeNode : attributeNodes) { 424 String prefix = attributeNode.getPrefix(); 425 if (XMLNS.equals(prefix)) { 426 String name = attributeNode.getNodeName(); 427 String value = attributeNode.getNodeValue(); 428 if (value.equals(ANDROID_URI)) { 429 mAndroidNamespacePrefix = name; 430 if (mAndroidNamespacePrefix.startsWith(XMLNS_PREFIX)) { 431 mAndroidNamespacePrefix = 432 mAndroidNamespacePrefix.substring(XMLNS_PREFIX.length()); 433 } 434 } 435 } 436 } 437 438 if (mAndroidNamespacePrefix == null) { 439 mAndroidNamespacePrefix = ANDROID_NS_NAME; 440 } 441 } 442 443 return mAndroidNamespacePrefix; 444 } 445 getAndroidNamespacePrefix(Document document)446 protected static String getAndroidNamespacePrefix(Document document) { 447 String nsPrefix = null; 448 List<Attr> attributeNodes = findNamespaceAttributes(document); 449 for (Node attributeNode : attributeNodes) { 450 String prefix = attributeNode.getPrefix(); 451 if (XMLNS.equals(prefix)) { 452 String name = attributeNode.getNodeName(); 453 String value = attributeNode.getNodeValue(); 454 if (value.equals(ANDROID_URI)) { 455 nsPrefix = name; 456 if (nsPrefix.startsWith(XMLNS_PREFIX)) { 457 nsPrefix = 458 nsPrefix.substring(XMLNS_PREFIX.length()); 459 } 460 } 461 } 462 } 463 464 if (nsPrefix == null) { 465 nsPrefix = ANDROID_NS_NAME; 466 } 467 468 return nsPrefix; 469 } 470 findNamespaceAttributes()471 protected List<Attr> findNamespaceAttributes() { 472 Document document = getDomDocument(); 473 return findNamespaceAttributes(document); 474 } 475 findNamespaceAttributes(Document document)476 protected static List<Attr> findNamespaceAttributes(Document document) { 477 if (document != null) { 478 Element root = document.getDocumentElement(); 479 return findNamespaceAttributes(root); 480 } 481 482 return Collections.emptyList(); 483 } 484 findNamespaceAttributes(Node root)485 protected static List<Attr> findNamespaceAttributes(Node root) { 486 List<Attr> result = new ArrayList<Attr>(); 487 NamedNodeMap attributes = root.getAttributes(); 488 for (int i = 0, n = attributes.getLength(); i < n; i++) { 489 Node attributeNode = attributes.item(i); 490 491 String prefix = attributeNode.getPrefix(); 492 if (XMLNS.equals(prefix)) { 493 result.add((Attr) attributeNode); 494 } 495 } 496 497 return result; 498 } 499 findLayoutAttributes(Node root)500 protected List<Attr> findLayoutAttributes(Node root) { 501 List<Attr> result = new ArrayList<Attr>(); 502 NamedNodeMap attributes = root.getAttributes(); 503 for (int i = 0, n = attributes.getLength(); i < n; i++) { 504 Node attributeNode = attributes.item(i); 505 506 String name = attributeNode.getLocalName(); 507 if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) 508 && ANDROID_URI.equals(attributeNode.getNamespaceURI())) { 509 result.add((Attr) attributeNode); 510 } 511 } 512 513 return result; 514 } 515 insertNamespace(String xmlText, String namespaceDeclarations)516 protected String insertNamespace(String xmlText, String namespaceDeclarations) { 517 // Insert namespace declarations into the extracted XML fragment 518 int firstSpace = xmlText.indexOf(' '); 519 int elementEnd = xmlText.indexOf('>'); 520 int insertAt; 521 if (firstSpace != -1 && firstSpace < elementEnd) { 522 insertAt = firstSpace; 523 } else { 524 insertAt = elementEnd; 525 } 526 xmlText = xmlText.substring(0, insertAt) + namespaceDeclarations 527 + xmlText.substring(insertAt); 528 529 return xmlText; 530 } 531 532 /** Remove sections of the document that correspond to top level layout attributes; 533 * these are placed on the include element instead */ stripTopLayoutAttributes(Element primary, int start, String xml)534 protected String stripTopLayoutAttributes(Element primary, int start, String xml) { 535 if (primary != null) { 536 // List of attributes to remove 537 List<IndexedRegion> skip = new ArrayList<IndexedRegion>(); 538 NamedNodeMap attributes = primary.getAttributes(); 539 for (int i = 0, n = attributes.getLength(); i < n; i++) { 540 Node attr = attributes.item(i); 541 String name = attr.getLocalName(); 542 if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) 543 && ANDROID_URI.equals(attr.getNamespaceURI())) { 544 if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) { 545 // These are special and are left in 546 continue; 547 } 548 549 if (attr instanceof IndexedRegion) { 550 skip.add((IndexedRegion) attr); 551 } 552 } 553 } 554 if (skip.size() > 0) { 555 Collections.sort(skip, new Comparator<IndexedRegion>() { 556 // Sort in start order 557 @Override 558 public int compare(IndexedRegion r1, IndexedRegion r2) { 559 return r1.getStartOffset() - r2.getStartOffset(); 560 } 561 }); 562 563 // Successively cut out the various layout attributes 564 // TODO remove adjacent whitespace too (but not newlines, unless they 565 // are newly adjacent) 566 StringBuilder sb = new StringBuilder(xml.length()); 567 int nextStart = 0; 568 569 // Copy out all the sections except the skip sections 570 for (IndexedRegion r : skip) { 571 int regionStart = r.getStartOffset(); 572 // Adjust to string offsets since we've copied the string out of 573 // the document 574 regionStart -= start; 575 576 sb.append(xml.substring(nextStart, regionStart)); 577 578 nextStart = regionStart + r.getLength(); 579 } 580 if (nextStart < xml.length()) { 581 sb.append(xml.substring(nextStart)); 582 } 583 584 return sb.toString(); 585 } 586 } 587 588 return xml; 589 } 590 getIndent(String line, int max)591 protected static String getIndent(String line, int max) { 592 int i = 0; 593 int n = Math.min(max, line.length()); 594 for (; i < n; i++) { 595 char c = line.charAt(i); 596 if (!Character.isWhitespace(c)) { 597 return line.substring(0, i); 598 } 599 } 600 601 if (n < line.length()) { 602 return line.substring(0, n); 603 } else { 604 return line; 605 } 606 } 607 dedent(String xml)608 protected static String dedent(String xml) { 609 String[] lines = xml.split("\n"); //$NON-NLS-1$ 610 if (lines.length < 2) { 611 // The first line never has any indentation since we copy it out from the 612 // element start index 613 return xml; 614 } 615 616 String indentPrefix = getIndent(lines[1], lines[1].length()); 617 for (int i = 2, n = lines.length; i < n; i++) { 618 String line = lines[i]; 619 620 // Ignore blank lines 621 if (line.trim().length() == 0) { 622 continue; 623 } 624 625 indentPrefix = getIndent(line, indentPrefix.length()); 626 627 if (indentPrefix.length() == 0) { 628 return xml; 629 } 630 } 631 632 StringBuilder sb = new StringBuilder(); 633 for (String line : lines) { 634 if (line.startsWith(indentPrefix)) { 635 sb.append(line.substring(indentPrefix.length())); 636 } else { 637 sb.append(line); 638 } 639 sb.append('\n'); 640 } 641 return sb.toString(); 642 } 643 getText(int start, int end)644 protected String getText(int start, int end) { 645 try { 646 IStructuredDocument document = mDelegate.getEditor().getStructuredDocument(); 647 return document.get(start, end - start); 648 } catch (BadLocationException e) { 649 // the region offset was invalid. ignore. 650 return null; 651 } 652 } 653 getElements()654 protected List<Element> getElements() { 655 return mElements; 656 } 657 initElements()658 protected List<Element> initElements() { 659 List<Element> nodes = new ArrayList<Element>(); 660 661 assert mTreeSelection == null || mSelection == null : 662 "treeSel= " + mTreeSelection + ", sel=" + mSelection; 663 664 // Initialize mSelectionStart and mSelectionEnd based on the selection context, which 665 // is either a treeSelection (when invoked from the layout editor or the outline), or 666 // a selection (when invoked from an XML editor) 667 if (mTreeSelection != null) { 668 int end = Integer.MIN_VALUE; 669 int start = Integer.MAX_VALUE; 670 for (TreePath path : mTreeSelection.getPaths()) { 671 Object lastSegment = path.getLastSegment(); 672 if (lastSegment instanceof CanvasViewInfo) { 673 CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment; 674 UiViewElementNode uiNode = viewInfo.getUiViewNode(); 675 if (uiNode == null) { 676 continue; 677 } 678 Node xmlNode = uiNode.getXmlNode(); 679 if (xmlNode instanceof Element) { 680 Element element = (Element) xmlNode; 681 nodes.add(element); 682 IndexedRegion region = getRegion(element); 683 start = Math.min(start, region.getStartOffset()); 684 end = Math.max(end, region.getEndOffset()); 685 } 686 } 687 } 688 if (start >= 0) { 689 mSelectionStart = start; 690 mSelectionEnd = end; 691 } 692 } else if (mSelection != null) { 693 mSelectionStart = mSelection.getOffset(); 694 mSelectionEnd = mSelectionStart + mSelection.getLength(); 695 mOriginalSelectionStart = mSelectionStart; 696 mOriginalSelectionEnd = mSelectionEnd; 697 698 // Figure out the range of selected nodes from the document offsets 699 IStructuredDocument doc = mDelegate.getEditor().getStructuredDocument(); 700 Pair<Element, Element> range = DomUtilities.getElementRange(doc, 701 mSelectionStart, mSelectionEnd); 702 if (range != null) { 703 Element first = range.getFirst(); 704 Element last = range.getSecond(); 705 706 // Adjust offsets to get rid of surrounding text nodes (if you happened 707 // to select a text range and included whitespace on either end etc) 708 mSelectionStart = getRegion(first).getStartOffset(); 709 mSelectionEnd = getRegion(last).getEndOffset(); 710 711 if (mSelectionStart > mSelectionEnd) { 712 int tmp = mSelectionStart; 713 mSelectionStart = mSelectionEnd; 714 mSelectionEnd = tmp; 715 } 716 717 if (first == last) { 718 nodes.add(first); 719 } else if (first.getParentNode() == last.getParentNode()) { 720 // Add the range 721 Node node = first; 722 while (node != null) { 723 if (node instanceof Element) { 724 nodes.add((Element) node); 725 } 726 if (node == last) { 727 break; 728 } 729 node = node.getNextSibling(); 730 } 731 } else { 732 // Different parents: this means we have an uneven selection, selecting 733 // elements from different levels. We can't extract ranges like that. 734 } 735 } 736 } else { 737 assert false; 738 } 739 740 // Make sure that the list of elements is unique 741 //Set<Element> seen = new HashSet<Element>(); 742 //for (Element element : nodes) { 743 // assert !seen.contains(element) : element; 744 // seen.add(element); 745 //} 746 747 return nodes; 748 } 749 getPrimaryElement()750 protected Element getPrimaryElement() { 751 List<Element> elements = getElements(); 752 if (elements != null && elements.size() == 1) { 753 return elements.get(0); 754 } 755 756 return null; 757 } 758 getDomDocument()759 protected Document getDomDocument() { 760 if (mDelegate.getUiRootNode() != null) { 761 return mDelegate.getUiRootNode().getXmlDocument(); 762 } else { 763 return getElements().get(0).getOwnerDocument(); 764 } 765 } 766 getSelectedViewInfos()767 protected List<CanvasViewInfo> getSelectedViewInfos() { 768 List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>(); 769 if (mTreeSelection != null) { 770 for (TreePath path : mTreeSelection.getPaths()) { 771 Object lastSegment = path.getLastSegment(); 772 if (lastSegment instanceof CanvasViewInfo) { 773 infos.add((CanvasViewInfo) lastSegment); 774 } 775 } 776 } 777 return infos; 778 } 779 validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status)780 protected boolean validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status) { 781 if (infos.size() == 0) { 782 status.addFatalError("No selection to extract"); 783 return false; 784 } 785 786 return true; 787 } 788 validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status)789 protected boolean validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status) { 790 for (CanvasViewInfo info : infos) { 791 if (info.isRoot()) { 792 status.addFatalError("Cannot refactor the root"); 793 return false; 794 } 795 } 796 797 return true; 798 } 799 validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status)800 protected boolean validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status) { 801 if (infos.size() > 1) { 802 // All elements must be siblings (e.g. same parent) 803 List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos 804 .size()); 805 for (CanvasViewInfo info : infos) { 806 UiViewElementNode node = info.getUiViewNode(); 807 if (node != null) { 808 nodes.add(node); 809 } 810 } 811 if (nodes.size() == 0) { 812 status.addFatalError("No selected views"); 813 return false; 814 } 815 816 UiElementNode parent = nodes.get(0).getUiParent(); 817 for (UiViewElementNode node : nodes) { 818 if (parent != node.getUiParent()) { 819 status.addFatalError("The selected elements must be adjacent"); 820 return false; 821 } 822 } 823 // Ensure that the siblings are contiguous; no gaps. 824 // If we've selected all the children of the parent then we don't need 825 // to look. 826 List<UiElementNode> siblings = parent.getUiChildren(); 827 if (siblings.size() != nodes.size()) { 828 Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes); 829 boolean inRange = false; 830 int remaining = nodes.size(); 831 for (UiElementNode node : siblings) { 832 boolean in = nodeSet.contains(node); 833 if (in) { 834 remaining--; 835 if (remaining == 0) { 836 break; 837 } 838 inRange = true; 839 } else if (inRange) { 840 status.addFatalError("The selected elements must be adjacent"); 841 return false; 842 } 843 } 844 } 845 } 846 847 return true; 848 } 849 850 /** 851 * Updates the given element with a new name if the current id reflects the old 852 * element type. If the name was changed, it will return the new name. 853 */ ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit)854 protected String ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit) { 855 String oldType = element.getTagName(); 856 if (oldType.indexOf('.') == -1) { 857 oldType = ANDROID_WIDGET_PREFIX + oldType; 858 } 859 String oldTypeBase = oldType.substring(oldType.lastIndexOf('.') + 1); 860 String id = getId(element); 861 if (id == null || id.length() == 0 862 || id.toLowerCase(Locale.US).contains(oldTypeBase.toLowerCase(Locale.US))) { 863 String newTypeBase = newType.substring(newType.lastIndexOf('.') + 1); 864 return ensureHasId(rootEdit, element, newTypeBase); 865 } 866 867 return null; 868 } 869 870 /** 871 * Returns the {@link IndexedRegion} for the given node 872 * 873 * @param node the node to look up the region for 874 * @return the corresponding region, or null 875 */ getRegion(Node node)876 public static IndexedRegion getRegion(Node node) { 877 if (node instanceof IndexedRegion) { 878 return (IndexedRegion) node; 879 } 880 881 return null; 882 } 883 ensureHasId(MultiTextEdit rootEdit, Element element, String prefix)884 protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix) { 885 return ensureHasId(rootEdit, element, prefix, true); 886 } 887 ensureHasId(MultiTextEdit rootEdit, Element element, String prefix, boolean apply)888 protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix, 889 boolean apply) { 890 String id = mGeneratedIdMap.get(element); 891 if (id != null) { 892 return NEW_ID_PREFIX + id; 893 } 894 895 if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID) 896 || (prefix != null && !getId(element).startsWith(prefix))) { 897 id = DomUtilities.getFreeWidgetId(element, mGeneratedIds, prefix); 898 // Make sure we don't use this one again 899 mGeneratedIds.add(id); 900 mGeneratedIdMap.put(element, id); 901 id = NEW_ID_PREFIX + id; 902 if (apply) { 903 setAttribute(rootEdit, element, 904 ANDROID_URI, getAndroidNamespacePrefix(), ATTR_ID, id); 905 } 906 return id; 907 } 908 909 return getId(element); 910 } 911 getFirstAttributeOffset(Element element)912 protected int getFirstAttributeOffset(Element element) { 913 IndexedRegion region = getRegion(element); 914 if (region != null) { 915 int startOffset = region.getStartOffset(); 916 int endOffset = region.getEndOffset(); 917 String text = getText(startOffset, endOffset); 918 String name = element.getLocalName(); 919 int nameOffset = text.indexOf(name); 920 if (nameOffset != -1) { 921 return startOffset + nameOffset + name.length(); 922 } 923 } 924 925 return -1; 926 } 927 928 /** 929 * Returns the id of the given element 930 * 931 * @param element the element to look up the id for 932 * @return the corresponding id, or an empty string (should not be null 933 * according to the DOM API, but has been observed to be null on 934 * some versions of Eclipse) 935 */ getId(Element element)936 public static String getId(Element element) { 937 return element.getAttributeNS(ANDROID_URI, ATTR_ID); 938 } 939 ensureNewId(String id)940 protected String ensureNewId(String id) { 941 if (id != null && id.length() > 0) { 942 if (id.startsWith(ID_PREFIX)) { 943 id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length()); 944 } else if (!id.startsWith(NEW_ID_PREFIX)) { 945 id = NEW_ID_PREFIX + id; 946 } 947 } else { 948 id = null; 949 } 950 951 return id; 952 } 953 getViewClass(String fqcn)954 protected String getViewClass(String fqcn) { 955 // Don't include android.widget. as a package prefix in layout files 956 if (fqcn.startsWith(ANDROID_WIDGET_PREFIX)) { 957 fqcn = fqcn.substring(ANDROID_WIDGET_PREFIX.length()); 958 } 959 960 return fqcn; 961 } 962 setAttribute(MultiTextEdit rootEdit, Element element, String attributeUri, String attributePrefix, String attributeName, String attributeValue)963 protected void setAttribute(MultiTextEdit rootEdit, Element element, 964 String attributeUri, 965 String attributePrefix, String attributeName, String attributeValue) { 966 int offset = getFirstAttributeOffset(element); 967 if (offset != -1) { 968 if (element.hasAttributeNS(attributeUri, attributeName)) { 969 replaceAttributeDeclaration(rootEdit, offset, element, attributePrefix, 970 attributeUri, attributeName, attributeValue); 971 } else { 972 addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName, 973 attributeValue); 974 } 975 } 976 } 977 addAttributeDeclaration(MultiTextEdit rootEdit, int offset, String attributePrefix, String attributeName, String attributeValue)978 private void addAttributeDeclaration(MultiTextEdit rootEdit, int offset, 979 String attributePrefix, String attributeName, String attributeValue) { 980 StringBuilder sb = new StringBuilder(); 981 sb.append(' '); 982 983 if (attributePrefix != null) { 984 sb.append(attributePrefix).append(':'); 985 } 986 sb.append(attributeName).append('=').append('"'); 987 sb.append(attributeValue).append('"'); 988 989 InsertEdit setAttribute = new InsertEdit(offset, sb.toString()); 990 rootEdit.addChild(setAttribute); 991 } 992 993 /** Replaces the value declaration of the given attribute */ replaceAttributeDeclaration(MultiTextEdit rootEdit, int offset, Element element, String attributePrefix, String attributeUri, String attributeName, String attributeValue)994 private void replaceAttributeDeclaration(MultiTextEdit rootEdit, int offset, 995 Element element, String attributePrefix, String attributeUri, 996 String attributeName, String attributeValue) { 997 // Find attribute value and replace it 998 IStructuredModel model = mDelegate.getEditor().getModelForRead(); 999 try { 1000 IStructuredDocument doc = model.getStructuredDocument(); 1001 1002 IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); 1003 ITextRegionList list = region.getRegions(); 1004 int regionStart = region.getStart(); 1005 1006 int valueStart = -1; 1007 boolean useNextValue = false; 1008 String targetName = attributePrefix != null 1009 ? attributePrefix + ':' + attributeName : attributeName; 1010 1011 // Look at all attribute values and look for an id reference match 1012 for (int j = 0; j < region.getNumberOfRegions(); j++) { 1013 ITextRegion subRegion = list.get(j); 1014 String type = subRegion.getType(); 1015 if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { 1016 // What about prefix? 1017 if (targetName.equals(region.getText(subRegion))) { 1018 useNextValue = true; 1019 } 1020 } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { 1021 if (useNextValue) { 1022 valueStart = regionStart + subRegion.getStart(); 1023 break; 1024 } 1025 } 1026 } 1027 1028 if (valueStart != -1) { 1029 String oldValue = element.getAttributeNS(attributeUri, attributeName); 1030 int start = valueStart + 1; // Skip opening " 1031 ReplaceEdit setAttribute = new ReplaceEdit(start, oldValue.length(), 1032 attributeValue); 1033 try { 1034 rootEdit.addChild(setAttribute); 1035 } catch (MalformedTreeException mte) { 1036 AdtPlugin.log(mte, "Could not replace attribute %1$s with %2$s", 1037 attributeName, attributeValue); 1038 throw mte; 1039 } 1040 } 1041 } finally { 1042 model.releaseFromRead(); 1043 } 1044 } 1045 1046 /** Strips out the given attribute, if defined */ removeAttribute(MultiTextEdit rootEdit, Element element, String uri, String attributeName)1047 protected void removeAttribute(MultiTextEdit rootEdit, Element element, String uri, 1048 String attributeName) { 1049 if (element.hasAttributeNS(uri, attributeName)) { 1050 Attr attribute = element.getAttributeNodeNS(uri, attributeName); 1051 removeAttribute(rootEdit, attribute); 1052 } 1053 } 1054 1055 /** Strips out the given attribute, if defined */ removeAttribute(MultiTextEdit rootEdit, Attr attribute)1056 protected void removeAttribute(MultiTextEdit rootEdit, Attr attribute) { 1057 IndexedRegion region = getRegion(attribute); 1058 if (region != null) { 1059 int startOffset = region.getStartOffset(); 1060 int endOffset = region.getEndOffset(); 1061 DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset); 1062 rootEdit.addChild(deletion); 1063 } 1064 } 1065 1066 1067 /** 1068 * Removes the given element's opening and closing tags (including all of its 1069 * attributes) but leaves any children alone 1070 * 1071 * @param rootEdit the multi edit to add the removal operation to 1072 * @param element the element to delete the open and closing tags for 1073 * @param skip a list of elements that should not be modified (for example because they 1074 * are targeted for deletion) 1075 * 1076 * TODO: Rename this to "unwrap" ? And allow for handling nested deletions. 1077 */ removeElementTags(MultiTextEdit rootEdit, Element element, List<Element> skip, boolean changeIndentation)1078 protected void removeElementTags(MultiTextEdit rootEdit, Element element, List<Element> skip, 1079 boolean changeIndentation) { 1080 IndexedRegion elementRegion = getRegion(element); 1081 if (elementRegion == null) { 1082 return; 1083 } 1084 1085 // Look for the opening tag 1086 IStructuredModel model = mDelegate.getEditor().getModelForRead(); 1087 try { 1088 int startLineInclusive = -1; 1089 int endLineInclusive = -1; 1090 IStructuredDocument doc = model.getStructuredDocument(); 1091 if (doc != null) { 1092 int start = elementRegion.getStartOffset(); 1093 IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start); 1094 ITextRegionList list = region.getRegions(); 1095 int regionStart = region.getStart(); 1096 int startOffset = regionStart; 1097 for (int j = 0; j < region.getNumberOfRegions(); j++) { 1098 ITextRegion subRegion = list.get(j); 1099 String type = subRegion.getType(); 1100 if (DOMRegionContext.XML_TAG_OPEN.equals(type)) { 1101 startOffset = regionStart + subRegion.getStart(); 1102 } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) { 1103 int endOffset = regionStart + subRegion.getStart() + subRegion.getLength(); 1104 1105 DeleteEdit deletion = createDeletion(doc, startOffset, endOffset); 1106 rootEdit.addChild(deletion); 1107 startLineInclusive = doc.getLineOfOffset(endOffset) + 1; 1108 break; 1109 } 1110 } 1111 1112 // Find the close tag 1113 // Look at all attribute values and look for an id reference match 1114 region = doc.getRegionAtCharacterOffset(elementRegion.getEndOffset() 1115 - element.getTagName().length() - 1); 1116 list = region.getRegions(); 1117 regionStart = region.getStartOffset(); 1118 startOffset = -1; 1119 for (int j = 0; j < region.getNumberOfRegions(); j++) { 1120 ITextRegion subRegion = list.get(j); 1121 String type = subRegion.getType(); 1122 if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) { 1123 startOffset = regionStart + subRegion.getStart(); 1124 } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) { 1125 int endOffset = regionStart + subRegion.getStart() + subRegion.getLength(); 1126 if (startOffset != -1) { 1127 DeleteEdit deletion = createDeletion(doc, startOffset, endOffset); 1128 rootEdit.addChild(deletion); 1129 endLineInclusive = doc.getLineOfOffset(startOffset) - 1; 1130 } 1131 break; 1132 } 1133 } 1134 } 1135 1136 // Dedent the contents 1137 if (changeIndentation && startLineInclusive != -1 && endLineInclusive != -1) { 1138 String indent = AndroidXmlEditor.getIndentAtOffset(doc, getRegion(element) 1139 .getStartOffset()); 1140 setIndentation(rootEdit, indent, doc, startLineInclusive, endLineInclusive, 1141 element, skip); 1142 } 1143 } finally { 1144 model.releaseFromRead(); 1145 } 1146 } 1147 removeIndentation(MultiTextEdit rootEdit, String removeIndent, IStructuredDocument doc, int startLineInclusive, int endLineInclusive, Element element, List<Element> skip)1148 protected void removeIndentation(MultiTextEdit rootEdit, String removeIndent, 1149 IStructuredDocument doc, int startLineInclusive, int endLineInclusive, 1150 Element element, List<Element> skip) { 1151 if (startLineInclusive > endLineInclusive) { 1152 return; 1153 } 1154 int indentLength = removeIndent.length(); 1155 if (indentLength == 0) { 1156 return; 1157 } 1158 1159 try { 1160 for (int line = startLineInclusive; line <= endLineInclusive; line++) { 1161 IRegion info = doc.getLineInformation(line); 1162 int lineStart = info.getOffset(); 1163 int lineLength = info.getLength(); 1164 int lineEnd = lineStart + lineLength; 1165 if (overlaps(lineStart, lineEnd, element, skip)) { 1166 continue; 1167 } 1168 String lineText = getText(lineStart, 1169 lineStart + Math.min(lineLength, indentLength)); 1170 if (lineText.startsWith(removeIndent)) { 1171 rootEdit.addChild(new DeleteEdit(lineStart, indentLength)); 1172 } 1173 } 1174 } catch (BadLocationException e) { 1175 AdtPlugin.log(e, null); 1176 } 1177 } 1178 setIndentation(MultiTextEdit rootEdit, String indent, IStructuredDocument doc, int startLineInclusive, int endLineInclusive, Element element, List<Element> skip)1179 protected void setIndentation(MultiTextEdit rootEdit, String indent, 1180 IStructuredDocument doc, int startLineInclusive, int endLineInclusive, 1181 Element element, List<Element> skip) { 1182 if (startLineInclusive > endLineInclusive) { 1183 return; 1184 } 1185 int indentLength = indent.length(); 1186 if (indentLength == 0) { 1187 return; 1188 } 1189 1190 try { 1191 for (int line = startLineInclusive; line <= endLineInclusive; line++) { 1192 IRegion info = doc.getLineInformation(line); 1193 int lineStart = info.getOffset(); 1194 int lineLength = info.getLength(); 1195 int lineEnd = lineStart + lineLength; 1196 if (overlaps(lineStart, lineEnd, element, skip)) { 1197 continue; 1198 } 1199 String lineText = getText(lineStart, lineStart + lineLength); 1200 int indentEnd = getFirstNonSpace(lineText); 1201 rootEdit.addChild(new ReplaceEdit(lineStart, indentEnd, indent)); 1202 } 1203 } catch (BadLocationException e) { 1204 AdtPlugin.log(e, null); 1205 } 1206 } 1207 getFirstNonSpace(String s)1208 private int getFirstNonSpace(String s) { 1209 for (int i = 0; i < s.length(); i++) { 1210 if (!Character.isWhitespace(s.charAt(i))) { 1211 return i; 1212 } 1213 } 1214 1215 return s.length(); 1216 } 1217 1218 /** Returns true if the given line overlaps any of the given elements */ overlaps(int startOffset, int endOffset, Element element, List<Element> overlaps)1219 private static boolean overlaps(int startOffset, int endOffset, 1220 Element element, List<Element> overlaps) { 1221 for (Element e : overlaps) { 1222 if (e == element) { 1223 continue; 1224 } 1225 1226 IndexedRegion region = getRegion(e); 1227 if (region.getEndOffset() >= startOffset && region.getStartOffset() <= endOffset) { 1228 return true; 1229 } 1230 } 1231 return false; 1232 } 1233 createDeletion(IStructuredDocument doc, int startOffset, int endOffset)1234 protected DeleteEdit createDeletion(IStructuredDocument doc, int startOffset, int endOffset) { 1235 // Expand to delete the whole line? 1236 try { 1237 IRegion info = doc.getLineInformationOfOffset(startOffset); 1238 int lineBegin = info.getOffset(); 1239 // Is the text on the line leading up to the deletion region, 1240 // and the text following it, all whitespace? 1241 boolean deleteLine = true; 1242 if (lineBegin < startOffset) { 1243 String prefix = getText(lineBegin, startOffset); 1244 if (prefix.trim().length() > 0) { 1245 deleteLine = false; 1246 } 1247 } 1248 info = doc.getLineInformationOfOffset(endOffset); 1249 int lineEnd = info.getOffset() + info.getLength(); 1250 if (lineEnd > endOffset) { 1251 String suffix = getText(endOffset, lineEnd); 1252 if (suffix.trim().length() > 0) { 1253 deleteLine = false; 1254 } 1255 } 1256 if (deleteLine) { 1257 startOffset = lineBegin; 1258 endOffset = Math.min(doc.getLength(), lineEnd + 1); 1259 } 1260 } catch (BadLocationException e) { 1261 AdtPlugin.log(e, null); 1262 } 1263 1264 1265 return new DeleteEdit(startOffset, endOffset - startOffset); 1266 } 1267 1268 /** 1269 * Rewrite the edits in the given {@link MultiTextEdit} such that same edits are 1270 * applied, but the resulting range is also formatted 1271 */ reformat(MultiTextEdit edit, XmlFormatStyle style)1272 protected MultiTextEdit reformat(MultiTextEdit edit, XmlFormatStyle style) { 1273 String xml = mDelegate.getEditor().getStructuredDocument().get(); 1274 return reformat(xml, edit, style); 1275 } 1276 1277 /** 1278 * Rewrite the edits in the given {@link MultiTextEdit} such that same edits are 1279 * applied, but the resulting range is also formatted 1280 * 1281 * @param oldContents the original contents that should be edited by a 1282 * {@link MultiTextEdit} 1283 * @param edit the {@link MultiTextEdit} to be applied to some string 1284 * @param style the formatting style to use 1285 * @return a new {@link MultiTextEdit} which performs the same edits as the input edit 1286 * but also reformats the text 1287 */ reformat(String oldContents, MultiTextEdit edit, XmlFormatStyle style)1288 public static MultiTextEdit reformat(String oldContents, MultiTextEdit edit, 1289 XmlFormatStyle style) { 1290 IDocument document = new org.eclipse.jface.text.Document(); 1291 document.set(oldContents); 1292 1293 try { 1294 edit.apply(document); 1295 } catch (MalformedTreeException e) { 1296 AdtPlugin.log(e, null); 1297 return null; // Abort formatting 1298 } catch (BadLocationException e) { 1299 AdtPlugin.log(e, null); 1300 return null; // Abort formatting 1301 } 1302 1303 String actual = document.get(); 1304 1305 // TODO: Try to format only the affected portion of the document. 1306 // To do that we need to find out what the affected offsets are; we know 1307 // the MultiTextEdit's affected range, but that is referring to offsets 1308 // in the old document. Use that to compute offsets in the new document. 1309 //int distanceFromEnd = actual.length() - edit.getExclusiveEnd(); 1310 //IStructuredModel model = DomUtilities.createStructuredModel(actual); 1311 //int start = edit.getOffset(); 1312 //int end = actual.length() - distanceFromEnd; 1313 //int length = end - start; 1314 //TextEdit format = AndroidXmlFormattingStrategy.format(model, start, length); 1315 EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); 1316 String formatted = EclipseXmlPrettyPrinter.prettyPrint(actual, formatPrefs, style, 1317 null /*lineSeparator*/); 1318 1319 1320 // Figure out how much of the before and after strings are identical and narrow 1321 // the replacement scope 1322 boolean foundDifference = false; 1323 int firstDifference = 0; 1324 int lastDifference = formatted.length(); 1325 int start = 0; 1326 int end = oldContents.length(); 1327 1328 for (int i = 0, j = start; i < formatted.length() && j < end; i++, j++) { 1329 if (formatted.charAt(i) != oldContents.charAt(j)) { 1330 firstDifference = i; 1331 foundDifference = true; 1332 break; 1333 } 1334 } 1335 1336 if (!foundDifference) { 1337 // No differences - the document is already formatted, nothing to do 1338 return null; 1339 } 1340 1341 lastDifference = firstDifference + 1; 1342 for (int i = formatted.length() - 1, j = end - 1; 1343 i > firstDifference && j > start; 1344 i--, j--) { 1345 if (formatted.charAt(i) != oldContents.charAt(j)) { 1346 lastDifference = i + 1; 1347 break; 1348 } 1349 } 1350 1351 start += firstDifference; 1352 end -= (formatted.length() - lastDifference); 1353 end = Math.max(start, end); 1354 formatted = formatted.substring(firstDifference, lastDifference); 1355 1356 ReplaceEdit format = new ReplaceEdit(start, end - start, 1357 formatted); 1358 1359 MultiTextEdit newEdit = new MultiTextEdit(); 1360 newEdit.addChild(format); 1361 1362 return newEdit; 1363 } 1364 getElementDescriptor(String fqcn)1365 protected ViewElementDescriptor getElementDescriptor(String fqcn) { 1366 AndroidTargetData data = mDelegate.getEditor().getTargetData(); 1367 if (data != null) { 1368 return data.getLayoutDescriptors().findDescriptorByClass(fqcn); 1369 } 1370 1371 return null; 1372 } 1373 1374 /** Create a wizard for this refactoring */ createWizard()1375 abstract VisualRefactoringWizard createWizard(); 1376 1377 public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor { 1378 private final Map<String, String> mArguments; 1379 VisualRefactoringDescriptor( String id, String project, String description, String comment, Map<String, String> arguments)1380 public VisualRefactoringDescriptor( 1381 String id, String project, String description, String comment, 1382 Map<String, String> arguments) { 1383 super(id, project, description, comment, STRUCTURAL_CHANGE | MULTI_CHANGE); 1384 mArguments = arguments; 1385 } 1386 getArguments()1387 public Map<String, String> getArguments() { 1388 return mArguments; 1389 } 1390 createRefactoring(Map<String, String> args)1391 protected abstract Refactoring createRefactoring(Map<String, String> args); 1392 1393 @Override createRefactoring(RefactoringStatus status)1394 public Refactoring createRefactoring(RefactoringStatus status) throws CoreException { 1395 try { 1396 return createRefactoring(mArguments); 1397 } catch (NullPointerException e) { 1398 status.addFatalError("Failed to recreate refactoring from descriptor"); 1399 return null; 1400 } 1401 } 1402 } 1403 } 1404