blob: 4cab41962d051785cd9cf0145c306f9fde2b0731 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.editors.formatting;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findLineStart;
import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findTextStart;
import static com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors.SELECTOR_TAG;
import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_MEDIUM;
import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_PARTITION;
import static org.eclipse.jface.text.formatter.FormattingContextProperties.CONTEXT_REGION;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.xml.XmlFormatPreferences;
import com.android.ide.common.xml.XmlFormatStyle;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.resources.ResourceType;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy;
import org.eclipse.jface.text.formatter.IFormattingContext;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.ui.internal.XMLFormattingStrategy;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
/**
* Formatter which formats XML content according to the established Android coding
* conventions. It performs the format by computing the smallest set of DOM nodes
* overlapping the formatted region, then it pretty-prints that XML region
* using the {@link EclipseXmlPrettyPrinter}, and then it replaces the affected region
* by the pretty-printed region.
* <p>
* This strategy is also used for delegation. If the user has chosen to use the
* standard Eclipse XML formatter, this strategy simply delegates to the
* default XML formatting strategy in WTP.
*/
@SuppressWarnings("restriction")
public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy {
private IRegion mRegion;
private final Queue<IDocument> mDocuments = new LinkedList<IDocument>();
private final LinkedList<TypedPosition> mPartitions = new LinkedList<TypedPosition>();
private ContextBasedFormattingStrategy mDelegate = null;
/** False if document is known not to be in an Android project, null until initialized */
private Boolean mIsAndroid;
/**
* Creates a new {@link AndroidXmlFormattingStrategy}
*/
public AndroidXmlFormattingStrategy() {
}
private ContextBasedFormattingStrategy getDelegate() {
if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()
|| mIsAndroid != null && !mIsAndroid.booleanValue()) {
if (mDelegate == null) {
mDelegate = new XMLFormattingStrategy();
}
return mDelegate;
}
return null;
}
@Override
public void format() {
// Use Eclipse XML formatter instead?
ContextBasedFormattingStrategy delegate = getDelegate();
if (delegate != null) {
delegate.format();
return;
}
super.format();
IDocument document = mDocuments.poll();
TypedPosition partition = mPartitions.poll();
if (document != null && partition != null && mRegion != null) {
try {
if (document instanceof IStructuredDocument) {
IStructuredDocument structuredDocument = (IStructuredDocument) document;
IModelManager modelManager = StructuredModelManager.getModelManager();
IStructuredModel model = modelManager.getModelForEdit(structuredDocument);
if (model != null) {
try {
TextEdit edit = format(model, mRegion.getOffset(),
mRegion.getLength());
if (edit != null) {
try {
model.aboutToChangeModel();
edit.apply(document);
}
finally {
model.changedModel();
}
}
}
finally {
model.releaseFromEdit();
}
}
}
}
catch (BadLocationException e) {
AdtPlugin.log(e, "Formatting error");
}
}
}
/**
* Creates a {@link TextEdit} for formatting the given model's XML in the text range
* starting at offset start with the given length. Note that the exact formatting
* offsets may be adjusted to format a complete element.
*
* @param model the model to be formatted
* @param start the starting offset
* @param length the length of the text range to be formatted
* @return a {@link TextEdit} which edits the model into a formatted document
*/
private static TextEdit format(IStructuredModel model, int start, int length) {
int end = start + length;
TextEdit edit = new MultiTextEdit();
IStructuredDocument document = model.getStructuredDocument();
Node startNode = null;
Node endNode = null;
Document domDocument = null;
if (model instanceof IDOMModel) {
IDOMModel domModel = (IDOMModel) model;
domDocument = domModel.getDocument();
} else {
// This should not happen
return edit;
}
IStructuredDocumentRegion startRegion = document.getRegionAtCharacterOffset(start);
if (startRegion != null) {
int startOffset = startRegion.getStartOffset();
IndexedRegion currentIndexedRegion = model.getIndexedRegion(startOffset);
if (currentIndexedRegion instanceof IDOMNode) {
IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
startNode = currentDOMNode;
}
}
boolean isOpenTagOnly = false;
int openTagEnd = -1;
IStructuredDocumentRegion endRegion = document.getRegionAtCharacterOffset(end);
if (endRegion != null) {
int endOffset = Math.max(endRegion.getStartOffset(),
endRegion.getEndOffset() - 1);
IndexedRegion currentIndexedRegion = model.getIndexedRegion(endOffset);
// If you place the caret right on the right edge of an element, such as this:
// <foo name="value">|
// then the DOM model will consider the region containing the caret to be
// whatever nodes FOLLOWS the element, usually a text node.
// Detect this case, and look into the previous range.
if (currentIndexedRegion instanceof Text
&& currentIndexedRegion.getStartOffset() == end && end > 0) {
end--;
currentIndexedRegion = model.getIndexedRegion(end);
endRegion = document.getRegionAtCharacterOffset(
currentIndexedRegion.getStartOffset());
}
if (currentIndexedRegion instanceof IDOMNode) {
IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
endNode = currentDOMNode;
// See if this range is fully within the opening tag
if (endNode == startNode && endRegion == startRegion) {
ITextRegion subRegion = endRegion.getRegionAtCharacterOffset(end);
ITextRegionList regions = endRegion.getRegions();
int index = regions.indexOf(subRegion);
if (index != -1) {
// Skip past initial occurrence of close tag if we place the caret
// right on a >
subRegion = regions.get(index);
String type = subRegion.getType();
if (type == XML_TAG_CLOSE || type == XML_EMPTY_TAG_CLOSE) {
index--;
}
}
for (; index >= 0; index--) {
subRegion = regions.get(index);
String type = subRegion.getType();
if (type == XML_TAG_OPEN) {
isOpenTagOnly = true;
} else if (type == XML_EMPTY_TAG_CLOSE || type == XML_TAG_CLOSE
|| type == XML_END_TAG_OPEN) {
break;
}
}
int max = regions.size();
for (index = Math.max(0, index); index < max; index++) {
subRegion = regions.get(index);
String type = subRegion.getType();
if (type == XML_EMPTY_TAG_CLOSE || type == XML_TAG_CLOSE) {
openTagEnd = subRegion.getEnd() + endRegion.getStartOffset();
}
}
if (openTagEnd == -1) {
isOpenTagOnly = false;
}
}
}
}
String[] indentationLevels = null;
Node root = null;
int initialDepth = 0;
int replaceStart;
int replaceEnd;
boolean endWithNewline = false;
if (startNode == null || endNode == null) {
// Process the entire document
root = domDocument;
// both document and documentElement should be <= 0
initialDepth = -1;
startNode = root;
endNode = root;
replaceStart = 0;
replaceEnd = document.getLength();
try {
endWithNewline = replaceEnd > 0 && document.getChar(replaceEnd - 1) == '\n';
} catch (BadLocationException e) {
// Can't happen
}
} else {
root = DomUtilities.getCommonAncestor(startNode, endNode);
initialDepth = root != null ? DomUtilities.getDepth(root) - 1 : 0;
// Regions must be non-null since the DOM nodes are non null, but Eclipse null
// analysis doesn't realize it:
assert startRegion != null && endRegion != null;
replaceStart = ((IndexedRegion) startNode).getStartOffset();
if (isOpenTagOnly) {
replaceEnd = openTagEnd;
} else {
replaceEnd = ((IndexedRegion) endNode).getEndOffset();
}
// Look up the indentation level of the start node, if it is an element
// and it starts on its own line
if (startNode.getNodeType() == Node.ELEMENT_NODE) {
// Measure the indentation of the start node such that we can indent
// the reformatted version of the node exactly in place and it should blend
// in if the surrounding content does not use the same indentation size etc.
// However, it's possible for the start node to have deeper depth than other
// content we're formatting, as in the following scenario for example:
// <foo>
// <bar/>
// </foo>
// <baz/>
// If you select this text range, we want <foo> to be formatted at whatever
// level it is, and we also need to know the indentation level to use
// for </baz>. We don't measure the depth of <bar/>, a child of the start node,
// since from the initial indentation level and on down we want to normalize
// the output.
IndentationMeasurer m = new IndentationMeasurer(startNode, endNode, document);
indentationLevels = m.measure(initialDepth, root);
// Wipe out any levels deeper than the start node's level
// (which may not be the smallest level, e.g. where you select a child
// and the end of its parent etc).
// (Since we're ONLY measuring the node and its parents, you might wonder
// why this is doing a full subtree traversal instead of just walking up
// the parent chain and looking up the indentation for each. The reason for
// this is that some of theses nodes, which have not yet been formatted,
// may be sharing lines with other nodes, and we disregard indentation for
// any nodes that don't start a line since the indentation may only be correct
// for the first element, so therefore we look for other nodes at the same
// level that do have indentation info at the front of the line.
int depth = DomUtilities.getDepth(startNode) - 1;
for (int i = depth + 1; i < indentationLevels.length; i++) {
indentationLevels[i] = null;
}
}
}
XmlFormatStyle style = guessStyle(model, domDocument);
XmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
String delimiter = TextUtilities.getDefaultLineDelimiter(document);
XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, delimiter);
printer.setEndWithNewline(endWithNewline);
if (indentationLevels != null) {
printer.setIndentationLevels(indentationLevels);
}
StringBuilder sb = new StringBuilder(length);
printer.prettyPrint(initialDepth, root, startNode, endNode, sb, isOpenTagOnly);
String formatted = sb.toString();
ReplaceEdit replaceEdit = createReplaceEdit(document, replaceStart, replaceEnd, formatted,
prefs);
if (replaceEdit != null) {
edit.addChild(replaceEdit);
}
// Attempt to fix the selection range since otherwise, with the document shifting
// under it, you end up selecting a "random" portion of text now shifted into the
// old positions of the formatted text:
if (replaceEdit != null && replaceStart != 0 && replaceEnd != document.getLength()) {
ITextEditor editor = AdtUtils.getActiveTextEditor();
if (editor != null) {
editor.setHighlightRange(replaceEdit.getOffset(), replaceEdit.getText().length(),
false /*moveCursor*/);
}
}
return edit;
}
/**
* Create a {@link ReplaceEdit} which replaces the text in the given document with the
* given new formatted content. The replaceStart and replaceEnd parameters point to
* the equivalent unformatted text in the document, but the actual edit range may be
* adjusted (for example to make the edit smaller if the beginning and/or end is
* identical, and so on)
*/
@VisibleForTesting
static ReplaceEdit createReplaceEdit(IDocument document, int replaceStart,
int replaceEnd, String formatted, XmlFormatPreferences prefs) {
// If replacing a node somewhere in the middle, start the replacement at the
// beginning of the current line
int index = replaceStart;
try {
while (index > 0) {
char c = document.getChar(index - 1);
if (c == '\n') {
if (index < replaceStart) {
replaceStart = index;
}
break;
} else if (!Character.isWhitespace(c)) {
// The replaced node does not start on its own line; in that case,
// remove the initial indentation in the reformatted element
for (int i = 0; i < formatted.length(); i++) {
if (!Character.isWhitespace(formatted.charAt(i))) {
formatted = formatted.substring(i);
break;
}
}
break;
}
index--;
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
// If there are multiple blank lines before the insert position, collapse them down
// to one
int prevNewlineIndex = -1;
boolean beginsWithNewline = false;
for (int i = 0, n = formatted.length(); i < n; i++) {
char c = formatted.charAt(i);
if (c == '\n') {
beginsWithNewline = true;
break;
} else if (!Character.isWhitespace(c)) { // \r is whitespace so is handled correctly
break;
}
}
try {
for (index = replaceStart - 1; index > 0; index--) {
char c = document.getChar(index);
if (c == '\n') {
if (prevNewlineIndex != -1) {
replaceStart = prevNewlineIndex;
}
prevNewlineIndex = index;
if (index > 0 && document.getChar(index - 1) == '\r') {
prevNewlineIndex--;
}
} else if (!Character.isWhitespace(c)) {
break;
}
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
if (prefs.removeEmptyLines && prevNewlineIndex != -1 && beginsWithNewline) {
replaceStart = prevNewlineIndex + 1;
}
// Search forwards too
int nextNewlineIndex = -1;
try {
int max = document.getLength();
for (index = replaceEnd; index < max; index++) {
char c = document.getChar(index);
if (c == '\n') {
if (nextNewlineIndex != -1) {
replaceEnd = nextNewlineIndex + 1;
}
nextNewlineIndex = index;
} else if (!Character.isWhitespace(c)) {
break;
}
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
boolean endsWithNewline = false;
for (int i = formatted.length() - 1; i >= 0; i--) {
char c = formatted.charAt(i);
if (c == '\n') {
endsWithNewline = true;
break;
} else if (!Character.isWhitespace(c)) {
break;
}
}
if (prefs.removeEmptyLines && nextNewlineIndex != -1 && endsWithNewline) {
replaceEnd = nextNewlineIndex + 1;
}
// Figure out how much of the before and after strings are identical and narrow
// the replacement scope
boolean foundDifference = false;
int firstDifference = 0;
int lastDifference = formatted.length();
try {
for (int i = 0, j = replaceStart; i < formatted.length() && j < replaceEnd; i++, j++) {
if (formatted.charAt(i) != document.getChar(j)) {
firstDifference = i;
foundDifference = true;
break;
}
}
if (!foundDifference) {
// No differences - the document is already formatted, nothing to do
return null;
}
lastDifference = firstDifference + 1;
for (int i = formatted.length() - 1, j = replaceEnd - 1;
i > firstDifference && j > replaceStart;
i--, j--) {
if (formatted.charAt(i) != document.getChar(j)) {
lastDifference = i + 1;
break;
}
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
replaceStart += firstDifference;
replaceEnd -= (formatted.length() - lastDifference);
replaceEnd = Math.max(replaceStart, replaceEnd);
formatted = formatted.substring(firstDifference, lastDifference);
ReplaceEdit replaceEdit = new ReplaceEdit(replaceStart, replaceEnd - replaceStart,
formatted);
return replaceEdit;
}
/**
* Guess what style to use to edit the given document - layout, resource, manifest, ... ? */
static XmlFormatStyle guessStyle(IStructuredModel model, Document domDocument) {
// The "layout" style is used for most XML resource file types:
// layouts, color-lists and state-lists, animations, drawables, menus, etc
XmlFormatStyle style = XmlFormatStyle.get(domDocument);
if (style == XmlFormatStyle.FILE) {
style = XmlFormatStyle.LAYOUT;
}
// The "resource" style is used for most value-based XML files:
// strings, dimensions, booleans, colors, integers, plurals,
// integer-arrays, string-arrays, and typed-arrays
Element rootElement = domDocument.getDocumentElement();
if (rootElement != null
&& SdkConstants.TAG_RESOURCES.equals(rootElement.getTagName())) {
style = XmlFormatStyle.RESOURCE;
}
// Selectors are also used similar to resources
if (rootElement != null && SELECTOR_TAG.equals(rootElement.getTagName())) {
return XmlFormatStyle.RESOURCE;
}
// The "manifest" style is used for manifest files
String baseLocation = model.getBaseLocation();
if (baseLocation != null) {
if (baseLocation.endsWith(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
style = XmlFormatStyle.MANIFEST;
} else {
int lastSlash = baseLocation.lastIndexOf('/');
if (lastSlash != -1) {
int end = baseLocation.lastIndexOf('/', lastSlash - 1); // -1 is okay
String resourceFolder = baseLocation.substring(end + 1, lastSlash);
String[] segments = resourceFolder.split("-"); //$NON-NLS-1$
ResourceType type = ResourceType.getEnum(segments[0]);
if (type != null) {
// <resources> files found in res/xml/ should be formatted as
// resource files!
if (type == ResourceType.XML && style == XmlFormatStyle.RESOURCE) {
return style;
}
style = EclipseXmlPrettyPrinter.get(type);
}
}
}
}
return style;
}
private Boolean isAndroid(IDocument document) {
if (mIsAndroid == null) {
// Look up the corresponding IResource for this document. This isn't
// readily available, so take advantage of the structured model's base location
// string and convert it to an IResource to look up its project.
if (document instanceof IStructuredDocument) {
IStructuredDocument structuredDocument = (IStructuredDocument) document;
IModelManager modelManager = StructuredModelManager.getModelManager();
IStructuredModel model = modelManager.getModelForRead(structuredDocument);
if (model != null) {
String location = model.getBaseLocation();
model.releaseFromRead();
if (location != null) {
if (!location.endsWith(ANDROID_MANIFEST_XML)
&& !location.contains("/res/")) { //$NON-NLS-1$
// See if it looks like a foreign document
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceRoot root = workspace.getRoot();
IResource member = root.findMember(location);
if (member.exists()) {
IProject project = member.getProject();
if (project.isAccessible() &&
!BaseProjectHelper.isAndroidProject(project)) {
mIsAndroid = false;
return false;
}
}
}
// Ignore Maven POM files even in Android projects
if (location.endsWith("/pom.xml")) { //$NON-NLS-1$
mIsAndroid = false;
return false;
}
}
}
}
mIsAndroid = true;
}
return mIsAndroid.booleanValue();
}
@Override
public void formatterStarts(final IFormattingContext context) {
// Use Eclipse XML formatter instead?
ContextBasedFormattingStrategy delegate = getDelegate();
if (delegate != null) {
delegate.formatterStarts(context);
// We also need the super implementation because it stores items into the
// map, and we can't override the getPreferences method, so we need for
// this delegating strategy to supply the correct values when it is called
// instead of the delegate
super.formatterStarts(context);
return;
}
super.formatterStarts(context);
mRegion = (IRegion) context.getProperty(CONTEXT_REGION);
TypedPosition partition = (TypedPosition) context.getProperty(CONTEXT_PARTITION);
IDocument document = (IDocument) context.getProperty(CONTEXT_MEDIUM);
mPartitions.offer(partition);
mDocuments.offer(document);
if (!isAndroid(document)) {
// It's some foreign type of project: use default
// formatter
delegate = getDelegate();
if (delegate != null) {
delegate.formatterStarts(context);
}
}
}
@Override
public void formatterStops() {
// Use Eclipse XML formatter instead?
ContextBasedFormattingStrategy delegate = getDelegate();
if (delegate != null) {
delegate.formatterStops();
// See formatterStarts for an explanation
super.formatterStops();
return;
}
super.formatterStops();
mRegion = null;
mDocuments.clear();
mPartitions.clear();
}
/**
* Utility class which can measure the indentation strings for various node levels in
* a given node range
*/
static class IndentationMeasurer {
private final Map<Integer, String> mDepth = new HashMap<Integer, String>();
private final Node mStartNode;
private final Node mEndNode;
private final IStructuredDocument mDocument;
private boolean mDone = false;
private boolean mInRange = false;
private int mMaxDepth;
public IndentationMeasurer(Node mStartNode, Node mEndNode, IStructuredDocument document) {
super();
this.mStartNode = mStartNode;
this.mEndNode = mEndNode;
mDocument = document;
}
/**
* Measure the various depths found in the range (defined in the constructor)
* under the given node which should be a common ancestor of the start and end
* nodes. The result is a string array where each index corresponds to a depth,
* and the string is either empty, or the complete indentation string to be used
* to indent to the given depth (note that these strings are not cumulative)
*
* @param initialDepth the initial depth to use when visiting
* @param root the root node to look for depths under
* @return a string array containing nulls or indentation strings
*/
public String[] measure(int initialDepth, Node root) {
visit(initialDepth, root);
String[] indentationLevels = new String[mMaxDepth + 1];
for (Map.Entry<Integer, String> entry : mDepth.entrySet()) {
int depth = entry.getKey();
String indentation = entry.getValue();
indentationLevels[depth] = indentation;
}
return indentationLevels;
}
private void visit(int depth, Node node) {
// Look up indentation for this level
if (node.getNodeType() == Node.ELEMENT_NODE && mDepth.get(depth) == null) {
// Look up the depth
try {
IndexedRegion region = (IndexedRegion) node;
int lineStart = findLineStart(mDocument, region.getStartOffset());
int textStart = findTextStart(mDocument, lineStart, region.getEndOffset());
// Ensure that the text which begins the line is this element, otherwise
// we could be measuring the indentation of a parent element which begins
// the line
if (textStart == region.getStartOffset()) {
String indent = mDocument.get(lineStart,
Math.max(0, textStart - lineStart));
mDepth.put(depth, indent);
if (depth > mMaxDepth) {
mMaxDepth = depth;
}
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
}
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
visit(depth + 1, child);
if (mDone) {
return;
}
}
if (node == mEndNode) {
mDone = true;
}
}
}
}