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