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