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