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