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 }