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