| /* |
| * 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; |
| } |
| } |
| } |
| } |