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