private int mIsEditXmlModelPending;
/**
+ * Usually null, but during an editing operation, represents the highest
+ * node which should be formatted when the editing operation is complete.
+ */
+ private UiElementNode mFormatNode;
+
+ /**
+ * Whether {@link #mFormatNode} should be formatted recursively, or just
+ * the node itself (its arguments)
+ */
+ private boolean mFormatChildren;
+
+ /**
* Creates a form editor.
* <p/>The editor will setup a {@link ITargetChangeListener} and call
* {@link #initUiRootNode(boolean)}, when the SDK or the target changes.
// Notify the model we're done modifying it. This must *always* be executed.
model.changedModel();
+ if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) {
+ if (!mFormatNode.hasError()) {
+ if (mFormatNode == getUiRootNode()) {
+ reformatDocument();
+ } else {
+ Node node = mFormatNode.getXmlNode();
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int begin = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (!mFormatChildren) {
+ // This will format just the attribute list
+ end = begin + 1;
+ }
+
+ model.aboutToChangeModel();
+ try {
+ reformatRegion(begin, end);
+ } finally {
+ model.changedModel();
+ }
+ }
+ }
+ }
+ mFormatNode = null;
+ mFormatChildren = false;
+ }
+
// Clean up the undo unit. This is done more than once as explained
// above for beginRecording.
for (int i = 0; i < undoReverseCount; i++) {
}
/**
+ * Does this editor participate in the "format GUI editor changes" option?
+ *
+ * @return true if this editor supports automatically formatting XML
+ * affected by GUI changes
+ */
+ public boolean supportsFormatOnGuiEdit() {
+ return false;
+ }
+
+ /**
+ * Mark the given node as needing to be formatted when the current edits are
+ * done, provided the user has turned that option on (see
+ * {@link AdtPrefs#getFormatGuiXml()}).
+ *
+ * @param node the node to be scheduled for formatting
+ * @param attributesOnly if true, only update the attributes list of the
+ * node, otherwise update the node recursively (e.g. all children
+ * too)
+ */
+ public void scheduleNodeReformat(UiElementNode node, boolean attributesOnly) {
+ if (!supportsFormatOnGuiEdit()) {
+ return;
+ }
+
+ if (node == mFormatNode) {
+ if (!attributesOnly) {
+ mFormatChildren = true;
+ }
+ } else if (mFormatNode == null) {
+ mFormatNode = node;
+ mFormatChildren = !attributesOnly;
+ } else {
+ if (mFormatNode.isAncestorOf(node)) {
+ mFormatChildren = true;
+ } else if (node.isAncestorOf(mFormatNode)) {
+ mFormatNode = node;
+ mFormatChildren = true;
+ } else {
+ // Two independent nodes; format their closest common ancestor.
+ // Later we could consider having a small number of independent nodes
+ // and formatting those, and only switching to formatting the common ancestor
+ // when the number of individual nodes gets large.
+ mFormatChildren = true;
+ mFormatNode = UiElementNode.getCommonAncestor(mFormatNode, node);
+ }
+ }
+ }
+
+ /**
* Creates an "undo recording" session by calling the undoableAction runnable
* under an undo session.
* <p/>
end = Math.min(end, documentLength);
begin = Math.min(begin, end);
- // It turns out the XML formatter does *NOT* format things correctly if you
- // select just a region of text. You *MUST* also include the leading whitespace
- // on the line, or it will dedent all the content to column 0. Therefore,
- // we must figure out the offset of the start of the line that contains the
- // beginning of the tag.
- try {
- IRegion lineInformation = document.getLineInformationOfOffset(begin);
- if (lineInformation != null) {
- int lineBegin = lineInformation.getOffset();
- if (lineBegin != begin) {
- begin = lineBegin;
- } else if (begin > 0) {
- // Trick #2: It turns out that, if an XML element starts in column 0,
- // then the XML formatter will NOT indent it (even if its parent is
- // indented). If you on the other hand include the end of the previous
- // line (the newline), THEN the formatter also correctly inserts the
- // element. Therefore, we adjust the beginning range to include the
- // previous line (if we are not already in column 0 of the first line)
- // in the case where the element starts the line.
- begin--;
+ if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) {
+ // Workarounds which only apply to the builtin Eclipse formatter:
+ //
+ // It turns out the XML formatter does *NOT* format things correctly if you
+ // select just a region of text. You *MUST* also include the leading whitespace
+ // on the line, or it will dedent all the content to column 0. Therefore,
+ // we must figure out the offset of the start of the line that contains the
+ // beginning of the tag.
+ try {
+ IRegion lineInformation = document.getLineInformationOfOffset(begin);
+ if (lineInformation != null) {
+ int lineBegin = lineInformation.getOffset();
+ if (lineBegin != begin) {
+ begin = lineBegin;
+ } else if (begin > 0) {
+ // Trick #2: It turns out that, if an XML element starts in column 0,
+ // then the XML formatter will NOT indent it (even if its parent is
+ // indented). If you on the other hand include the end of the previous
+ // line (the newline), THEN the formatter also correctly inserts the
+ // element. Therefore, we adjust the beginning range to include the
+ // previous line (if we are not already in column 0 of the first line)
+ // in the case where the element starts the line.
+ begin--;
+ }
}
+ } catch (BadLocationException e) {
+ // This cannot happen because we already clamped the offsets
+ AdtPlugin.log(e, e.toString());
}
- } catch (BadLocationException e) {
- // This cannot happen because we already clamped the offsets
- AdtPlugin.log(e, e.toString());
}
if (textViewer instanceof StructuredTextViewer) {
if (canFormat) {
StyledText textWidget = textViewer.getTextWidget();
textWidget.setSelection(begin, end);
- structuredTextViewer.doOperation(operation);
+
+ try {
+ // Formatting does not affect the XML model so ignore notifications
+ // about model edits from this
+ mIgnoreXmlUpdate = true;
+ structuredTextViewer.doOperation(operation);
+ } finally {
+ mIgnoreXmlUpdate = false;
+ }
+
+ textWidget.setSelection(0, 0);
}
}
}
int operation = StructuredTextViewer.FORMAT_DOCUMENT;
boolean canFormat = structuredTextViewer.canDoOperation(operation);
if (canFormat) {
- structuredTextViewer.doOperation(operation);
+ try {
+ // Formatting does not affect the XML model so ignore notifications
+ // about model edits from this
+ mIgnoreXmlUpdate = true;
+ structuredTextViewer.doOperation(operation);
+ } finally {
+ mIgnoreXmlUpdate = false;
+ }
}
}
}
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
-import com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
private final LinkedList<TypedPosition> mPartitions = new LinkedList<TypedPosition>();
private ContextBasedFormattingStrategy mDelegate = null;
+ /**
+ * Creates a new {@link AndroidXmlFormattingStrategy}
+ */
public AndroidXmlFormattingStrategy() {
}
private XmlFormatPreferences() {
}
+ /**
+ * Creates a new {@link XmlFormatPreferences} based on the current settings
+ * in {@link AdtPrefs}
+ *
+ * @return an {@link XmlFormatPreferences} object
+ */
public static XmlFormatPreferences create() {
XmlFormatPreferences p = new XmlFormatPreferences();
AdtPrefs prefs = AdtPrefs.getPrefs();
// The XML module settings do not have a public API. We should replace this with JDT
// settings anyway since that's more likely what users have configured and want applied
// to their XML files
+ /**
+ * Returns the string to use to indent one indentation level
+ *
+ * @return the string used to indent one indentation level
+ */
@SuppressWarnings({
"restriction", "deprecation"
})
}
}
- // Put the comment on a line on its own? Only if it does not follow some other comment
- // (e.g. is the first child in an element or follows some other element only separated
- // by whitespace)
+ // Put the comment on a line on its own? Only if it was separated by a blank line
+ // in the previous version of the document. In other words, if the document
+ // adds blank lines between comments this formatter will preserve that fact, and vice
+ // versa for a tightly formatted document it will preserve that convention as well.
if (!mPrefs.removeEmptyLines && depth > 0 && !isSuffixComment) {
Node curr = node.getPreviousSibling();
- if (curr == null
- || curr.getNodeType() == Node.ELEMENT_NODE
- || (curr.getNodeType() == Node.TEXT_NODE
- && curr.getNodeValue().trim().length() == 0
- && (curr.getPreviousSibling() == null
- || curr.getPreviousSibling().getNodeType() == Node.ELEMENT_NODE))) {
+ if (curr == null) {
mOut.append(mLineSeparator);
+ } else if (curr.getNodeType() == Node.TEXT_NODE) {
+ String text = curr.getNodeValue();
+ // Count how many newlines we find in the trailing whitespace of the
+ // text node
+ int newLines = 0;
+ for (int i = text.length() - 1; i >= 0; i--) {
+ char c = text.charAt(i);
+ if (Character.isWhitespace(c)) {
+ if (c == '\n') {
+ newLines++;
+ if (newLines == 2) {
+ break;
+ }
+ }
+ } else {
+ break;
+ }
+ }
+ if (newLines >= 2) {
+ mOut.append(mLineSeparator);
+ } else if (text.trim().length() == 0 && curr.getPreviousSibling() == null) {
+ // Comment before first child in node
+ mOut.append(mLineSeparator);
+ }
}
}
+
// TODO: Reformat the comment text?
if (!multiLine) {
if (!isSuffixComment) {
mGraphicalEditor.recomputeLayout();
}
+ @Override
+ public boolean supportsFormatOnGuiEdit() {
+ return true;
+ }
+
/**
* Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
*/
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
+/**
+ * Various utility methods for manipulating DOM nodes.
+ */
@SuppressWarnings("restriction") // No replacement for restricted XML model yet
public class DomUtilities {
private static final String AMPERSAND_ENTITY = "&"; //$NON-NLS-1$
node.setDirty(false);
}
- if (mUiParent != null) {
- mUiParent.formatOnInsert(this);
- }
+ getEditor().scheduleNodeReformat(this, false);
invokeUiUpdateListeners(UiUpdateState.CREATED);
return mXmlNode;
xmlParent.removeChild(previousSibling);
}
- if (mUiParent != null) {
- mUiParent.formatOnDeletion(this);
- }
-
invokeUiUpdateListeners(UiUpdateState.DELETED);
return oldXmlNode;
}
return result;
}
+ if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) {
+ // If auto formatting, don't bother with attribute sorting here since the
+ // order will be corrected as soon as the edit is committed anyway
+ for (UiAttributeNode uiAttribute : dirtyAttributes) {
+ commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
+ uiAttribute.setDirty(false);
+ }
+
+ return result;
+ }
+
String firstName = dirtyAttributes.get(0).getDescriptor().getXmlLocalName();
NamedNodeMap attributes = ((Element) element).getAttributes();
List<Attr> move = new ArrayList<Attr>();
value = ""; //$NON-NLS-1$ -- this removes an attribute
}
+ getEditor().scheduleNodeReformat(this, true);
+
// Try with all internal attributes
UiAttributeNode uiAttr = setInternalAttrValue(
getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
}
}
- /** Handles reformatting of the XML buffer when a given node has been inserted.
- *
- * @param node The node that was inserted.
- */
- private void formatOnInsert(UiElementNode node) {
- // Reformat parent if it's the first child (such that it for example can force
- // children into their own lines.)
- if (mUiChildren.size() == 1) {
- reformat();
- } else {
- // In theory, we should ONLY have to reformat the node itself:
- // uiNode.reformat();
- //
- // However, the XML formatter does not correctly handle this; in particular
- // it will -dedent- a correctly indented child. Here's an example:
- //
- // @formatter:off
- // <?xml version="1.0" encoding="utf-8"?>
- // <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- // android:layout_width="fill_parent" android:layout_height="fill_parent"
- // android:orientation="vertical">
- // <LinearLayout android:id="@+id/LinearLayout01"
- // android:layout_width="wrap_content" android:layout_height="wrap_content">
- // <Button android:id="@+id/Button03"></Button>
- // </LinearLayout>
- // </LinearLayout>
- // @formatter:on
- //
- // If we have just inserted the button inside the nested LinearLayout, and
- // attempt to format it, it will incorrectly dedent the button to be flush with
- // its parent.
- //
- // Therefore, for now, in this case, format the PARENT on insert. This means that
- // siblings can be formatted as well, but that can't be helped.
-
- // This should be "uiNode.reformat();" instead of "reformat()" if formatting
- // worked correctly:
- reformat();
- }
- }
-
/**
- * Handles reformatting of the XML buffer when a given node has been removed.
+ * Returns true if this node is an ancestor (parent, grandparent, and so on)
+ * of the given node. Note that a node is not considered an ancestor of
+ * itself.
*
- * @param node The node that was removed.
+ * @param node the node to test
+ * @return true if this node is an ancestor of the given node
*/
- private void formatOnDeletion(UiElementNode node) {
- // Reformat parent if it's the last child removed, such that we can for example
- // place the closing element back on the same line as the opening tag (if the
- // user has that mode configured in the formatting options.)
- if (mUiChildren.size() <= 1) {
- // <= 1 instead of == 0: turns out the parent hasn't always deleted
- // this child from its its children list yet.
- reformat();
+ public boolean isAncestorOf(UiElementNode node) {
+ node = node.getUiParent();
+ while (node != null) {
+ if (node == this) {
+ return true;
+ }
+ node = node.getUiParent();
}
+ return false;
}
/**
- * Reformats the XML corresponding to the given XML node. This will do nothing if we have
- * errors, or if the user has turned off XML auto-formatting.
+ * Finds the nearest common parent of the two given nodes (which could be one of the
+ * two nodes as well)
+ *
+ * @param node1 the first node to test
+ * @param node2 the second node to test
+ * @return the nearest common parent of the two given nodes
*/
- private void reformat() {
- if (mHasError || !AdtPrefs.getPrefs().getFormatGuiXml()) {
- return;
+ public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) {
+ while (node2 != null) {
+ UiElementNode current = node1;
+ while (current != null && current != node2) {
+ current = current.getUiParent();
+ }
+ if (current == node2) {
+ return current;
+ }
+ node2 = node2.getUiParent();
}
- AndroidXmlEditor editor = getEditor();
- if (editor != null && mXmlNode != null) {
- editor.reformatNode(mXmlNode);
- }
+ return null;
}
}
import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.internal.build.DebugKeyProvider;
import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
return mBuildForceResResfresh;
}
+ /**
+ * Should changes made by GUI editors automatically format the corresponding XML nodes
+ * affected by the edit?
+ *
+ * @return true if the GUI editors should format affected XML regions
+ */
public boolean getFormatGuiXml() {
- return mFormatGuiXml;
+ // The format-GUI-editors flag only applies when the custom formatter is used,
+ // since the built-in formatter has problems editing partial documents
+ return mFormatGuiXml && mCustomXmlFormatter;
}
+ /**
+ * Should the XML formatter use a custom Android XML formatter (following
+ * Android code style) or use the builtin Eclipse XML formatter?
+ *
+ * @return true if the Android formatter should be used instead of the
+ * default Eclipse one
+ */
public boolean getUseCustomXmlFormatter() {
return mCustomXmlFormatter;
}
+ /**
+ * Should the Android XML formatter use the Eclipse XML indentation settings
+ * (usually one tab character) instead of the default 4 space character
+ * indent?
+ *
+ * @return true if the Eclipse XML indentation settings should be use
+ */
public boolean isUseEclipseIndent() {
return mUseEclipseIndent;
}
+ /**
+ * Should the Android XML formatter try to avoid inserting blank lines to
+ * make the format as compact as possible (no blank lines between elements,
+ * no blank lines surrounding comments, etc).
+ *
+ * @return true to remove blank lines
+ */
public boolean isRemoveEmptyLines() {
return mRemoveEmptyLines;
}
+ /**
+ * Should the Android XML formatter attempt to place a single attribute on
+ * the same line as the element open tag?
+ *
+ * @return true if single-attribute elements should place the attribute on
+ * the same line as the element open tag
+ */
public boolean isOneAttributeOnFirstLine() {
return mOneAttributeOnFirstLine;
}
+ /**
+ * Returns the sort order to be applied to the attributes (one of which can
+ * be {@link AttributeSortOrder#NO_SORTING}).
+ *
+ * @return the sort order to apply to the attributes
+ */
public AttributeSortOrder getAttributeSort() {
if (mAttributeSort == null) {
return AttributeSortOrder.LOGICAL;
return mAttributeSort;
}
+ /**
+ * Returns whether a space should be inserted before the closing {@code >}
+ * character in open tags and before the closing {@code />} characters in
+ * empty tag. Note that the {@link XmlFormatStyle#RESOURCE} style overrides
+ * this setting to make it more compact for the {@code <item>} elements.
+ *
+ * @return true if an empty space should be inserted before {@code >} or
+ * {@code />}.
+ */
public boolean isSpaceBeforeClose() {
return mSpaceBeforeClose;
}
+ /**
+ * Returns whether the file should be automatically formatted on save.
+ *
+ * @return true if the XML files should be formatted on save.
+ */
public boolean isFormatOnSave() {
return mFormatOnSave;
}
private BooleanFieldEditor mRemoveEmptyEditor;
private BooleanFieldEditor mOneAttrPerLineEditor;
private BooleanFieldEditor mSpaceBeforeCloseEditor;
+ private BooleanFieldEditor mFormatGuiXmlEditor;
+ /**
+ * Constructs a new Android editors preference page
+ */
public EditorsPage() {
super(GRID);
setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore());
},
parent, true));
- addField(new BooleanFieldEditor(AdtPrefs.PREFS_FORMAT_GUI_XML,
+ mFormatGuiXmlEditor = new BooleanFieldEditor(AdtPrefs.PREFS_FORMAT_GUI_XML,
"Automatically format the XML edited by the visual layout editor",
- parent));
+ parent);
+ addField(mFormatGuiXmlEditor);
addField(new BooleanFieldEditor(AdtPrefs.PREFS_FORMAT_ON_SAVE,
"Format on Save",
mRemoveEmptyEditor.setEnabled(enabled, parent);
mOneAttrPerLineEditor.setEnabled(enabled, parent);
mSpaceBeforeCloseEditor.setEnabled(enabled, parent);
+ mFormatGuiXmlEditor.setEnabled(enabled, parent);
}
/**
"<resources>\n" +
"\n" +
" <dimen name=\"colorstrip_height\">6dip</dimen>\n" +
- "\n" +
" <!-- comment1 -->\n" +
" <dimen name=\"title_height\">45dip</dimen>\n" +
"\n" +
"</resources>");
}
+ public void testLineCommentSpacing() throws Exception {
+ checkFormat(
+ XmlFormatStyle.RESOURCE,
+ "<resources>\n" +
+ "\n" +
+ " <dimen name=\"colorstrip_height\">6dip</dimen>\n" +
+ " <!-- comment1 -->\n" +
+ " <dimen name=\"title_height\">45dip</dimen>\n" +
+ " <!-- comment2: no newlines -->\n" +
+ " <dimen name=\"now_playing_height\">90dip</dimen>\n" +
+ " <dimen name=\"text_size_small\">14sp</dimen>\n" +
+ "\n" +
+ " <!-- comment3: newline above and below -->\n" +
+ "\n" +
+ " <dimen name=\"text_size_medium\">18sp</dimen>\n" +
+ " <dimen name=\"text_size_large\">22sp</dimen>\n" +
+ "\n" +
+ "</resources>",
+
+ "<resources>\n" +
+ "\n" +
+ " <dimen name=\"colorstrip_height\">6dip</dimen>\n" +
+ " <!-- comment1 -->\n" +
+ " <dimen name=\"title_height\">45dip</dimen>\n" +
+ " <!-- comment2: no newlines -->\n" +
+ " <dimen name=\"now_playing_height\">90dip</dimen>\n" +
+ " <dimen name=\"text_size_small\">14sp</dimen>\n" +
+ "\n" +
+ " <!-- comment3: newline above and below -->\n" +
+ "\n" +
+ " <dimen name=\"text_size_medium\">18sp</dimen>\n" +
+ " <dimen name=\"text_size_large\">22sp</dimen>\n" +
+ "\n" +
+ "</resources>");
+ }
}