Home | History | Annotate | Download | only in formatting
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.formatting;
     17 
     18 import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
     19 import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findLineStart;
     20 import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findTextStart;
     21 import static com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors.SELECTOR_TAG;
     22 import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_MEDIUM;
     23 import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_PARTITION;
     24 import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_REGION;
     25 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
     26 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
     27 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
     28 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
     29 
     30 import com.android.SdkConstants;
     31 import com.android.annotations.VisibleForTesting;
     32 import com.android.ide.common.xml.XmlFormatPreferences;
     33 import com.android.ide.common.xml.XmlFormatStyle;
     34 import com.android.ide.common.xml.XmlPrettyPrinter;
     35 import com.android.ide.eclipse.adt.AdtPlugin;
     36 import com.android.ide.eclipse.adt.AdtUtils;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     38 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     39 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     40 import com.android.resources.ResourceType;
     41 
     42 import org.eclipse.core.resources.IProject;
     43 import org.eclipse.core.resources.IResource;
     44 import org.eclipse.core.resources.IWorkspace;
     45 import org.eclipse.core.resources.IWorkspaceRoot;
     46 import org.eclipse.core.resources.ResourcesPlugin;
     47 import org.eclipse.jface.text.BadLocationException;
     48 import org.eclipse.jface.text.IDocument;
     49 import org.eclipse.jface.text.IRegion;
     50 import org.eclipse.jface.text.TextUtilities;
     51 import org.eclipse.jface.text.TypedPosition;
     52 import org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy;
     53 import org.eclipse.jface.text.formatter.IFormattingContext;
     54 import org.eclipse.text.edits.MultiTextEdit;
     55 import org.eclipse.text.edits.ReplaceEdit;
     56 import org.eclipse.text.edits.TextEdit;
     57 import org.eclipse.ui.texteditor.ITextEditor;
     58 import org.eclipse.wst.sse.core.StructuredModelManager;
     59 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     60 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     61 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     62 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     63 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
     64 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
     65 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
     66 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
     67 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
     68 import org.eclipse.wst.xml.ui.internal.XMLFormattingStrategy;
     69 import org.w3c.dom.Document;
     70 import org.w3c.dom.Element;
     71 import org.w3c.dom.Node;
     72 import org.w3c.dom.NodeList;
     73 import org.w3c.dom.Text;
     74 
     75 import java.util.HashMap;
     76 import java.util.LinkedList;
     77 import java.util.Map;
     78 import java.util.Queue;
     79 
     80 /**
     81  * Formatter which formats XML content according to the established Android coding
     82  * conventions. It performs the format by computing the smallest set of DOM nodes
     83  * overlapping the formatted region, then it pretty-prints that XML region
     84  * using the {@link EclipseXmlPrettyPrinter}, and then it replaces the affected region
     85  * by the pretty-printed region.
     86  * <p>
     87  * This strategy is also used for delegation. If the user has chosen to use the
     88  * standard Eclipse XML formatter, this strategy simply delegates to the
     89  * default XML formatting strategy in WTP.
     90  */
     91 @SuppressWarnings("restriction")
     92 public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy {
     93     private IRegion mRegion;
     94     private final Queue<IDocument> mDocuments = new LinkedList<IDocument>();
     95     private final LinkedList<TypedPosition> mPartitions = new LinkedList<TypedPosition>();
     96     private ContextBasedFormattingStrategy mDelegate = null;
     97     /** False if document is known not to be in an Android project, null until initialized */
     98     private Boolean mIsAndroid;
     99 
    100     /**
    101      * Creates a new {@link AndroidXmlFormattingStrategy}
    102      */
    103     public AndroidXmlFormattingStrategy() {
    104     }
    105 
    106     private ContextBasedFormattingStrategy getDelegate() {
    107         if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()
    108                 || mIsAndroid != null && !mIsAndroid.booleanValue()) {
    109             if (mDelegate == null) {
    110                 mDelegate = new XMLFormattingStrategy();
    111             }
    112 
    113             return mDelegate;
    114         }
    115 
    116         return null;
    117     }
    118 
    119     @Override
    120     public void format() {
    121         // Use Eclipse XML formatter instead?
    122         ContextBasedFormattingStrategy delegate = getDelegate();
    123         if (delegate != null) {
    124             delegate.format();
    125             return;
    126         }
    127 
    128         super.format();
    129 
    130         IDocument document = mDocuments.poll();
    131         TypedPosition partition = mPartitions.poll();
    132 
    133         if (document != null && partition != null && mRegion != null) {
    134             try {
    135                 if (document instanceof IStructuredDocument) {
    136                     IStructuredDocument structuredDocument = (IStructuredDocument) document;
    137                     IModelManager modelManager = StructuredModelManager.getModelManager();
    138                     IStructuredModel model = modelManager.getModelForEdit(structuredDocument);
    139                     if (model != null) {
    140                         try {
    141                             TextEdit edit = format(model, mRegion.getOffset(),
    142                                     mRegion.getLength());
    143                             if (edit != null) {
    144                                 try {
    145                                     model.aboutToChangeModel();
    146                                     edit.apply(document);
    147                                 }
    148                                 finally {
    149                                     model.changedModel();
    150                                 }
    151                             }
    152                         }
    153                         finally {
    154                             model.releaseFromEdit();
    155                         }
    156                     }
    157                 }
    158             }
    159             catch (BadLocationException e) {
    160                 AdtPlugin.log(e, "Formatting error");
    161             }
    162         }
    163     }
    164 
    165     /**
    166      * Creates a {@link TextEdit} for formatting the given model's XML in the text range
    167      * starting at offset start with the given length. Note that the exact formatting
    168      * offsets may be adjusted to format a complete element.
    169      *
    170      * @param model the model to be formatted
    171      * @param start the starting offset
    172      * @param length the length of the text range to be formatted
    173      * @return a {@link TextEdit} which edits the model into a formatted document
    174      */
    175     private static TextEdit format(IStructuredModel model, int start, int length) {
    176         int end = start + length;
    177 
    178         TextEdit edit = new MultiTextEdit();
    179         IStructuredDocument document = model.getStructuredDocument();
    180 
    181         Node startNode = null;
    182         Node endNode = null;
    183         Document domDocument = null;
    184 
    185         if (model instanceof IDOMModel) {
    186             IDOMModel domModel = (IDOMModel) model;
    187             domDocument = domModel.getDocument();
    188         } else {
    189             // This should not happen
    190             return edit;
    191         }
    192 
    193         IStructuredDocumentRegion startRegion = document.getRegionAtCharacterOffset(start);
    194         if (startRegion != null) {
    195             int startOffset = startRegion.getStartOffset();
    196             IndexedRegion currentIndexedRegion = model.getIndexedRegion(startOffset);
    197             if (currentIndexedRegion instanceof IDOMNode) {
    198                 IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
    199                 startNode = currentDOMNode;
    200             }
    201         }
    202 
    203         boolean isOpenTagOnly = false;
    204         int openTagEnd = -1;
    205 
    206         IStructuredDocumentRegion endRegion = document.getRegionAtCharacterOffset(end);
    207         if (endRegion != null) {
    208             int endOffset = Math.max(endRegion.getStartOffset(),
    209                     endRegion.getEndOffset() - 1);
    210             IndexedRegion currentIndexedRegion = model.getIndexedRegion(endOffset);
    211 
    212             // If you place the caret right on the right edge of an element, such as this:
    213             //     <foo name="value">|
    214             // then the DOM model will consider the region containing the caret to be
    215             // whatever nodes FOLLOWS the element, usually a text node.
    216             // Detect this case, and look into the previous range.
    217             if (currentIndexedRegion instanceof Text
    218                     && currentIndexedRegion.getStartOffset() == end && end > 0) {
    219                 end--;
    220                 currentIndexedRegion = model.getIndexedRegion(end);
    221                 endRegion = document.getRegionAtCharacterOffset(
    222                         currentIndexedRegion.getStartOffset());
    223             }
    224 
    225             if (currentIndexedRegion instanceof IDOMNode) {
    226                 IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
    227                 endNode = currentDOMNode;
    228 
    229                 // See if this range is fully within the opening tag
    230                 if (endNode == startNode && endRegion == startRegion) {
    231                     ITextRegion subRegion = endRegion.getRegionAtCharacterOffset(end);
    232                     ITextRegionList regions = endRegion.getRegions();
    233                     int index = regions.indexOf(subRegion);
    234                     if (index != -1) {
    235                         // Skip past initial occurrence of close tag if we place the caret
    236                         // right on a >
    237                         subRegion = regions.get(index);
    238                         String type = subRegion.getType();
    239                         if (type == XML_TAG_CLOSE || type == XML_EMPTY_TAG_CLOSE) {
    240                             index--;
    241                         }
    242                     }
    243                     for (; index >= 0; index--) {
    244                         subRegion = regions.get(index);
    245                         String type = subRegion.getType();
    246                         if (type == XML_TAG_OPEN) {
    247                             isOpenTagOnly = true;
    248                         } else if (type == XML_EMPTY_TAG_CLOSE || type == XML_TAG_CLOSE
    249                                 || type == XML_END_TAG_OPEN) {
    250                             break;
    251                         }
    252                     }
    253 
    254                     int max = regions.size();
    255                     for (index = Math.max(0, index); index < max; index++) {
    256                         subRegion = regions.get(index);
    257                         String type = subRegion.getType();
    258                         if (type == XML_EMPTY_TAG_CLOSE || type == XML_TAG_CLOSE) {
    259                             openTagEnd = subRegion.getEnd() + endRegion.getStartOffset();
    260                         }
    261                     }
    262 
    263                     if (openTagEnd == -1) {
    264                         isOpenTagOnly = false;
    265                     }
    266                 }
    267             }
    268         }
    269 
    270         String[] indentationLevels = null;
    271         Node root = null;
    272         int initialDepth = 0;
    273         int replaceStart;
    274         int replaceEnd;
    275         boolean endWithNewline = false;
    276         if (startNode == null || endNode == null) {
    277             // Process the entire document
    278             root = domDocument;
    279             // both document and documentElement should be <= 0
    280             initialDepth = -1;
    281             startNode = root;
    282             endNode = root;
    283             replaceStart = 0;
    284             replaceEnd = document.getLength();
    285             try {
    286                 endWithNewline = replaceEnd > 0 && document.getChar(replaceEnd - 1) == '\n';
    287             } catch (BadLocationException e) {
    288                 // Can't happen
    289             }
    290         } else {
    291             root = DomUtilities.getCommonAncestor(startNode, endNode);
    292             initialDepth = root != null ? DomUtilities.getDepth(root) - 1 : 0;
    293 
    294             // Regions must be non-null since the DOM nodes are non null, but Eclipse null
    295             // analysis doesn't realize it:
    296             assert startRegion != null && endRegion != null;
    297 
    298             replaceStart = ((IndexedRegion) startNode).getStartOffset();
    299             if (isOpenTagOnly) {
    300                 replaceEnd = openTagEnd;
    301             } else {
    302                 replaceEnd = ((IndexedRegion) endNode).getEndOffset();
    303             }
    304 
    305             // Look up the indentation level of the start node, if it is an element
    306             // and it starts on its own line
    307             if (startNode.getNodeType() == Node.ELEMENT_NODE) {
    308                 // Measure the indentation of the start node such that we can indent
    309                 // the reformatted version of the node exactly in place and it should blend
    310                 // in if the surrounding content does not use the same indentation size etc.
    311                 // However, it's possible for the start node to have deeper depth than other
    312                 // content we're formatting, as in the following scenario for example:
    313                 //      <foo>
    314                 //         <bar/>
    315                 //      </foo>
    316                 //   <baz/>
    317                 // If you select this text range, we want <foo> to be formatted at whatever
    318                 // level it is, and we also need to know the indentation level to use
    319                 // for </baz>. We don't measure the depth of <bar/>, a child of the start node,
    320                 // since from the initial indentation level and on down we want to normalize
    321                 // the output.
    322                 IndentationMeasurer m = new IndentationMeasurer(startNode, endNode, document);
    323                 indentationLevels = m.measure(initialDepth, root);
    324 
    325                 // Wipe out any levels deeper than the start node's level
    326                 // (which may not be the smallest level, e.g. where you select a child
    327                 // and the end of its parent etc).
    328                 // (Since we're ONLY measuring the node and its parents, you might wonder
    329                 // why this is doing a full subtree traversal instead of just walking up
    330                 // the parent chain and looking up the indentation for each. The reason for
    331                 // this is that some of theses nodes, which have not yet been formatted,
    332                 // may be sharing lines with other nodes, and we disregard indentation for
    333                 // any nodes that don't start a line since the indentation may only be correct
    334                 // for the first element, so therefore we look for other nodes at the same
    335                 // level that do have indentation info at the front of the line.
    336                 int depth = DomUtilities.getDepth(startNode) - 1;
    337                 for (int i = depth + 1; i < indentationLevels.length; i++) {
    338                     indentationLevels[i] = null;
    339                 }
    340             }
    341         }
    342 
    343         XmlFormatStyle style = guessStyle(model, domDocument);
    344         XmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
    345         String delimiter = TextUtilities.getDefaultLineDelimiter(document);
    346         XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, delimiter);
    347         printer.setEndWithNewline(endWithNewline);
    348 
    349         if (indentationLevels != null) {
    350             printer.setIndentationLevels(indentationLevels);
    351         }
    352 
    353         StringBuilder sb = new StringBuilder(length);
    354         printer.prettyPrint(initialDepth, root, startNode, endNode, sb, isOpenTagOnly);
    355 
    356         String formatted = sb.toString();
    357         ReplaceEdit replaceEdit = createReplaceEdit(document, replaceStart, replaceEnd, formatted,
    358                 prefs);
    359         if (replaceEdit != null) {
    360             edit.addChild(replaceEdit);
    361         }
    362 
    363         // Attempt to fix the selection range since otherwise, with the document shifting
    364         // under it, you end up selecting a "random" portion of text now shifted into the
    365         // old positions of the formatted text:
    366         if (replaceEdit != null && replaceStart != 0 && replaceEnd != document.getLength()) {
    367             ITextEditor editor = AdtUtils.getActiveTextEditor();
    368             if (editor != null) {
    369                 editor.setHighlightRange(replaceEdit.getOffset(), replaceEdit.getText().length(),
    370                         false /*moveCursor*/);
    371             }
    372         }
    373 
    374         return edit;
    375     }
    376 
    377     /**
    378      * Create a {@link ReplaceEdit} which replaces the text in the given document with the
    379      * given new formatted content. The replaceStart and replaceEnd parameters point to
    380      * the equivalent unformatted text in the document, but the actual edit range may be
    381      * adjusted (for example to make the edit smaller if the beginning and/or end is
    382      * identical, and so on)
    383      */
    384     @VisibleForTesting
    385     static ReplaceEdit createReplaceEdit(IDocument document, int replaceStart,
    386             int replaceEnd, String formatted, XmlFormatPreferences prefs) {
    387         // If replacing a node somewhere in the middle, start the replacement at the
    388         // beginning of the current line
    389         int index = replaceStart;
    390         try {
    391             while (index > 0) {
    392                 char c = document.getChar(index - 1);
    393                 if (c == '\n') {
    394                     if (index < replaceStart) {
    395                         replaceStart = index;
    396                     }
    397                     break;
    398                 } else if (!Character.isWhitespace(c)) {
    399                     // The replaced node does not start on its own line; in that case,
    400                     // remove the initial indentation in the reformatted element
    401                     for (int i = 0; i < formatted.length(); i++) {
    402                         if (!Character.isWhitespace(formatted.charAt(i))) {
    403                             formatted = formatted.substring(i);
    404                             break;
    405                         }
    406                     }
    407                     break;
    408                 }
    409                 index--;
    410             }
    411         } catch (BadLocationException e) {
    412             AdtPlugin.log(e, null);
    413         }
    414 
    415         // If there are multiple blank lines before the insert position, collapse them down
    416         // to one
    417         int prevNewlineIndex = -1;
    418         boolean beginsWithNewline = false;
    419         for (int i = 0, n = formatted.length(); i < n; i++) {
    420             char c = formatted.charAt(i);
    421             if (c == '\n') {
    422                 beginsWithNewline = true;
    423                 break;
    424             } else if (!Character.isWhitespace(c)) { // \r is whitespace so is handled correctly
    425                 break;
    426             }
    427         }
    428         try {
    429             for (index = replaceStart - 1; index > 0; index--) {
    430                 char c = document.getChar(index);
    431                 if (c == '\n') {
    432                     if (prevNewlineIndex != -1) {
    433                         replaceStart = prevNewlineIndex;
    434                     }
    435                     prevNewlineIndex = index;
    436                     if (index > 0 && document.getChar(index - 1) == '\r') {
    437                         prevNewlineIndex--;
    438                     }
    439                 } else if (!Character.isWhitespace(c)) {
    440                     break;
    441                 }
    442             }
    443         } catch (BadLocationException e) {
    444             AdtPlugin.log(e, null);
    445         }
    446         if (prefs.removeEmptyLines && prevNewlineIndex != -1 && beginsWithNewline) {
    447             replaceStart = prevNewlineIndex + 1;
    448         }
    449 
    450         // Search forwards too
    451         int nextNewlineIndex = -1;
    452         try {
    453             int max = document.getLength();
    454             for (index = replaceEnd; index < max; index++) {
    455                 char c = document.getChar(index);
    456                 if (c == '\n') {
    457                     if (nextNewlineIndex != -1) {
    458                         replaceEnd = nextNewlineIndex + 1;
    459                     }
    460                     nextNewlineIndex = index;
    461                 } else if (!Character.isWhitespace(c)) {
    462                     break;
    463                 }
    464             }
    465         } catch (BadLocationException e) {
    466             AdtPlugin.log(e, null);
    467         }
    468         boolean endsWithNewline = false;
    469         for (int i = formatted.length() - 1; i >= 0; i--) {
    470             char c = formatted.charAt(i);
    471             if (c == '\n') {
    472                 endsWithNewline = true;
    473                 break;
    474             } else if (!Character.isWhitespace(c)) {
    475                 break;
    476             }
    477         }
    478 
    479         if (prefs.removeEmptyLines && nextNewlineIndex != -1 && endsWithNewline) {
    480             replaceEnd = nextNewlineIndex + 1;
    481         }
    482 
    483         // Figure out how much of the before and after strings are identical and narrow
    484         // the replacement scope
    485         boolean foundDifference = false;
    486         int firstDifference = 0;
    487         int lastDifference = formatted.length();
    488         try {
    489             for (int i = 0, j = replaceStart; i < formatted.length() && j < replaceEnd; i++, j++) {
    490                 if (formatted.charAt(i) != document.getChar(j)) {
    491                     firstDifference = i;
    492                     foundDifference = true;
    493                     break;
    494                 }
    495             }
    496 
    497             if (!foundDifference) {
    498                 // No differences - the document is already formatted, nothing to do
    499                 return null;
    500             }
    501 
    502             lastDifference = firstDifference + 1;
    503             for (int i = formatted.length() - 1, j = replaceEnd - 1;
    504                     i > firstDifference && j > replaceStart;
    505                     i--, j--) {
    506                 if (formatted.charAt(i) != document.getChar(j)) {
    507                     lastDifference = i + 1;
    508                     break;
    509                 }
    510             }
    511         } catch (BadLocationException e) {
    512             AdtPlugin.log(e, null);
    513         }
    514 
    515         replaceStart += firstDifference;
    516         replaceEnd -= (formatted.length() - lastDifference);
    517         replaceEnd = Math.max(replaceStart, replaceEnd);
    518         formatted = formatted.substring(firstDifference, lastDifference);
    519 
    520         ReplaceEdit replaceEdit = new ReplaceEdit(replaceStart, replaceEnd - replaceStart,
    521                 formatted);
    522         return replaceEdit;
    523     }
    524 
    525     /**
    526      * Guess what style to use to edit the given document - layout, resource, manifest, ... ? */
    527     static XmlFormatStyle guessStyle(IStructuredModel model, Document domDocument) {
    528         // The "layout" style is used for most XML resource file types:
    529         // layouts, color-lists and state-lists, animations, drawables, menus, etc
    530         XmlFormatStyle style = XmlFormatStyle.get(domDocument);
    531         if (style == XmlFormatStyle.FILE) {
    532             style = XmlFormatStyle.LAYOUT;
    533         }
    534 
    535         // The "resource" style is used for most value-based XML files:
    536         // strings, dimensions, booleans, colors, integers, plurals,
    537         // integer-arrays, string-arrays, and typed-arrays
    538         Element rootElement = domDocument.getDocumentElement();
    539         if (rootElement != null
    540                 && SdkConstants.TAG_RESOURCES.equals(rootElement.getTagName())) {
    541             style = XmlFormatStyle.RESOURCE;
    542         }
    543 
    544         // Selectors are also used similar to resources
    545         if (rootElement != null && SELECTOR_TAG.equals(rootElement.getTagName())) {
    546             return XmlFormatStyle.RESOURCE;
    547         }
    548 
    549         // The "manifest" style is used for manifest files
    550         String baseLocation = model.getBaseLocation();
    551         if (baseLocation != null) {
    552             if (baseLocation.endsWith(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
    553                 style = XmlFormatStyle.MANIFEST;
    554             } else {
    555                 int lastSlash = baseLocation.lastIndexOf('/');
    556                 if (lastSlash != -1) {
    557                     int end = baseLocation.lastIndexOf('/', lastSlash - 1); // -1 is okay
    558                     String resourceFolder = baseLocation.substring(end + 1, lastSlash);
    559                     String[] segments = resourceFolder.split("-"); //$NON-NLS-1$
    560                     ResourceType type = ResourceType.getEnum(segments[0]);
    561                     if (type != null) {
    562                         // <resources> files found in res/xml/ should be formatted as
    563                         // resource files!
    564                         if (type == ResourceType.XML && style == XmlFormatStyle.RESOURCE) {
    565                             return style;
    566                         }
    567                         style = EclipseXmlPrettyPrinter.get(type);
    568                     }
    569                 }
    570             }
    571         }
    572 
    573         return style;
    574     }
    575 
    576     private Boolean isAndroid(IDocument document) {
    577         if (mIsAndroid == null) {
    578             // Look up the corresponding IResource for this document. This isn't
    579             // readily available, so take advantage of the structured model's base location
    580             // string and convert it to an IResource to look up its project.
    581             if (document instanceof IStructuredDocument) {
    582                 IStructuredDocument structuredDocument = (IStructuredDocument) document;
    583                 IModelManager modelManager = StructuredModelManager.getModelManager();
    584 
    585                 IStructuredModel model = modelManager.getModelForRead(structuredDocument);
    586                 if (model != null) {
    587                     String location = model.getBaseLocation();
    588                     model.releaseFromRead();
    589                     if (location != null) {
    590                         if (!location.endsWith(ANDROID_MANIFEST_XML)
    591                                 && !location.contains("/res/")) { //$NON-NLS-1$
    592                             // See if it looks like a foreign document
    593                             IWorkspace workspace = ResourcesPlugin.getWorkspace();
    594                             IWorkspaceRoot root = workspace.getRoot();
    595                             IResource member = root.findMember(location);
    596                             if (member.exists()) {
    597                                 IProject project = member.getProject();
    598                                 if (project.isAccessible() &&
    599                                         !BaseProjectHelper.isAndroidProject(project)) {
    600                                     mIsAndroid = false;
    601                                     return false;
    602                                 }
    603                             }
    604                         }
    605                         // Ignore Maven POM files even in Android projects
    606                         if (location.endsWith("/pom.xml")) { //$NON-NLS-1$
    607                             mIsAndroid = false;
    608                             return false;
    609                         }
    610                     }
    611                 }
    612             }
    613 
    614             mIsAndroid = true;
    615         }
    616 
    617         return mIsAndroid.booleanValue();
    618     }
    619 
    620     @Override
    621     public void formatterStarts(final IFormattingContext context) {
    622         // Use Eclipse XML formatter instead?
    623         ContextBasedFormattingStrategy delegate = getDelegate();
    624         if (delegate != null) {
    625             delegate.formatterStarts(context);
    626 
    627             // We also need the super implementation because it stores items into the
    628             // map, and we can't override the getPreferences method, so we need for
    629             // this delegating strategy to supply the correct values when it is called
    630             // instead of the delegate
    631             super.formatterStarts(context);
    632 
    633             return;
    634         }
    635 
    636         super.formatterStarts(context);
    637         mRegion = (IRegion) context.getProperty(CONTEXT_REGION);
    638         TypedPosition partition = (TypedPosition) context.getProperty(CONTEXT_PARTITION);
    639         IDocument document = (IDocument) context.getProperty(CONTEXT_MEDIUM);
    640         mPartitions.offer(partition);
    641         mDocuments.offer(document);
    642 
    643         if (!isAndroid(document)) {
    644             // It's some foreign type of project: use default
    645             // formatter
    646             delegate = getDelegate();
    647             if (delegate != null) {
    648                 delegate.formatterStarts(context);
    649             }
    650         }
    651     }
    652 
    653     @Override
    654     public void formatterStops() {
    655         // Use Eclipse XML formatter instead?
    656         ContextBasedFormattingStrategy delegate = getDelegate();
    657         if (delegate != null) {
    658             delegate.formatterStops();
    659             // See formatterStarts for an explanation
    660             super.formatterStops();
    661 
    662             return;
    663         }
    664 
    665         super.formatterStops();
    666         mRegion = null;
    667         mDocuments.clear();
    668         mPartitions.clear();
    669     }
    670 
    671     /**
    672      * Utility class which can measure the indentation strings for various node levels in
    673      * a given node range
    674      */
    675     static class IndentationMeasurer {
    676         private final Map<Integer, String> mDepth = new HashMap<Integer, String>();
    677         private final Node mStartNode;
    678         private final Node mEndNode;
    679         private final IStructuredDocument mDocument;
    680         private boolean mDone = false;
    681         private boolean mInRange = false;
    682         private int mMaxDepth;
    683 
    684         public IndentationMeasurer(Node mStartNode, Node mEndNode, IStructuredDocument document) {
    685             super();
    686             this.mStartNode = mStartNode;
    687             this.mEndNode = mEndNode;
    688             mDocument = document;
    689         }
    690 
    691         /**
    692          * Measure the various depths found in the range (defined in the constructor)
    693          * under the given node which should be a common ancestor of the start and end
    694          * nodes. The result is a string array where each index corresponds to a depth,
    695          * and the string is either empty, or the complete indentation string to be used
    696          * to indent to the given depth (note that these strings are not cumulative)
    697          *
    698          * @param initialDepth the initial depth to use when visiting
    699          * @param root the root node to look for depths under
    700          * @return a string array containing nulls or indentation strings
    701          */
    702         public String[] measure(int initialDepth, Node root) {
    703             visit(initialDepth, root);
    704             String[] indentationLevels = new String[mMaxDepth + 1];
    705             for (Map.Entry<Integer, String> entry : mDepth.entrySet()) {
    706                 int depth = entry.getKey();
    707                 String indentation = entry.getValue();
    708                 indentationLevels[depth] = indentation;
    709             }
    710 
    711             return indentationLevels;
    712         }
    713 
    714         private void visit(int depth, Node node) {
    715             // Look up indentation for this level
    716             if (node.getNodeType() == Node.ELEMENT_NODE && mDepth.get(depth) == null) {
    717                 // Look up the depth
    718                 try {
    719                     IndexedRegion region = (IndexedRegion) node;
    720                     int lineStart = findLineStart(mDocument, region.getStartOffset());
    721                     int textStart = findTextStart(mDocument, lineStart, region.getEndOffset());
    722 
    723                     // Ensure that the text which begins the line is this element, otherwise
    724                     // we could be measuring the indentation of a parent element which begins
    725                     // the line
    726                     if (textStart == region.getStartOffset()) {
    727                         String indent = mDocument.get(lineStart,
    728                                 Math.max(0, textStart - lineStart));
    729                         mDepth.put(depth, indent);
    730 
    731                         if (depth > mMaxDepth) {
    732                             mMaxDepth = depth;
    733                         }
    734                     }
    735                 } catch (BadLocationException e) {
    736                     AdtPlugin.log(e, null);
    737                 }
    738             }
    739 
    740             NodeList children = node.getChildNodes();
    741             for (int i = 0, n = children.getLength(); i < n; i++) {
    742                 Node child = children.item(i);
    743                 visit(depth + 1, child);
    744                 if (mDone) {
    745                     return;
    746                 }
    747             }
    748 
    749             if (node == mEndNode) {
    750                 mDone = true;
    751             }
    752         }
    753     }
    754 }