aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java1403
1 files changed, 1403 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
new file mode 100644
index 000000000..904a3a084
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
@@ -0,0 +1,1403 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.XMLNS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.text.edits.DeleteEdit;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MalformedTreeException;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parent class for the various visual refactoring operations; contains shared
+ * implementations needed by most of them
+ */
+@SuppressWarnings("restriction") // XML model
+public abstract class VisualRefactoring extends Refactoring {
+ private static final String KEY_FILE = "file"; //$NON-NLS-1$
+ private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$
+ private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$
+ private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$
+
+ protected final IFile mFile;
+ protected final LayoutEditorDelegate mDelegate;
+ protected final IProject mProject;
+ protected int mSelectionStart = -1;
+ protected int mSelectionEnd = -1;
+ protected final List<Element> mElements;
+ protected final ITreeSelection mTreeSelection;
+ protected final ITextSelection mSelection;
+ /** Same as {@link #mSelectionStart} but not adjusted to element edges */
+ protected int mOriginalSelectionStart = -1;
+ /** Same as {@link #mSelectionEnd} but not adjusted to element edges */
+ protected int mOriginalSelectionEnd = -1;
+
+ protected final Map<Element, String> mGeneratedIdMap = new HashMap<Element, String>();
+ protected final Set<String> mGeneratedIds = new HashSet<String>();
+
+ protected List<Change> mChanges;
+ private String mAndroidNamespacePrefix;
+
+ /**
+ * This constructor is solely used by {@link VisualRefactoringDescriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ VisualRefactoring(Map<String, String> arguments) {
+ IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
+ mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ path = Path.fromPortableString(arguments.get(KEY_FILE));
+ mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
+ mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END));
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+ mDelegate = null;
+ mElements = null;
+ mSelection = null;
+ mTreeSelection = null;
+ }
+
+ @VisibleForTesting
+ VisualRefactoring(List<Element> elements, LayoutEditorDelegate delegate) {
+ mElements = elements;
+ mDelegate = delegate;
+
+ mFile = delegate != null ? delegate.getEditor().getInputFile() : null;
+ mProject = delegate != null ? delegate.getEditor().getProject() : null;
+ mSelectionStart = 0;
+ mSelectionEnd = 0;
+ mOriginalSelectionStart = 0;
+ mOriginalSelectionEnd = 0;
+ mSelection = null;
+ mTreeSelection = null;
+
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (Element element : elements) {
+ if (element instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) element;
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ mOriginalSelectionStart = start;
+ mOriginalSelectionEnd = end;
+ }
+ }
+
+ public VisualRefactoring(IFile file, LayoutEditorDelegate editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ mFile = file;
+ mDelegate = editor;
+ mProject = file.getProject();
+ mSelection = selection;
+ mTreeSelection = treeSelection;
+
+ // Initialize mSelectionStart and mSelectionEnd based on the selection context, which
+ // is either a treeSelection (when invoked from the layout editor or the outline), or
+ // a selection (when invoked from an XML editor)
+ if (treeSelection != null) {
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (TreePath path : treeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment;
+ UiViewElementNode uiNode = viewInfo.getUiViewNode();
+ if (uiNode == null) {
+ continue;
+ }
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xmlNode;
+
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+ }
+ if (selection != null) {
+ mOriginalSelectionStart = selection.getOffset();
+ mOriginalSelectionEnd = mOriginalSelectionStart + selection.getLength();
+ }
+ } else if (selection != null) {
+ // TODO: update selection to boundaries!
+ mSelectionStart = selection.getOffset();
+ mSelectionEnd = mSelectionStart + selection.getLength();
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+ }
+
+ mElements = initElements();
+ }
+
+ @NonNull
+ protected abstract List<Change> computeChanges(IProgressMonitor monitor);
+
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+ mChanges = new ArrayList<Change>();
+ try {
+ monitor.beginTask("Checking post-conditions...", 5);
+
+ // Reset state for each computeChanges call, in case the user goes back
+ // and forth in the refactoring wizard
+ mGeneratedIdMap.clear();
+ mGeneratedIds.clear();
+ List<Change> changes = computeChanges(monitor);
+ mChanges.addAll(changes);
+
+ monitor.worked(1);
+ } finally {
+ monitor.done();
+ }
+
+ return status;
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ try {
+ monitor.beginTask("Applying changes...", 1);
+
+ CompositeChange change = new CompositeChange(
+ getName(),
+ mChanges.toArray(new Change[mChanges.size()])) {
+ @Override
+ public ChangeDescriptor getDescriptor() {
+ VisualRefactoringDescriptor desc = createDescriptor();
+ return new RefactoringChangeDescriptor(desc);
+ }
+ };
+
+ monitor.worked(1);
+ return change;
+
+ } finally {
+ monitor.done();
+ }
+ }
+
+ protected abstract VisualRefactoringDescriptor createDescriptor();
+
+ protected Map<String, String> createArgumentMap() {
+ HashMap<String, String> args = new HashMap<String, String>();
+ args.put(KEY_PROJECT, mProject.getFullPath().toPortableString());
+ args.put(KEY_FILE, mFile.getFullPath().toPortableString());
+ args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
+ args.put(KEY_SEL_END, Integer.toString(mSelectionEnd));
+
+ return args;
+ }
+
+ IFile getFile() {
+ return mFile;
+ }
+
+ // ---- Shared functionality ----
+
+
+ protected void openFile(IFile file) {
+ GraphicalEditorPart graphicalEditor = mDelegate.getGraphicalEditor();
+ IFile leavingFile = graphicalEditor.getEditedFile();
+
+ try {
+ // Duplicate the current state into the newly created file
+ String state = ConfigurationDescription.getDescription(leavingFile);
+
+ // TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current
+ // theme to show.
+
+ file.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state);
+ } catch (CoreException e) {
+ // pass
+ }
+
+ /* TBD: "Show Included In" if supported.
+ * Not sure if this is a good idea.
+ if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ try {
+ Reference include = Reference.create(graphicalEditor.getEditedFile());
+ file.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, include);
+ } catch (CoreException e) {
+ // pass - worst that can happen is that we don't start with inclusion
+ }
+ }
+ */
+
+ try {
+ IEditorPart part =
+ IDE.openEditor(mDelegate.getEditor().getEditorSite().getPage(), file);
+ if (part instanceof AndroidXmlEditor && AdtPrefs.getPrefs().getFormatGuiXml()) {
+ AndroidXmlEditor newEditor = (AndroidXmlEditor) part;
+ newEditor.reformatDocument();
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open new included layout");
+ }
+ }
+
+
+ /** Produce a list of edits to replace references to the given id with the given new id */
+ protected static List<TextEdit> replaceIds(String androidNamePrefix,
+ IStructuredDocument doc, int skipStart, int skipEnd,
+ String rootId, String referenceId) {
+ if (rootId == null) {
+ return Collections.emptyList();
+ }
+
+ // We need to search for either @+id/ or @id/
+ String match1 = rootId;
+ String match2;
+ if (match1.startsWith(ID_PREFIX)) {
+ match2 = '"' + NEW_ID_PREFIX + match1.substring(ID_PREFIX.length()) + '"';
+ match1 = '"' + match1 + '"';
+ } else if (match1.startsWith(NEW_ID_PREFIX)) {
+ match2 = '"' + ID_PREFIX + match1.substring(NEW_ID_PREFIX.length()) + '"';
+ match1 = '"' + match1 + '"';
+ } else {
+ return Collections.emptyList();
+ }
+
+ String namePrefix = androidNamePrefix + ':' + ATTR_LAYOUT_RESOURCE_PREFIX;
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+
+ IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion();
+ for (; region != null; region = region.getNext()) {
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+
+ // Look at all attribute values and look for an id reference match
+ String attributeName = ""; //$NON-NLS-1$
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ attributeName = region.getText(subRegion);
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ // Only replace references in layout attributes
+ if (!attributeName.startsWith(namePrefix)) {
+ continue;
+ }
+ // Skip occurrences in the given skip range
+ int subRegionStart = regionStart + subRegion.getStart();
+ if (subRegionStart >= skipStart && subRegionStart <= skipEnd) {
+ continue;
+ }
+
+ String attributeValue = region.getText(subRegion);
+ if (attributeValue.equals(match1) || attributeValue.equals(match2)) {
+ int start = subRegionStart + 1; // skip quote
+ int end = start + rootId.length();
+
+ edits.add(new ReplaceEdit(start, end - start, referenceId));
+ }
+ }
+ }
+ }
+
+ return edits;
+ }
+
+ /** Get the id of the root selected element, if any */
+ protected String getRootId() {
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String oldId = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
+ // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
+ if (oldId != null && oldId.length() > 0) {
+ return oldId;
+ }
+ }
+
+ return null;
+ }
+
+ protected String getAndroidNamespacePrefix() {
+ if (mAndroidNamespacePrefix == null) {
+ List<Attr> attributeNodes = findNamespaceAttributes();
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ String name = attributeNode.getNodeName();
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ mAndroidNamespacePrefix = name;
+ if (mAndroidNamespacePrefix.startsWith(XMLNS_PREFIX)) {
+ mAndroidNamespacePrefix =
+ mAndroidNamespacePrefix.substring(XMLNS_PREFIX.length());
+ }
+ }
+ }
+ }
+
+ if (mAndroidNamespacePrefix == null) {
+ mAndroidNamespacePrefix = ANDROID_NS_NAME;
+ }
+ }
+
+ return mAndroidNamespacePrefix;
+ }
+
+ protected static String getAndroidNamespacePrefix(Document document) {
+ String nsPrefix = null;
+ List<Attr> attributeNodes = findNamespaceAttributes(document);
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ String name = attributeNode.getNodeName();
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ nsPrefix = name;
+ if (nsPrefix.startsWith(XMLNS_PREFIX)) {
+ nsPrefix =
+ nsPrefix.substring(XMLNS_PREFIX.length());
+ }
+ }
+ }
+ }
+
+ if (nsPrefix == null) {
+ nsPrefix = ANDROID_NS_NAME;
+ }
+
+ return nsPrefix;
+ }
+
+ protected List<Attr> findNamespaceAttributes() {
+ Document document = getDomDocument();
+ return findNamespaceAttributes(document);
+ }
+
+ protected static List<Attr> findNamespaceAttributes(Document document) {
+ if (document != null) {
+ Element root = document.getDocumentElement();
+ return findNamespaceAttributes(root);
+ }
+
+ return Collections.emptyList();
+ }
+
+ protected static List<Attr> findNamespaceAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ protected List<Attr> findLayoutAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String name = attributeNode.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ protected String insertNamespace(String xmlText, String namespaceDeclarations) {
+ // Insert namespace declarations into the extracted XML fragment
+ int firstSpace = xmlText.indexOf(' ');
+ int elementEnd = xmlText.indexOf('>');
+ int insertAt;
+ if (firstSpace != -1 && firstSpace < elementEnd) {
+ insertAt = firstSpace;
+ } else {
+ insertAt = elementEnd;
+ }
+ xmlText = xmlText.substring(0, insertAt) + namespaceDeclarations
+ + xmlText.substring(insertAt);
+
+ return xmlText;
+ }
+
+ /** Remove sections of the document that correspond to top level layout attributes;
+ * these are placed on the include element instead */
+ protected String stripTopLayoutAttributes(Element primary, int start, String xml) {
+ if (primary != null) {
+ // List of attributes to remove
+ List<IndexedRegion> skip = new ArrayList<IndexedRegion>();
+ NamedNodeMap attributes = primary.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attr = attributes.item(i);
+ String name = attr.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attr.getNamespaceURI())) {
+ if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // These are special and are left in
+ continue;
+ }
+
+ if (attr instanceof IndexedRegion) {
+ skip.add((IndexedRegion) attr);
+ }
+ }
+ }
+ if (skip.size() > 0) {
+ Collections.sort(skip, new Comparator<IndexedRegion>() {
+ // Sort in start order
+ @Override
+ public int compare(IndexedRegion r1, IndexedRegion r2) {
+ return r1.getStartOffset() - r2.getStartOffset();
+ }
+ });
+
+ // Successively cut out the various layout attributes
+ // TODO remove adjacent whitespace too (but not newlines, unless they
+ // are newly adjacent)
+ StringBuilder sb = new StringBuilder(xml.length());
+ int nextStart = 0;
+
+ // Copy out all the sections except the skip sections
+ for (IndexedRegion r : skip) {
+ int regionStart = r.getStartOffset();
+ // Adjust to string offsets since we've copied the string out of
+ // the document
+ regionStart -= start;
+
+ sb.append(xml.substring(nextStart, regionStart));
+
+ nextStart = regionStart + r.getLength();
+ }
+ if (nextStart < xml.length()) {
+ sb.append(xml.substring(nextStart));
+ }
+
+ return sb.toString();
+ }
+ }
+
+ return xml;
+ }
+
+ protected static String getIndent(String line, int max) {
+ int i = 0;
+ int n = Math.min(max, line.length());
+ for (; i < n; i++) {
+ char c = line.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ return line.substring(0, i);
+ }
+ }
+
+ if (n < line.length()) {
+ return line.substring(0, n);
+ } else {
+ return line;
+ }
+ }
+
+ protected static String dedent(String xml) {
+ String[] lines = xml.split("\n"); //$NON-NLS-1$
+ if (lines.length < 2) {
+ // The first line never has any indentation since we copy it out from the
+ // element start index
+ return xml;
+ }
+
+ String indentPrefix = getIndent(lines[1], lines[1].length());
+ for (int i = 2, n = lines.length; i < n; i++) {
+ String line = lines[i];
+
+ // Ignore blank lines
+ if (line.trim().length() == 0) {
+ continue;
+ }
+
+ indentPrefix = getIndent(line, indentPrefix.length());
+
+ if (indentPrefix.length() == 0) {
+ return xml;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String line : lines) {
+ if (line.startsWith(indentPrefix)) {
+ sb.append(line.substring(indentPrefix.length()));
+ } else {
+ sb.append(line);
+ }
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+
+ protected String getText(int start, int end) {
+ try {
+ IStructuredDocument document = mDelegate.getEditor().getStructuredDocument();
+ return document.get(start, end - start);
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ return null;
+ }
+ }
+
+ protected List<Element> getElements() {
+ return mElements;
+ }
+
+ protected List<Element> initElements() {
+ List<Element> nodes = new ArrayList<Element>();
+
+ assert mTreeSelection == null || mSelection == null :
+ "treeSel= " + mTreeSelection + ", sel=" + mSelection;
+
+ // Initialize mSelectionStart and mSelectionEnd based on the selection context, which
+ // is either a treeSelection (when invoked from the layout editor or the outline), or
+ // a selection (when invoked from an XML editor)
+ if (mTreeSelection != null) {
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (TreePath path : mTreeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment;
+ UiViewElementNode uiNode = viewInfo.getUiViewNode();
+ if (uiNode == null) {
+ continue;
+ }
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode instanceof Element) {
+ Element element = (Element) xmlNode;
+ nodes.add(element);
+ IndexedRegion region = getRegion(element);
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ }
+ } else if (mSelection != null) {
+ mSelectionStart = mSelection.getOffset();
+ mSelectionEnd = mSelectionStart + mSelection.getLength();
+ mOriginalSelectionStart = mSelectionStart;
+ mOriginalSelectionEnd = mSelectionEnd;
+
+ // Figure out the range of selected nodes from the document offsets
+ IStructuredDocument doc = mDelegate.getEditor().getStructuredDocument();
+ Pair<Element, Element> range = DomUtilities.getElementRange(doc,
+ mSelectionStart, mSelectionEnd);
+ if (range != null) {
+ Element first = range.getFirst();
+ Element last = range.getSecond();
+
+ // Adjust offsets to get rid of surrounding text nodes (if you happened
+ // to select a text range and included whitespace on either end etc)
+ mSelectionStart = getRegion(first).getStartOffset();
+ mSelectionEnd = getRegion(last).getEndOffset();
+
+ if (mSelectionStart > mSelectionEnd) {
+ int tmp = mSelectionStart;
+ mSelectionStart = mSelectionEnd;
+ mSelectionEnd = tmp;
+ }
+
+ if (first == last) {
+ nodes.add(first);
+ } else if (first.getParentNode() == last.getParentNode()) {
+ // Add the range
+ Node node = first;
+ while (node != null) {
+ if (node instanceof Element) {
+ nodes.add((Element) node);
+ }
+ if (node == last) {
+ break;
+ }
+ node = node.getNextSibling();
+ }
+ } else {
+ // Different parents: this means we have an uneven selection, selecting
+ // elements from different levels. We can't extract ranges like that.
+ }
+ }
+ } else {
+ assert false;
+ }
+
+ // Make sure that the list of elements is unique
+ //Set<Element> seen = new HashSet<Element>();
+ //for (Element element : nodes) {
+ // assert !seen.contains(element) : element;
+ // seen.add(element);
+ //}
+
+ return nodes;
+ }
+
+ protected Element getPrimaryElement() {
+ List<Element> elements = getElements();
+ if (elements != null && elements.size() == 1) {
+ return elements.get(0);
+ }
+
+ return null;
+ }
+
+ protected Document getDomDocument() {
+ if (mDelegate.getUiRootNode() != null) {
+ return mDelegate.getUiRootNode().getXmlDocument();
+ } else {
+ return getElements().get(0).getOwnerDocument();
+ }
+ }
+
+ protected List<CanvasViewInfo> getSelectedViewInfos() {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mTreeSelection != null) {
+ for (TreePath path : mTreeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ infos.add((CanvasViewInfo) lastSegment);
+ }
+ }
+ }
+ return infos;
+ }
+
+ protected boolean validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ if (infos.size() == 0) {
+ status.addFatalError("No selection to extract");
+ return false;
+ }
+
+ return true;
+ }
+
+ protected boolean validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ for (CanvasViewInfo info : infos) {
+ if (info.isRoot()) {
+ status.addFatalError("Cannot refactor the root");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected boolean validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ if (infos.size() > 1) {
+ // All elements must be siblings (e.g. same parent)
+ List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
+ .size());
+ for (CanvasViewInfo info : infos) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ if (nodes.size() == 0) {
+ status.addFatalError("No selected views");
+ return false;
+ }
+
+ UiElementNode parent = nodes.get(0).getUiParent();
+ for (UiViewElementNode node : nodes) {
+ if (parent != node.getUiParent()) {
+ status.addFatalError("The selected elements must be adjacent");
+ return false;
+ }
+ }
+ // Ensure that the siblings are contiguous; no gaps.
+ // If we've selected all the children of the parent then we don't need
+ // to look.
+ List<UiElementNode> siblings = parent.getUiChildren();
+ if (siblings.size() != nodes.size()) {
+ Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
+ boolean inRange = false;
+ int remaining = nodes.size();
+ for (UiElementNode node : siblings) {
+ boolean in = nodeSet.contains(node);
+ if (in) {
+ remaining--;
+ if (remaining == 0) {
+ break;
+ }
+ inRange = true;
+ } else if (inRange) {
+ status.addFatalError("The selected elements must be adjacent");
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Updates the given element with a new name if the current id reflects the old
+ * element type. If the name was changed, it will return the new name.
+ */
+ protected String ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit) {
+ String oldType = element.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ String oldTypeBase = oldType.substring(oldType.lastIndexOf('.') + 1);
+ String id = getId(element);
+ if (id == null || id.length() == 0
+ || id.toLowerCase(Locale.US).contains(oldTypeBase.toLowerCase(Locale.US))) {
+ String newTypeBase = newType.substring(newType.lastIndexOf('.') + 1);
+ return ensureHasId(rootEdit, element, newTypeBase);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link IndexedRegion} for the given node
+ *
+ * @param node the node to look up the region for
+ * @return the corresponding region, or null
+ */
+ public static IndexedRegion getRegion(Node node) {
+ if (node instanceof IndexedRegion) {
+ return (IndexedRegion) node;
+ }
+
+ return null;
+ }
+
+ protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix) {
+ return ensureHasId(rootEdit, element, prefix, true);
+ }
+
+ protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix,
+ boolean apply) {
+ String id = mGeneratedIdMap.get(element);
+ if (id != null) {
+ return NEW_ID_PREFIX + id;
+ }
+
+ if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID)
+ || (prefix != null && !getId(element).startsWith(prefix))) {
+ id = DomUtilities.getFreeWidgetId(element, mGeneratedIds, prefix);
+ // Make sure we don't use this one again
+ mGeneratedIds.add(id);
+ mGeneratedIdMap.put(element, id);
+ id = NEW_ID_PREFIX + id;
+ if (apply) {
+ setAttribute(rootEdit, element,
+ ANDROID_URI, getAndroidNamespacePrefix(), ATTR_ID, id);
+ }
+ return id;
+ }
+
+ return getId(element);
+ }
+
+ protected int getFirstAttributeOffset(Element element) {
+ IndexedRegion region = getRegion(element);
+ if (region != null) {
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ String name = element.getLocalName();
+ int nameOffset = text.indexOf(name);
+ if (nameOffset != -1) {
+ return startOffset + nameOffset + name.length();
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the id of the given element
+ *
+ * @param element the element to look up the id for
+ * @return the corresponding id, or an empty string (should not be null
+ * according to the DOM API, but has been observed to be null on
+ * some versions of Eclipse)
+ */
+ public static String getId(Element element) {
+ return element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ }
+
+ protected String ensureNewId(String id) {
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ } else if (!id.startsWith(NEW_ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id;
+ }
+ } else {
+ id = null;
+ }
+
+ return id;
+ }
+
+ protected String getViewClass(String fqcn) {
+ // Don't include android.widget. as a package prefix in layout files
+ if (fqcn.startsWith(ANDROID_WIDGET_PREFIX)) {
+ fqcn = fqcn.substring(ANDROID_WIDGET_PREFIX.length());
+ }
+
+ return fqcn;
+ }
+
+ protected void setAttribute(MultiTextEdit rootEdit, Element element,
+ String attributeUri,
+ String attributePrefix, String attributeName, String attributeValue) {
+ int offset = getFirstAttributeOffset(element);
+ if (offset != -1) {
+ if (element.hasAttributeNS(attributeUri, attributeName)) {
+ replaceAttributeDeclaration(rootEdit, offset, element, attributePrefix,
+ attributeUri, attributeName, attributeValue);
+ } else {
+ addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName,
+ attributeValue);
+ }
+ }
+ }
+
+ private void addAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ String attributePrefix, String attributeName, String attributeValue) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(' ');
+
+ if (attributePrefix != null) {
+ sb.append(attributePrefix).append(':');
+ }
+ sb.append(attributeName).append('=').append('"');
+ sb.append(attributeValue).append('"');
+
+ InsertEdit setAttribute = new InsertEdit(offset, sb.toString());
+ rootEdit.addChild(setAttribute);
+ }
+
+ /** Replaces the value declaration of the given attribute */
+ private void replaceAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ Element element, String attributePrefix, String attributeUri,
+ String attributeName, String attributeValue) {
+ // Find attribute value and replace it
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+
+ int valueStart = -1;
+ boolean useNextValue = false;
+ String targetName = attributePrefix != null
+ ? attributePrefix + ':' + attributeName : attributeName;
+
+ // Look at all attribute values and look for an id reference match
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ // What about prefix?
+ if (targetName.equals(region.getText(subRegion))) {
+ useNextValue = true;
+ }
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ if (useNextValue) {
+ valueStart = regionStart + subRegion.getStart();
+ break;
+ }
+ }
+ }
+
+ if (valueStart != -1) {
+ String oldValue = element.getAttributeNS(attributeUri, attributeName);
+ int start = valueStart + 1; // Skip opening "
+ ReplaceEdit setAttribute = new ReplaceEdit(start, oldValue.length(),
+ attributeValue);
+ try {
+ rootEdit.addChild(setAttribute);
+ } catch (MalformedTreeException mte) {
+ AdtPlugin.log(mte, "Could not replace attribute %1$s with %2$s",
+ attributeName, attributeValue);
+ throw mte;
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ /** Strips out the given attribute, if defined */
+ protected void removeAttribute(MultiTextEdit rootEdit, Element element, String uri,
+ String attributeName) {
+ if (element.hasAttributeNS(uri, attributeName)) {
+ Attr attribute = element.getAttributeNodeNS(uri, attributeName);
+ removeAttribute(rootEdit, attribute);
+ }
+ }
+
+ /** Strips out the given attribute, if defined */
+ protected void removeAttribute(MultiTextEdit rootEdit, Attr attribute) {
+ IndexedRegion region = getRegion(attribute);
+ if (region != null) {
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ rootEdit.addChild(deletion);
+ }
+ }
+
+
+ /**
+ * Removes the given element's opening and closing tags (including all of its
+ * attributes) but leaves any children alone
+ *
+ * @param rootEdit the multi edit to add the removal operation to
+ * @param element the element to delete the open and closing tags for
+ * @param skip a list of elements that should not be modified (for example because they
+ * are targeted for deletion)
+ *
+ * TODO: Rename this to "unwrap" ? And allow for handling nested deletions.
+ */
+ protected void removeElementTags(MultiTextEdit rootEdit, Element element, List<Element> skip,
+ boolean changeIndentation) {
+ IndexedRegion elementRegion = getRegion(element);
+ if (elementRegion == null) {
+ return;
+ }
+
+ // Look for the opening tag
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ int startLineInclusive = -1;
+ int endLineInclusive = -1;
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ int start = elementRegion.getStartOffset();
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+ int startOffset = regionStart;
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_OPEN.equals(type)) {
+ startOffset = regionStart + subRegion.getStart();
+ } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) {
+ int endOffset = regionStart + subRegion.getStart() + subRegion.getLength();
+
+ DeleteEdit deletion = createDeletion(doc, startOffset, endOffset);
+ rootEdit.addChild(deletion);
+ startLineInclusive = doc.getLineOfOffset(endOffset) + 1;
+ break;
+ }
+ }
+
+ // Find the close tag
+ // Look at all attribute values and look for an id reference match
+ region = doc.getRegionAtCharacterOffset(elementRegion.getEndOffset()
+ - element.getTagName().length() - 1);
+ list = region.getRegions();
+ regionStart = region.getStartOffset();
+ startOffset = -1;
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
+ startOffset = regionStart + subRegion.getStart();
+ } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) {
+ int endOffset = regionStart + subRegion.getStart() + subRegion.getLength();
+ if (startOffset != -1) {
+ DeleteEdit deletion = createDeletion(doc, startOffset, endOffset);
+ rootEdit.addChild(deletion);
+ endLineInclusive = doc.getLineOfOffset(startOffset) - 1;
+ }
+ break;
+ }
+ }
+ }
+
+ // Dedent the contents
+ if (changeIndentation && startLineInclusive != -1 && endLineInclusive != -1) {
+ String indent = AndroidXmlEditor.getIndentAtOffset(doc, getRegion(element)
+ .getStartOffset());
+ setIndentation(rootEdit, indent, doc, startLineInclusive, endLineInclusive,
+ element, skip);
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ protected void removeIndentation(MultiTextEdit rootEdit, String removeIndent,
+ IStructuredDocument doc, int startLineInclusive, int endLineInclusive,
+ Element element, List<Element> skip) {
+ if (startLineInclusive > endLineInclusive) {
+ return;
+ }
+ int indentLength = removeIndent.length();
+ if (indentLength == 0) {
+ return;
+ }
+
+ try {
+ for (int line = startLineInclusive; line <= endLineInclusive; line++) {
+ IRegion info = doc.getLineInformation(line);
+ int lineStart = info.getOffset();
+ int lineLength = info.getLength();
+ int lineEnd = lineStart + lineLength;
+ if (overlaps(lineStart, lineEnd, element, skip)) {
+ continue;
+ }
+ String lineText = getText(lineStart,
+ lineStart + Math.min(lineLength, indentLength));
+ if (lineText.startsWith(removeIndent)) {
+ rootEdit.addChild(new DeleteEdit(lineStart, indentLength));
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ protected void setIndentation(MultiTextEdit rootEdit, String indent,
+ IStructuredDocument doc, int startLineInclusive, int endLineInclusive,
+ Element element, List<Element> skip) {
+ if (startLineInclusive > endLineInclusive) {
+ return;
+ }
+ int indentLength = indent.length();
+ if (indentLength == 0) {
+ return;
+ }
+
+ try {
+ for (int line = startLineInclusive; line <= endLineInclusive; line++) {
+ IRegion info = doc.getLineInformation(line);
+ int lineStart = info.getOffset();
+ int lineLength = info.getLength();
+ int lineEnd = lineStart + lineLength;
+ if (overlaps(lineStart, lineEnd, element, skip)) {
+ continue;
+ }
+ String lineText = getText(lineStart, lineStart + lineLength);
+ int indentEnd = getFirstNonSpace(lineText);
+ rootEdit.addChild(new ReplaceEdit(lineStart, indentEnd, indent));
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private int getFirstNonSpace(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return i;
+ }
+ }
+
+ return s.length();
+ }
+
+ /** Returns true if the given line overlaps any of the given elements */
+ private static boolean overlaps(int startOffset, int endOffset,
+ Element element, List<Element> overlaps) {
+ for (Element e : overlaps) {
+ if (e == element) {
+ continue;
+ }
+
+ IndexedRegion region = getRegion(e);
+ if (region.getEndOffset() >= startOffset && region.getStartOffset() <= endOffset) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected DeleteEdit createDeletion(IStructuredDocument doc, int startOffset, int endOffset) {
+ // Expand to delete the whole line?
+ try {
+ IRegion info = doc.getLineInformationOfOffset(startOffset);
+ int lineBegin = info.getOffset();
+ // Is the text on the line leading up to the deletion region,
+ // and the text following it, all whitespace?
+ boolean deleteLine = true;
+ if (lineBegin < startOffset) {
+ String prefix = getText(lineBegin, startOffset);
+ if (prefix.trim().length() > 0) {
+ deleteLine = false;
+ }
+ }
+ info = doc.getLineInformationOfOffset(endOffset);
+ int lineEnd = info.getOffset() + info.getLength();
+ if (lineEnd > endOffset) {
+ String suffix = getText(endOffset, lineEnd);
+ if (suffix.trim().length() > 0) {
+ deleteLine = false;
+ }
+ }
+ if (deleteLine) {
+ startOffset = lineBegin;
+ endOffset = Math.min(doc.getLength(), lineEnd + 1);
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+
+ return new DeleteEdit(startOffset, endOffset - startOffset);
+ }
+
+ /**
+ * Rewrite the edits in the given {@link MultiTextEdit} such that same edits are
+ * applied, but the resulting range is also formatted
+ */
+ protected MultiTextEdit reformat(MultiTextEdit edit, XmlFormatStyle style) {
+ String xml = mDelegate.getEditor().getStructuredDocument().get();
+ return reformat(xml, edit, style);
+ }
+
+ /**
+ * Rewrite the edits in the given {@link MultiTextEdit} such that same edits are
+ * applied, but the resulting range is also formatted
+ *
+ * @param oldContents the original contents that should be edited by a
+ * {@link MultiTextEdit}
+ * @param edit the {@link MultiTextEdit} to be applied to some string
+ * @param style the formatting style to use
+ * @return a new {@link MultiTextEdit} which performs the same edits as the input edit
+ * but also reformats the text
+ */
+ public static MultiTextEdit reformat(String oldContents, MultiTextEdit edit,
+ XmlFormatStyle style) {
+ IDocument document = new org.eclipse.jface.text.Document();
+ document.set(oldContents);
+
+ try {
+ edit.apply(document);
+ } catch (MalformedTreeException e) {
+ AdtPlugin.log(e, null);
+ return null; // Abort formatting
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ return null; // Abort formatting
+ }
+
+ String actual = document.get();
+
+ // TODO: Try to format only the affected portion of the document.
+ // To do that we need to find out what the affected offsets are; we know
+ // the MultiTextEdit's affected range, but that is referring to offsets
+ // in the old document. Use that to compute offsets in the new document.
+ //int distanceFromEnd = actual.length() - edit.getExclusiveEnd();
+ //IStructuredModel model = DomUtilities.createStructuredModel(actual);
+ //int start = edit.getOffset();
+ //int end = actual.length() - distanceFromEnd;
+ //int length = end - start;
+ //TextEdit format = AndroidXmlFormattingStrategy.format(model, start, length);
+ EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create();
+ String formatted = EclipseXmlPrettyPrinter.prettyPrint(actual, formatPrefs, style,
+ null /*lineSeparator*/);
+
+
+ // Figure out how much of the before and after strings are identical and narrow
+ // the replacement scope
+ boolean foundDifference = false;
+ int firstDifference = 0;
+ int lastDifference = formatted.length();
+ int start = 0;
+ int end = oldContents.length();
+
+ for (int i = 0, j = start; i < formatted.length() && j < end; i++, j++) {
+ if (formatted.charAt(i) != oldContents.charAt(j)) {
+ firstDifference = i;
+ foundDifference = true;
+ break;
+ }
+ }
+
+ if (!foundDifference) {
+ // No differences - the document is already formatted, nothing to do
+ return null;
+ }
+
+ lastDifference = firstDifference + 1;
+ for (int i = formatted.length() - 1, j = end - 1;
+ i > firstDifference && j > start;
+ i--, j--) {
+ if (formatted.charAt(i) != oldContents.charAt(j)) {
+ lastDifference = i + 1;
+ break;
+ }
+ }
+
+ start += firstDifference;
+ end -= (formatted.length() - lastDifference);
+ end = Math.max(start, end);
+ formatted = formatted.substring(firstDifference, lastDifference);
+
+ ReplaceEdit format = new ReplaceEdit(start, end - start,
+ formatted);
+
+ MultiTextEdit newEdit = new MultiTextEdit();
+ newEdit.addChild(format);
+
+ return newEdit;
+ }
+
+ protected ViewElementDescriptor getElementDescriptor(String fqcn) {
+ AndroidTargetData data = mDelegate.getEditor().getTargetData();
+ if (data != null) {
+ return data.getLayoutDescriptors().findDescriptorByClass(fqcn);
+ }
+
+ return null;
+ }
+
+ /** Create a wizard for this refactoring */
+ abstract VisualRefactoringWizard createWizard();
+
+ public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor {
+ private final Map<String, String> mArguments;
+
+ public VisualRefactoringDescriptor(
+ String id, String project, String description, String comment,
+ Map<String, String> arguments) {
+ super(id, project, description, comment, STRUCTURAL_CHANGE | MULTI_CHANGE);
+ mArguments = arguments;
+ }
+
+ public Map<String, String> getArguments() {
+ return mArguments;
+ }
+
+ protected abstract Refactoring createRefactoring(Map<String, String> args);
+
+ @Override
+ public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+ try {
+ return createRefactoring(mArguments);
+ } catch (NullPointerException e) {
+ status.addFatalError("Failed to recreate refactoring from descriptor");
+ return null;
+ }
+ }
+ }
+}