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