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.AndroidConstants.FD_RES_LAYOUT;
     19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME;
     20 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
     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.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
     28 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML;
     29 import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML;
     30 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
     31 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
     32 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON;
     33 import static com.android.resources.ResourceType.LAYOUT;
     34 import static com.android.sdklib.SdkConstants.FD_RES;
     35 
     36 import com.android.AndroidConstants;
     37 import com.android.annotations.VisibleForTesting;
     38 import com.android.ide.eclipse.adt.AdtConstants;
     39 import com.android.ide.eclipse.adt.AdtPlugin;
     40 import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences;
     41 import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle;
     42 import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter;
     43 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     44 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
     45 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
     46 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     47 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     48 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     49 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
     50 import com.android.sdklib.SdkConstants;
     51 
     52 import org.eclipse.core.resources.IContainer;
     53 import org.eclipse.core.resources.IFile;
     54 import org.eclipse.core.resources.IFolder;
     55 import org.eclipse.core.resources.IProject;
     56 import org.eclipse.core.resources.IResource;
     57 import org.eclipse.core.runtime.CoreException;
     58 import org.eclipse.core.runtime.IPath;
     59 import org.eclipse.core.runtime.IProgressMonitor;
     60 import org.eclipse.core.runtime.OperationCanceledException;
     61 import org.eclipse.core.runtime.Path;
     62 import org.eclipse.jface.dialogs.IInputValidator;
     63 import org.eclipse.jface.text.ITextSelection;
     64 import org.eclipse.jface.viewers.ITreeSelection;
     65 import org.eclipse.ltk.core.refactoring.Change;
     66 import org.eclipse.ltk.core.refactoring.NullChange;
     67 import org.eclipse.ltk.core.refactoring.Refactoring;
     68 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     69 import org.eclipse.ltk.core.refactoring.TextFileChange;
     70 import org.eclipse.swt.widgets.Display;
     71 import org.eclipse.text.edits.InsertEdit;
     72 import org.eclipse.text.edits.MultiTextEdit;
     73 import org.eclipse.text.edits.ReplaceEdit;
     74 import org.eclipse.text.edits.TextEdit;
     75 import org.eclipse.ui.IWorkbenchPage;
     76 import org.eclipse.wst.sse.core.StructuredModelManager;
     77 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     78 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     79 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     80 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     81 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
     82 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
     83 import org.w3c.dom.Attr;
     84 import org.w3c.dom.Document;
     85 import org.w3c.dom.Element;
     86 import org.w3c.dom.NamedNodeMap;
     87 import org.w3c.dom.Node;
     88 
     89 import java.io.IOException;
     90 import java.util.ArrayList;
     91 import java.util.List;
     92 import java.util.Locale;
     93 import java.util.Map;
     94 
     95 /**
     96  * Extracts the selection and writes it out as a separate layout file, then adds an
     97  * include to that new layout file. Interactively asks the user for a new name for the
     98  * layout.
     99  */
    100 @SuppressWarnings("restriction") // XML model
    101 public class ExtractIncludeRefactoring extends VisualRefactoring {
    102     private static final String KEY_NAME = "name";                      //$NON-NLS-1$
    103     private static final String KEY_OCCURRENCES = "all-occurrences";    //$NON-NLS-1$
    104     private String mLayoutName;
    105     private boolean mReplaceOccurrences;
    106 
    107     /**
    108      * This constructor is solely used by {@link Descriptor},
    109      * to replay a previous refactoring.
    110      * @param arguments argument map created by #createArgumentMap.
    111      */
    112     ExtractIncludeRefactoring(Map<String, String> arguments) {
    113         super(arguments);
    114         mLayoutName = arguments.get(KEY_NAME);
    115         mReplaceOccurrences  = Boolean.parseBoolean(arguments.get(KEY_OCCURRENCES));
    116     }
    117 
    118     public ExtractIncludeRefactoring(
    119             IFile file,
    120             LayoutEditorDelegate delegate,
    121             ITextSelection selection,
    122             ITreeSelection treeSelection) {
    123         super(file, delegate, selection, treeSelection);
    124     }
    125 
    126     @VisibleForTesting
    127     ExtractIncludeRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
    128         super(selectedElements, editor);
    129     }
    130 
    131     @Override
    132     public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
    133             OperationCanceledException {
    134         RefactoringStatus status = new RefactoringStatus();
    135 
    136         try {
    137             pm.beginTask("Checking preconditions...", 6);
    138 
    139             if (mSelectionStart == -1 || mSelectionEnd == -1) {
    140                 status.addFatalError("No selection to extract");
    141                 return status;
    142             }
    143 
    144             // Make sure the selection is contiguous
    145             if (mTreeSelection != null) {
    146                 // TODO - don't do this if we based the selection on text. In this case,
    147                 // make sure we're -balanced-.
    148                 List<CanvasViewInfo> infos = getSelectedViewInfos();
    149                 if (!validateNotEmpty(infos, status)) {
    150                     return status;
    151                 }
    152 
    153                 if (!validateNotRoot(infos, status)) {
    154                     return status;
    155                 }
    156 
    157                 // Disable if you've selected a single include tag
    158                 if (infos.size() == 1) {
    159                     UiViewElementNode uiNode = infos.get(0).getUiViewNode();
    160                     if (uiNode != null) {
    161                         Node xmlNode = uiNode.getXmlNode();
    162                         if (xmlNode.getLocalName().equals(LayoutDescriptors.VIEW_INCLUDE)) {
    163                             status.addWarning("No point in refactoring a single include tag");
    164                         }
    165                     }
    166                 }
    167 
    168                 // Enforce that the selection is -contiguous-
    169                 if (!validateContiguous(infos, status)) {
    170                     return status;
    171                 }
    172             }
    173 
    174             // This also ensures that we have a valid DOM model:
    175             if (mElements.size() == 0) {
    176                 status.addFatalError("Nothing to extract");
    177                 return status;
    178             }
    179 
    180             pm.worked(1);
    181             return status;
    182 
    183         } finally {
    184             pm.done();
    185         }
    186     }
    187 
    188     @Override
    189     protected VisualRefactoringDescriptor createDescriptor() {
    190         String comment = getName();
    191         return new Descriptor(
    192                 mProject.getName(), //project
    193                 comment, //description
    194                 comment, //comment
    195                 createArgumentMap());
    196     }
    197 
    198     @Override
    199     protected Map<String, String> createArgumentMap() {
    200         Map<String, String> args = super.createArgumentMap();
    201         args.put(KEY_NAME, mLayoutName);
    202         args.put(KEY_OCCURRENCES, Boolean.toString(mReplaceOccurrences));
    203 
    204         return args;
    205     }
    206 
    207     @Override
    208     public String getName() {
    209         return "Extract as Include";
    210     }
    211 
    212     void setLayoutName(String layoutName) {
    213         mLayoutName = layoutName;
    214     }
    215 
    216     void setReplaceOccurrences(boolean selection) {
    217         mReplaceOccurrences = selection;
    218     }
    219 
    220     // ---- Actual implementation of Extract as Include modification computation ----
    221 
    222     @Override
    223     protected List<Change> computeChanges(IProgressMonitor monitor) {
    224         String extractedText = getExtractedText();
    225 
    226         String namespaceDeclarations = computeNamespaceDeclarations();
    227 
    228         // Insert namespace:
    229         extractedText = insertNamespace(extractedText, namespaceDeclarations);
    230 
    231         StringBuilder sb = new StringBuilder();
    232         sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
    233         sb.append(extractedText);
    234         sb.append('\n');
    235 
    236         List<Change> changes = new ArrayList<Change>();
    237 
    238         String newFileName = mLayoutName + DOT_XML;
    239         IProject project = mDelegate.getEditor().getProject();
    240         IFile sourceFile = mDelegate.getEditor().getInputFile();
    241         if (sourceFile == null) {
    242             return changes;
    243         }
    244 
    245         // Replace extracted elements by <include> tag
    246         handleIncludingFile(changes, sourceFile, mSelectionStart, mSelectionEnd,
    247                 getDomDocument(), getPrimaryElement());
    248 
    249         // Also extract in other variations of the same file (landscape/portrait, etc)
    250         boolean haveVariations = false;
    251         if (mReplaceOccurrences) {
    252             List<IFile> layouts = getOtherLayouts(sourceFile);
    253             for (IFile file : layouts) {
    254                 IModelManager modelManager = StructuredModelManager.getModelManager();
    255                 IStructuredModel model = null;
    256                 // We could enhance this with a SubMonitor to make the progress bar move as
    257                 // well.
    258                 monitor.subTask(String.format("Looking for duplicates in %1$s",
    259                         file.getProjectRelativePath()));
    260                 if (monitor.isCanceled()) {
    261                     throw new OperationCanceledException();
    262                 }
    263 
    264                 try {
    265                     model = modelManager.getModelForRead(file);
    266                     if (model instanceof IDOMModel) {
    267                         IDOMModel domModel = (IDOMModel) model;
    268                         IDOMDocument otherDocument = domModel.getDocument();
    269                         List<Element> otherElements = new ArrayList<Element>();
    270                         Element otherPrimary = null;
    271 
    272                         for (Element element : getElements()) {
    273                             Element other = DomUtilities.findCorresponding(element,
    274                                     otherDocument);
    275                             if (other != null) {
    276                                 // See if the structure is similar to what we have in this
    277                                 // document
    278                                 if (DomUtilities.isEquivalent(element, other)) {
    279                                     otherElements.add(other);
    280                                     if (element == getPrimaryElement()) {
    281                                         otherPrimary = other;
    282                                     }
    283                                 }
    284                             }
    285                         }
    286 
    287                         // Only perform extract in the other file if we find a match for
    288                         // ALL of elements being extracted, and if they too are contiguous
    289                         if (otherElements.size() == getElements().size() &&
    290                                 DomUtilities.isContiguous(otherElements)) {
    291                             // Find the range
    292                             int begin = Integer.MAX_VALUE;
    293                             int end = Integer.MIN_VALUE;
    294                             for (Element element : otherElements) {
    295                                 // Yes!! Extract this one as well!
    296                                 IndexedRegion region = getRegion(element);
    297                                 end = Math.max(end, region.getEndOffset());
    298                                 begin = Math.min(begin, region.getStartOffset());
    299                             }
    300                             handleIncludingFile(changes, file, begin,
    301                                     end, otherDocument, otherPrimary);
    302                             haveVariations = true;
    303                         }
    304                     }
    305                 } catch (IOException e) {
    306                     AdtPlugin.log(e, null);
    307                 } catch (CoreException e) {
    308                     AdtPlugin.log(e, null);
    309                 } finally {
    310                     if (model != null) {
    311                         model.releaseFromRead();
    312                     }
    313                 }
    314             }
    315         }
    316 
    317         // Add change to create the new file
    318         IContainer parent = sourceFile.getParent();
    319         if (haveVariations) {
    320             // If we're extracting from multiple configuration folders, then we need to
    321             // place the extracted include in the base layout folder (if not it goes next to
    322             // the including file)
    323             parent = mProject.getFolder(FD_RES).getFolder(FD_RES_LAYOUT);
    324         }
    325         IPath parentPath = parent.getProjectRelativePath();
    326         final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName));
    327         TextFileChange addFile = new TextFileChange("Create new separate layout", file);
    328         addFile.setTextType(AdtConstants.EXT_XML);
    329         changes.add(addFile);
    330 
    331         String newFile = sb.toString();
    332         if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    333             newFile = XmlPrettyPrinter.prettyPrint(newFile,
    334                     XmlFormatPreferences.create(), XmlFormatStyle.LAYOUT, null /*lineSeparator*/);
    335         }
    336         addFile.setEdit(new InsertEdit(0, newFile));
    337 
    338         Change finishHook = createFinishHook(file);
    339         changes.add(finishHook);
    340 
    341         return changes;
    342     }
    343 
    344     private void handleIncludingFile(List<Change> changes,
    345             IFile sourceFile, int begin, int end, Document document, Element primary) {
    346         TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
    347         MultiTextEdit rootEdit = new MultiTextEdit();
    348         change.setTextType(EXT_XML);
    349         changes.add(change);
    350 
    351         String referenceId = getReferenceId();
    352         // Replace existing elements in the source file and insert <include>
    353         String androidNsPrefix = getAndroidNamespacePrefix(document);
    354         String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId);
    355         int length = end - begin;
    356         ReplaceEdit replace = new ReplaceEdit(begin, length, include);
    357         rootEdit.addChild(replace);
    358 
    359         // Update any layout references to the old id with the new id
    360         if (referenceId != null && primary != null) {
    361             String rootId = getId(primary);
    362             IStructuredModel model = null;
    363             try {
    364                 model = StructuredModelManager.getModelManager().getModelForRead(sourceFile);
    365                 IStructuredDocument doc = model.getStructuredDocument();
    366                 if (doc != null && rootId != null) {
    367                     List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin,
    368                             end, rootId, referenceId);
    369                     for (TextEdit edit : replaceIds) {
    370                         rootEdit.addChild(edit);
    371                     }
    372 
    373                     if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    374                         MultiTextEdit formatted = reformat(doc.get(), rootEdit,
    375                                 XmlFormatStyle.LAYOUT);
    376                         if (formatted != null) {
    377                             rootEdit = formatted;
    378                         }
    379                     }
    380                 }
    381             } catch (IOException e) {
    382                 AdtPlugin.log(e, null);
    383             } catch (CoreException e) {
    384                 AdtPlugin.log(e, null);
    385             } finally {
    386                 if (model != null) {
    387                     model.releaseFromRead();
    388                 }
    389             }
    390         }
    391 
    392         change.setEdit(rootEdit);
    393     }
    394 
    395     /**
    396      * Returns a list of all the other layouts (in all configurations) in the project other
    397      * than the given source layout where the refactoring was initiated. Never null.
    398      */
    399     private List<IFile> getOtherLayouts(IFile sourceFile) {
    400         List<IFile> layouts = new ArrayList<IFile>(100);
    401         IPath sourcePath = sourceFile.getProjectRelativePath();
    402         IFolder resources = mProject.getFolder(SdkConstants.FD_RESOURCES);
    403         try {
    404             for (IResource folder : resources.members()) {
    405                 if (folder.getName().startsWith(AndroidConstants.FD_RES_LAYOUT) &&
    406                         folder instanceof IFolder) {
    407                     IFolder layoutFolder = (IFolder) folder;
    408                     for (IResource file : layoutFolder.members()) {
    409                         if (file.getName().endsWith(EXT_XML)
    410                                 && file instanceof IFile) {
    411                             if (!file.getProjectRelativePath().equals(sourcePath)) {
    412                                 layouts.add((IFile) file);
    413                             }
    414                         }
    415                     }
    416                 }
    417             }
    418         } catch (CoreException e) {
    419             AdtPlugin.log(e, null);
    420         }
    421 
    422         return layouts;
    423     }
    424 
    425     String getInitialName() {
    426         String defaultName = ""; //$NON-NLS-1$
    427         Element primary = getPrimaryElement();
    428         if (primary != null) {
    429             String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
    430             // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
    431             if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) {
    432                 // Use everything following the id/, and make it lowercase since that is
    433                 // the convention for layouts (and use Locale.US to ensure that "Image" becomes
    434                 // "image" etc)
    435                 defaultName = id.substring(id.indexOf('/') + 1).toLowerCase(Locale.US);
    436 
    437                 IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT);
    438 
    439                 if (validator.isValid(defaultName) != null) { // Already exists?
    440                     defaultName = ""; //$NON-NLS-1$
    441                 }
    442             }
    443         }
    444 
    445         return defaultName;
    446     }
    447 
    448     IFile getSourceFile() {
    449         return mFile;
    450     }
    451 
    452     private Change createFinishHook(final IFile file) {
    453         return new NullChange("Open extracted layout and refresh resources") {
    454             @Override
    455             public Change perform(IProgressMonitor pm) throws CoreException {
    456                 Display display = AdtPlugin.getDisplay();
    457                 display.asyncExec(new Runnable() {
    458                     @Override
    459                     public void run() {
    460                         openFile(file);
    461                         mDelegate.getGraphicalEditor().refreshProjectResources();
    462                         // Save file to trigger include finder scanning (as well as making
    463                         // the
    464                         // actual show-include feature work since it relies on reading
    465                         // files from
    466                         // disk, not a live buffer)
    467                         IWorkbenchPage page = mDelegate.getEditor().getEditorSite().getPage();
    468                         page.saveEditor(mDelegate.getEditor(), false);
    469                     }
    470                 });
    471 
    472                 // Not undoable: just return null instead of an undo-change.
    473                 return null;
    474             }
    475         };
    476     }
    477 
    478     private String computeNamespaceDeclarations() {
    479         String androidNsPrefix = null;
    480         String namespaceDeclarations = null;
    481 
    482         StringBuilder sb = new StringBuilder();
    483         List<Attr> attributeNodes = findNamespaceAttributes();
    484         for (Node attributeNode : attributeNodes) {
    485             String prefix = attributeNode.getPrefix();
    486             if (XMLNS.equals(prefix)) {
    487                 sb.append(' ');
    488                 String name = attributeNode.getNodeName();
    489                 sb.append(name);
    490                 sb.append('=').append('"');
    491 
    492                 String value = attributeNode.getNodeValue();
    493                 if (value.equals(ANDROID_URI)) {
    494                     androidNsPrefix = name;
    495                     if (androidNsPrefix.startsWith(XMLNS_COLON)) {
    496                         androidNsPrefix = androidNsPrefix.substring(XMLNS_COLON.length());
    497                     }
    498                 }
    499                 sb.append(DomUtilities.toXmlAttributeValue(value));
    500                 sb.append('"');
    501             }
    502         }
    503         namespaceDeclarations = sb.toString();
    504 
    505         if (androidNsPrefix == null) {
    506             androidNsPrefix = ANDROID_NS_NAME;
    507         }
    508 
    509         if (namespaceDeclarations.length() == 0) {
    510             sb.setLength(0);
    511             sb.append(' ');
    512             sb.append(XMLNS_COLON);
    513             sb.append(androidNsPrefix);
    514             sb.append('=').append('"');
    515             sb.append(ANDROID_URI);
    516             sb.append('"');
    517             namespaceDeclarations = sb.toString();
    518         }
    519 
    520         return namespaceDeclarations;
    521     }
    522 
    523     /** Returns the id to be used for the include tag itself (may be null) */
    524     private String getReferenceId() {
    525         String rootId = getRootId();
    526         if (rootId != null) {
    527             return rootId + "_ref";
    528         }
    529 
    530         return null;
    531     }
    532 
    533     /**
    534      * Compute the actual {@code <include>} string to be inserted in place of the old
    535      * selection
    536      */
    537     private static String computeIncludeString(Element primaryNode, String newName,
    538             String androidNsPrefix, String referenceId) {
    539         StringBuilder sb = new StringBuilder();
    540         sb.append("<include layout=\"@layout/"); //$NON-NLS-1$
    541         sb.append(newName);
    542         sb.append('"');
    543         sb.append(' ');
    544 
    545         // Create new id for the include itself
    546         if (referenceId != null) {
    547             sb.append(androidNsPrefix);
    548             sb.append(':');
    549             sb.append(ATTR_ID);
    550             sb.append('=').append('"');
    551             sb.append(referenceId);
    552             sb.append('"').append(' ');
    553         }
    554 
    555         // Add id string, unless it's a <merge>, since we may need to adjust any layout
    556         // references to apply to the <include> tag instead
    557 
    558         // I should move all the layout_ attributes as well
    559         // I also need to duplicate and modify the id and then replace
    560         // everything else in the file with this new id...
    561 
    562         // HACK: see issue 13494: We must duplicate the width/height attributes on the
    563         // <include> statement for designtime rendering only
    564         String width = null;
    565         String height = null;
    566         if (primaryNode == null) {
    567             // Multiple selection - in that case we will be creating an outer <merge>
    568             // so we need to set our own width/height on it
    569             width = height = VALUE_WRAP_CONTENT;
    570         } else {
    571             if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
    572                 width = VALUE_WRAP_CONTENT;
    573             } else {
    574                 width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
    575             }
    576             if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
    577                 height = VALUE_WRAP_CONTENT;
    578             } else {
    579                 height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
    580             }
    581         }
    582         if (width != null) {
    583             sb.append(' ');
    584             sb.append(androidNsPrefix);
    585             sb.append(':');
    586             sb.append(ATTR_LAYOUT_WIDTH);
    587             sb.append('=').append('"');
    588             sb.append(DomUtilities.toXmlAttributeValue(width));
    589             sb.append('"');
    590         }
    591         if (height != null) {
    592             sb.append(' ');
    593             sb.append(androidNsPrefix);
    594             sb.append(':');
    595             sb.append(ATTR_LAYOUT_HEIGHT);
    596             sb.append('=').append('"');
    597             sb.append(DomUtilities.toXmlAttributeValue(height));
    598             sb.append('"');
    599         }
    600 
    601         // Duplicate all the other layout attributes as well
    602         if (primaryNode != null) {
    603             NamedNodeMap attributes = primaryNode.getAttributes();
    604             for (int i = 0, n = attributes.getLength(); i < n; i++) {
    605                 Node attr = attributes.item(i);
    606                 String name = attr.getLocalName();
    607                 if (name.startsWith(ATTR_LAYOUT_PREFIX)
    608                         && ANDROID_URI.equals(attr.getNamespaceURI())) {
    609                     if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
    610                         // Already handled
    611                         continue;
    612                     }
    613 
    614                     sb.append(' ');
    615                     sb.append(androidNsPrefix);
    616                     sb.append(':');
    617                     sb.append(name);
    618                     sb.append('=').append('"');
    619                     sb.append(DomUtilities.toXmlAttributeValue(attr.getNodeValue()));
    620                     sb.append('"');
    621                 }
    622             }
    623         }
    624 
    625         sb.append("/>");
    626         return sb.toString();
    627     }
    628 
    629     /** Return the text in the document in the range start to end */
    630     private String getExtractedText() {
    631         String xml = getText(mSelectionStart, mSelectionEnd);
    632         Element primaryNode = getPrimaryElement();
    633         xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml);
    634         xml = dedent(xml);
    635 
    636         // Wrap siblings in <merge>?
    637         if (primaryNode == null) {
    638             StringBuilder sb = new StringBuilder();
    639             sb.append("<merge>\n"); //$NON-NLS-1$
    640             // indent an extra level
    641             for (String line : xml.split("\n")) { //$NON-NLS-1$
    642                 sb.append("    "); //$NON-NLS-1$
    643                 sb.append(line).append('\n');
    644             }
    645             sb.append("</merge>\n"); //$NON-NLS-1$
    646             xml = sb.toString();
    647         }
    648 
    649         return xml;
    650     }
    651 
    652     @Override
    653     VisualRefactoringWizard createWizard() {
    654         return new ExtractIncludeWizard(this, mDelegate);
    655     }
    656 
    657     public static class Descriptor extends VisualRefactoringDescriptor {
    658         public Descriptor(String project, String description, String comment,
    659                 Map<String, String> arguments) {
    660             super("com.android.ide.eclipse.adt.refactoring.extract.include", //$NON-NLS-1$
    661                     project, description, comment, arguments);
    662         }
    663 
    664         @Override
    665         protected Refactoring createRefactoring(Map<String, String> args) {
    666             return new ExtractIncludeRefactoring(args);
    667         }
    668     }
    669 }
    670