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 }