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