• 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.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      */
AndroidXmlFormattingStrategy()103     public AndroidXmlFormattingStrategy() {
104     }
105 
getDelegate()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
format()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      */
format(IStructuredModel model, int start, int length)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
createReplaceEdit(IDocument document, int replaceStart, int replaceEnd, String formatted, XmlFormatPreferences prefs)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, ... ? */
guessStyle(IStructuredModel model, Document domDocument)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 
isAndroid(IDocument document)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
formatterStarts(final IFormattingContext context)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
formatterStops()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 
IndentationMeasurer(Node mStartNode, Node mEndNode, IStructuredDocument document)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          */
measure(int initialDepth, Node root)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 
visit(int depth, Node node)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 }