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