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