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