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