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 */ 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 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 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 236 protected abstract List<Change> computeChanges(IProgressMonitor monitor); 237 238 @Override 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 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 285 protected abstract VisualRefactoringDescriptor createDescriptor(); 286 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 297 IFile getFile() { 298 return mFile; 299 } 300 301 // ---- Shared functionality ---- 302 303 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 */ 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 */ 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 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 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 471 protected List<Attr> findNamespaceAttributes() { 472 Document document = getDomDocument(); 473 return findNamespaceAttributes(document); 474 } 475 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 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 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 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 */ 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 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 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 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 654 protected List<Element> getElements() { 655 return mElements; 656 } 657 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 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 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 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 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 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 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 */ 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 */ 876 public static IndexedRegion getRegion(Node node) { 877 if (node instanceof IndexedRegion) { 878 return (IndexedRegion) node; 879 } 880 881 return null; 882 } 883 884 protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix) { 885 return ensureHasId(rootEdit, element, prefix, true); 886 } 887 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 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 */ 936 public static String getId(Element element) { 937 return element.getAttributeNS(ANDROID_URI, ATTR_ID); 938 } 939 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 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 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 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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 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 */ 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 */ 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 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 */ 1375 abstract VisualRefactoringWizard createWizard(); 1376 1377 public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor { 1378 private final Map<String, String> mArguments; 1379 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 1387 public Map<String, String> getArguments() { 1388 return mArguments; 1389 } 1390 1391 protected abstract Refactoring createRefactoring(Map<String, String> args); 1392 1393 @Override 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