Home | History | Annotate | Download | only in refactoring
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
     17 
     18 import static com.android.SdkConstants.ANDROID_NS_NAME;
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_ID;
     21 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     24 import static com.android.SdkConstants.DOT_XML;
     25 import static com.android.SdkConstants.EXT_XML;
     26 import static com.android.SdkConstants.FD_RES;
     27 import static com.android.SdkConstants.FD_RESOURCES;
     28 import static com.android.SdkConstants.FD_RES_LAYOUT;
     29 import static com.android.SdkConstants.ID_PREFIX;
     30 import static com.android.SdkConstants.NEW_ID_PREFIX;
     31 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
     32 import static com.android.SdkConstants.VIEW_INCLUDE;
     33 import static com.android.SdkConstants.XMLNS;
     34 import static com.android.SdkConstants.XMLNS_PREFIX;
     35 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
     36 import static com.android.resources.ResourceType.LAYOUT;
     37 
     38 import com.android.annotations.NonNull;
     39 import com.android.annotations.VisibleForTesting;
     40 import com.android.ide.common.xml.XmlFormatStyle;
     41 import com.android.ide.eclipse.adt.AdtPlugin;
     42 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
     43 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
     44 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     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.utils.XmlUtils;
     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(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 @NonNull 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(EXT_XML);
    329         changes.add(addFile);
    330 
    331         String newFile = sb.toString();
    332         if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    333             newFile = EclipseXmlPrettyPrinter.prettyPrint(newFile,
    334                     EclipseXmlFormatPreferences.create(), XmlFormatStyle.LAYOUT,
    335                     null /*lineSeparator*/);
    336         }
    337         addFile.setEdit(new InsertEdit(0, newFile));
    338 
    339         Change finishHook = createFinishHook(file);
    340         changes.add(finishHook);
    341 
    342         return changes;
    343     }
    344 
    345     private void handleIncludingFile(List<Change> changes,
    346             IFile sourceFile, int begin, int end, Document document, Element primary) {
    347         TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
    348         MultiTextEdit rootEdit = new MultiTextEdit();
    349         change.setTextType(EXT_XML);
    350         changes.add(change);
    351 
    352         String referenceId = getReferenceId();
    353         // Replace existing elements in the source file and insert <include>
    354         String androidNsPrefix = getAndroidNamespacePrefix(document);
    355         String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId);
    356         int length = end - begin;
    357         ReplaceEdit replace = new ReplaceEdit(begin, length, include);
    358         rootEdit.addChild(replace);
    359 
    360         // Update any layout references to the old id with the new id
    361         if (referenceId != null && primary != null) {
    362             String rootId = getId(primary);
    363             IStructuredModel model = null;
    364             try {
    365                 model = StructuredModelManager.getModelManager().getModelForRead(sourceFile);
    366                 IStructuredDocument doc = model.getStructuredDocument();
    367                 if (doc != null && rootId != null) {
    368                     List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin,
    369                             end, rootId, referenceId);
    370                     for (TextEdit edit : replaceIds) {
    371                         rootEdit.addChild(edit);
    372                     }
    373 
    374                     if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    375                         MultiTextEdit formatted = reformat(doc.get(), rootEdit,
    376                                 XmlFormatStyle.LAYOUT);
    377                         if (formatted != null) {
    378                             rootEdit = formatted;
    379                         }
    380                     }
    381                 }
    382             } catch (IOException e) {
    383                 AdtPlugin.log(e, null);
    384             } catch (CoreException e) {
    385                 AdtPlugin.log(e, null);
    386             } finally {
    387                 if (model != null) {
    388                     model.releaseFromRead();
    389                 }
    390             }
    391         }
    392 
    393         change.setEdit(rootEdit);
    394     }
    395 
    396     /**
    397      * Returns a list of all the other layouts (in all configurations) in the project other
    398      * than the given source layout where the refactoring was initiated. Never null.
    399      */
    400     private List<IFile> getOtherLayouts(IFile sourceFile) {
    401         List<IFile> layouts = new ArrayList<IFile>(100);
    402         IPath sourcePath = sourceFile.getProjectRelativePath();
    403         IFolder resources = mProject.getFolder(FD_RESOURCES);
    404         try {
    405             for (IResource folder : resources.members()) {
    406                 if (folder.getName().startsWith(FD_RES_LAYOUT) &&
    407                         folder instanceof IFolder) {
    408                     IFolder layoutFolder = (IFolder) folder;
    409                     for (IResource file : layoutFolder.members()) {
    410                         if (file.getName().endsWith(EXT_XML)
    411                                 && file instanceof IFile) {
    412                             if (!file.getProjectRelativePath().equals(sourcePath)) {
    413                                 layouts.add((IFile) file);
    414                             }
    415                         }
    416                     }
    417                 }
    418             }
    419         } catch (CoreException e) {
    420             AdtPlugin.log(e, null);
    421         }
    422 
    423         return layouts;
    424     }
    425 
    426     String getInitialName() {
    427         String defaultName = ""; //$NON-NLS-1$
    428         Element primary = getPrimaryElement();
    429         if (primary != null) {
    430             String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
    431             // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
    432             if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) {
    433                 // Use everything following the id/, and make it lowercase since that is
    434                 // the convention for layouts (and use Locale.US to ensure that "Image" becomes
    435                 // "image" etc)
    436                 defaultName = id.substring(id.indexOf('/') + 1).toLowerCase(Locale.US);
    437 
    438                 IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT);
    439 
    440                 if (validator.isValid(defaultName) != null) { // Already exists?
    441                     defaultName = ""; //$NON-NLS-1$
    442                 }
    443             }
    444         }
    445 
    446         return defaultName;
    447     }
    448 
    449     IFile getSourceFile() {
    450         return mFile;
    451     }
    452 
    453     private Change createFinishHook(final IFile file) {
    454         return new NullChange("Open extracted layout and refresh resources") {
    455             @Override
    456             public Change perform(IProgressMonitor pm) throws CoreException {
    457                 Display display = AdtPlugin.getDisplay();
    458                 display.asyncExec(new Runnable() {
    459                     @Override
    460                     public void run() {
    461                         openFile(file);
    462                         mDelegate.getGraphicalEditor().refreshProjectResources();
    463                         // Save file to trigger include finder scanning (as well as making
    464                         // the
    465                         // actual show-include feature work since it relies on reading
    466                         // files from
    467                         // disk, not a live buffer)
    468                         IWorkbenchPage page = mDelegate.getEditor().getEditorSite().getPage();
    469                         page.saveEditor(mDelegate.getEditor(), false);
    470                     }
    471                 });
    472 
    473                 // Not undoable: just return null instead of an undo-change.
    474                 return null;
    475             }
    476         };
    477     }
    478 
    479     private String computeNamespaceDeclarations() {
    480         String androidNsPrefix = null;
    481         String namespaceDeclarations = null;
    482 
    483         StringBuilder sb = new StringBuilder();
    484         List<Attr> attributeNodes = findNamespaceAttributes();
    485         for (Node attributeNode : attributeNodes) {
    486             String prefix = attributeNode.getPrefix();
    487             if (XMLNS.equals(prefix)) {
    488                 sb.append(' ');
    489                 String name = attributeNode.getNodeName();
    490                 sb.append(name);
    491                 sb.append('=').append('"');
    492 
    493                 String value = attributeNode.getNodeValue();
    494                 if (value.equals(ANDROID_URI)) {
    495                     androidNsPrefix = name;
    496                     if (androidNsPrefix.startsWith(XMLNS_PREFIX)) {
    497                         androidNsPrefix = androidNsPrefix.substring(XMLNS_PREFIX.length());
    498                     }
    499                 }
    500                 sb.append(XmlUtils.toXmlAttributeValue(value));
    501                 sb.append('"');
    502             }
    503         }
    504         namespaceDeclarations = sb.toString();
    505 
    506         if (androidNsPrefix == null) {
    507             androidNsPrefix = ANDROID_NS_NAME;
    508         }
    509 
    510         if (namespaceDeclarations.length() == 0) {
    511             sb.setLength(0);
    512             sb.append(' ');
    513             sb.append(XMLNS_PREFIX);
    514             sb.append(androidNsPrefix);
    515             sb.append('=').append('"');
    516             sb.append(ANDROID_URI);
    517             sb.append('"');
    518             namespaceDeclarations = sb.toString();
    519         }
    520 
    521         return namespaceDeclarations;
    522     }
    523 
    524     /** Returns the id to be used for the include tag itself (may be null) */
    525     private String getReferenceId() {
    526         String rootId = getRootId();
    527         if (rootId != null) {
    528             return rootId + "_ref";
    529         }
    530 
    531         return null;
    532     }
    533 
    534     /**
    535      * Compute the actual {@code <include>} string to be inserted in place of the old
    536      * selection
    537      */
    538     private static String computeIncludeString(Element primaryNode, String newName,
    539             String androidNsPrefix, String referenceId) {
    540         StringBuilder sb = new StringBuilder();
    541         sb.append("<include layout=\"@layout/"); //$NON-NLS-1$
    542         sb.append(newName);
    543         sb.append('"');
    544         sb.append(' ');
    545 
    546         // Create new id for the include itself
    547         if (referenceId != null) {
    548             sb.append(androidNsPrefix);
    549             sb.append(':');
    550             sb.append(ATTR_ID);
    551             sb.append('=').append('"');
    552             sb.append(referenceId);
    553             sb.append('"').append(' ');
    554         }
    555 
    556         // Add id string, unless it's a <merge>, since we may need to adjust any layout
    557         // references to apply to the <include> tag instead
    558 
    559         // I should move all the layout_ attributes as well
    560         // I also need to duplicate and modify the id and then replace
    561         // everything else in the file with this new id...
    562 
    563         // HACK: see issue 13494: We must duplicate the width/height attributes on the
    564         // <include> statement for designtime rendering only
    565         String width = null;
    566         String height = null;
    567         if (primaryNode == null) {
    568             // Multiple selection - in that case we will be creating an outer <merge>
    569             // so we need to set our own width/height on it
    570             width = height = VALUE_WRAP_CONTENT;
    571         } else {
    572             if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
    573                 width = VALUE_WRAP_CONTENT;
    574             } else {
    575                 width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
    576             }
    577             if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
    578                 height = VALUE_WRAP_CONTENT;
    579             } else {
    580                 height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
    581             }
    582         }
    583         if (width != null) {
    584             sb.append(' ');
    585             sb.append(androidNsPrefix);
    586             sb.append(':');
    587             sb.append(ATTR_LAYOUT_WIDTH);
    588             sb.append('=').append('"');
    589             sb.append(XmlUtils.toXmlAttributeValue(width));
    590             sb.append('"');
    591         }
    592         if (height != null) {
    593             sb.append(' ');
    594             sb.append(androidNsPrefix);
    595             sb.append(':');
    596             sb.append(ATTR_LAYOUT_HEIGHT);
    597             sb.append('=').append('"');
    598             sb.append(XmlUtils.toXmlAttributeValue(height));
    599             sb.append('"');
    600         }
    601 
    602         // Duplicate all the other layout attributes as well
    603         if (primaryNode != null) {
    604             NamedNodeMap attributes = primaryNode.getAttributes();
    605             for (int i = 0, n = attributes.getLength(); i < n; i++) {
    606                 Node attr = attributes.item(i);
    607                 String name = attr.getLocalName();
    608                 if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
    609                         && ANDROID_URI.equals(attr.getNamespaceURI())) {
    610                     if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
    611                         // Already handled
    612                         continue;
    613                     }
    614 
    615                     sb.append(' ');
    616                     sb.append(androidNsPrefix);
    617                     sb.append(':');
    618                     sb.append(name);
    619                     sb.append('=').append('"');
    620                     sb.append(XmlUtils.toXmlAttributeValue(attr.getNodeValue()));
    621                     sb.append('"');
    622                 }
    623             }
    624         }
    625 
    626         sb.append("/>");
    627         return sb.toString();
    628     }
    629 
    630     /** Return the text in the document in the range start to end */
    631     private String getExtractedText() {
    632         String xml = getText(mSelectionStart, mSelectionEnd);
    633         Element primaryNode = getPrimaryElement();
    634         xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml);
    635         xml = dedent(xml);
    636 
    637         // Wrap siblings in <merge>?
    638         if (primaryNode == null) {
    639             StringBuilder sb = new StringBuilder();
    640             sb.append("<merge>\n"); //$NON-NLS-1$
    641             // indent an extra level
    642             for (String line : xml.split("\n")) { //$NON-NLS-1$
    643                 sb.append("    "); //$NON-NLS-1$
    644                 sb.append(line).append('\n');
    645             }
    646             sb.append("</merge>\n"); //$NON-NLS-1$
    647             xml = sb.toString();
    648         }
    649 
    650         return xml;
    651     }
    652 
    653     @Override
    654     VisualRefactoringWizard createWizard() {
    655         return new ExtractIncludeWizard(this, mDelegate);
    656     }
    657 
    658     public static class Descriptor extends VisualRefactoringDescriptor {
    659         public Descriptor(String project, String description, String comment,
    660                 Map<String, String> arguments) {
    661             super("com.android.ide.eclipse.adt.refactoring.extract.include", //$NON-NLS-1$
    662                     project, description, comment, arguments);
    663         }
    664 
    665         @Override
    666         protected Refactoring createRefactoring(Map<String, String> args) {
    667             return new ExtractIncludeRefactoring(args);
    668         }
    669     }
    670 }
    671