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 */ VisualRefactoring(Map<String, String> arguments)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 VisualRefactoring(List<Element> elements, LayoutEditorDelegate delegate)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 VisualRefactoring(IFile file, LayoutEditorDelegate editor, ITextSelection selection, ITreeSelection treeSelection)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 computeChanges(IProgressMonitor monitor)237 protected abstract List<Change> computeChanges(IProgressMonitor monitor); 238 239 @Override checkFinalConditions(IProgressMonitor monitor)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 createChange(IProgressMonitor monitor)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 createDescriptor()286 protected abstract VisualRefactoringDescriptor createDescriptor(); 287 createArgumentMap()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 openFile(IFile file)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 */ replaceIds(String androidNamePrefix, IStructuredDocument doc, int skipStart, int skipEnd, String rootId, String referenceId)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 */ getRootId()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 getAndroidNamespacePrefix()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 getAndroidNamespacePrefix(Document document)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 findNamespaceAttributes()469 protected List<Attr> findNamespaceAttributes() { 470 Document document = getDomDocument(); 471 return findNamespaceAttributes(document); 472 } 473 findNamespaceAttributes(Document document)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 findNamespaceAttributes(Node root)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 findLayoutAttributes(Node root)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 insertNamespace(String xmlText, String namespaceDeclarations)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 */ stripTopLayoutAttributes(Element primary, int start, String xml)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 getIndent(String line, int max)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 dedent(String xml)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 getText(int start, int end)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 getElements()652 protected List<Element> getElements() { 653 return mElements; 654 } 655 initElements()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 getPrimaryElement()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 getDomDocument()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 getSelectedViewInfos()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 validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status)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 validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status)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 validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status)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 */ ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit)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 */ getRegion(Node node)874 public static IndexedRegion getRegion(Node node) { 875 if (node instanceof IndexedRegion) { 876 return (IndexedRegion) node; 877 } 878 879 return null; 880 } 881 ensureHasId(MultiTextEdit rootEdit, Element element, String prefix)882 protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix) { 883 return ensureHasId(rootEdit, element, prefix, true); 884 } 885 ensureHasId(MultiTextEdit rootEdit, Element element, String prefix, boolean apply)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 getFirstAttributeOffset(Element element)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 */ getId(Element element)934 public static String getId(Element element) { 935 return element.getAttributeNS(ANDROID_URI, ATTR_ID); 936 } 937 ensureNewId(String id)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 getViewClass(String fqcn)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 setAttribute(MultiTextEdit rootEdit, Element element, String attributeUri, String attributePrefix, String attributeName, String attributeValue)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 addAttributeDeclaration(MultiTextEdit rootEdit, int offset, String attributePrefix, String attributeName, String attributeValue)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 */ replaceAttributeDeclaration(MultiTextEdit rootEdit, int offset, Element element, String attributePrefix, String attributeUri, String attributeName, String attributeValue)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 */ removeAttribute(MultiTextEdit rootEdit, Element element, String uri, String attributeName)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 */ removeAttribute(MultiTextEdit rootEdit, Attr attribute)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 */ removeElementTags(MultiTextEdit rootEdit, Element element, List<Element> skip, boolean changeIndentation)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 removeIndentation(MultiTextEdit rootEdit, String removeIndent, IStructuredDocument doc, int startLineInclusive, int endLineInclusive, Element element, List<Element> skip)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 setIndentation(MultiTextEdit rootEdit, String indent, IStructuredDocument doc, int startLineInclusive, int endLineInclusive, Element element, List<Element> skip)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 getFirstNonSpace(String s)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 */ overlaps(int startOffset, int endOffset, Element element, List<Element> overlaps)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 createDeletion(IStructuredDocument doc, int startOffset, int endOffset)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 */ reformat(MultiTextEdit edit, XmlFormatStyle style)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 */ reformat(String oldContents, MultiTextEdit edit, XmlFormatStyle style)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 getElementDescriptor(String fqcn)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 */ createWizard()1373 abstract VisualRefactoringWizard createWizard(); 1374 1375 public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor { 1376 private final Map<String, String> mArguments; 1377 VisualRefactoringDescriptor( String id, String project, String description, String comment, Map<String, String> arguments)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 getArguments()1385 public Map<String, String> getArguments() { 1386 return mArguments; 1387 } 1388 createRefactoring(Map<String, String> args)1389 protected abstract Refactoring createRefactoring(Map<String, String> args); 1390 1391 @Override createRefactoring(RefactoringStatus status)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