blob: 255520fdf34e9b2383b298c7640c33a135583391 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* David Carver (STAR) - bug 297006 - String Comparison
*******************************************************************************/
package org.eclipse.wst.xml.core.internal.formatter;
import java.util.Iterator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.sse.core.StructuredModelManager;
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.Logger;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMText;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import org.eclipse.wst.xml.core.internal.ssemodelquery.ModelQueryAdapter;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class DefaultXMLPartitionFormatter {
/**
* Just a small container class that holds a DOMNode & documentRegion that
* should represent each other.
*/
protected class DOMRegion {
public IDOMNode domNode;
public IStructuredDocumentRegion documentRegion;
}
static private final String PRESERVE = "preserve";//$NON-NLS-1$
static private final String COLLAPSE = "collapse";//$NON-NLS-1$
static private final String REPLACE = "replace";//$NON-NLS-1$
static private final String PRESERVE_QUOTED = "\"preserve\"";//$NON-NLS-1$
static private final String XML_SPACE = "xml:space";//$NON-NLS-1$
static private final String XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform"; //$NON-NLS-1$
static private final String XSL_ATTRIBUTE = "attribute"; //$NON-NLS-1$
static private final String XSL_TEXT = "text"; //$NON-NLS-1$
static private final String SPACE = " "; //$NON-NLS-1$
static private final String EMPTY = ""; //$NON-NLS-1$
static private final String PROPERTY_WHITESPACE_FACET = "org.eclipse.wst.xsd.cm.properties/whitespace"; //$NON-NLS-1$
private XMLFormattingPreferences fPreferences = null;
private IProgressMonitor fProgressMonitor;
private int replaceSpaces(TextEdit textEdit, int spaceStartOffset, int availableLineWidth, String whitespaceRun) {
StringBuffer buff = new StringBuffer(whitespaceRun);
for(int i = 0; i < buff.length(); i++) {
buff.setCharAt(i, ' '); //$NON-NLS-1$
}
String replacementString = buff.toString();
if (!replacementString.equals(whitespaceRun)) {
ReplaceEdit replaceEdit = new ReplaceEdit(spaceStartOffset, whitespaceRun.length(), replacementString);
textEdit.addChild(replaceEdit);
}
return availableLineWidth;
}
private int collapseSpaces(TextEdit textEdit, int spaceStartOffset, int availableLineWidth, String whitespaceRun) {
// prefer to use use existing whitespace
int existingWhitespaceOffset = whitespaceRun.indexOf(' ');
if (existingWhitespaceOffset > -1) {
// delete whitespaces before and after existing whitespace
if (existingWhitespaceOffset > 0) {
DeleteEdit deleteEdit = new DeleteEdit(spaceStartOffset, existingWhitespaceOffset);
textEdit.addChild(deleteEdit);
}
if (existingWhitespaceOffset < whitespaceRun.length() - 1) {
int nextOffset = existingWhitespaceOffset + 1;
DeleteEdit deleteEdit = new DeleteEdit(spaceStartOffset + nextOffset, whitespaceRun.length() - nextOffset);
textEdit.addChild(deleteEdit);
}
}
else {
// delete all whitespace and insert new one
// collapse whitespace by deleting whitespace
DeleteEdit deleteEdit = new DeleteEdit(spaceStartOffset, whitespaceRun.length());
textEdit.addChild(deleteEdit);
// then insert one space
InsertEdit insertEdit = new InsertEdit(spaceStartOffset, SPACE);
textEdit.addChild(insertEdit);
}
// remember to account for space added
--availableLineWidth;
return availableLineWidth;
}
private int collapseAndIndent(TextEdit textEdit, int spaceStartOffset, int availableLineWidth, int indentLevel, String whitespaceRun, IStructuredDocumentRegion currentRegion) {
// Need to keep blank lines, but still collapse the whitespace
String lineDelimiters = null;
if (!getFormattingPreferences().getClearAllBlankLines()) {
lineDelimiters = extractLineDelimiters(whitespaceRun, currentRegion);
String formattedLine = lineDelimiters + getIndentString(indentLevel);
if(lineDelimiters.length() > 0 && !formattedLine.equals(whitespaceRun)) {
textEdit.addChild(new ReplaceEdit(spaceStartOffset, whitespaceRun.length(), formattedLine));
availableLineWidth = getFormattingPreferences().getMaxLineWidth() - indentLevel;
}
}
if (lineDelimiters == null || lineDelimiters.length() == 0) {
availableLineWidth = collapseSpaces(textEdit, spaceStartOffset, availableLineWidth, whitespaceRun);
}
return availableLineWidth;
}
private void deleteTrailingSpaces(TextEdit textEdit, ITextRegion currentTextRegion, IStructuredDocumentRegion currentDocumentRegion) {
int textEnd = currentTextRegion.getTextEnd();
int textEndOffset = currentDocumentRegion.getStartOffset() + textEnd;
int difference = currentTextRegion.getEnd() - textEnd;
DeleteEdit deleteEdit = new DeleteEdit(textEndOffset, difference);
textEdit.addChild(deleteEdit);
}
public TextEdit format(IDocument document, int start, int length) {
return format(document, start, length, new XMLFormattingPreferences());
}
public TextEdit format(IDocument document, int start, int length, XMLFormattingPreferences preferences) {
TextEdit edit = null;
if (document instanceof IStructuredDocument) {
IStructuredModel model = StructuredModelManager.getModelManager().getModelForEdit((IStructuredDocument) document);
if (model != null) {
try {
edit = format(model, start, length, preferences);
}
finally {
model.releaseFromEdit();
}
}
}
return edit;
}
public TextEdit format(IStructuredModel model, int start, int length) {
return format(model, start, length, new XMLFormattingPreferences());
}
public TextEdit format(IStructuredModel model, int start, int length, XMLFormattingPreferences preferences) {
setFormattingPreferences(preferences);
TextEdit edit = new MultiTextEdit();
IStructuredDocument document = model.getStructuredDocument();
// get initial document region
IStructuredDocumentRegion currentRegion = document.getRegionAtCharacterOffset(start);
if (currentRegion != null) {
int startOffset = currentRegion.getStartOffset();
// get initial dom node
IndexedRegion currentIndexedRegion = model.getIndexedRegion(startOffset);
if (currentIndexedRegion instanceof IDOMNode) {
// set up domRegion which will contain current region to be
// formatted
IDOMNode currentDOMNode = (IDOMNode) currentIndexedRegion;
DOMRegion domRegion = new DOMRegion();
domRegion.documentRegion = currentRegion;
domRegion.domNode = currentDOMNode;
XMLFormattingConstraints parentConstraints = getRegionConstraints(currentDOMNode);
/* if the whitespace strategy is declared as default, get it from the preferences */
if(XMLFormattingConstraints.DEFAULT.equals(parentConstraints.getWhitespaceStrategy()))
parentConstraints.setWhitespaceStrategy(preferences.getElementWhitespaceStrategy());
// TODO: initialize indentLevel
// initialize available line width
int lineWidth = getFormattingPreferences().getMaxLineWidth();
try {
IRegion lineInfo = document.getLineInformationOfOffset(startOffset);
lineWidth = lineWidth - (startOffset - lineInfo.getOffset());
}
catch (BadLocationException e) {
Logger.log(Logger.WARNING_DEBUG, e.getMessage(), e);
}
parentConstraints.setAvailableLineWidth(lineWidth);
// format all siblings (and their children) as long they
// overlap with start/length
Position formatRange = new Position(start, length);
formatSiblings(edit, domRegion, parentConstraints, formatRange);
}
}
return edit;
}
/**
* Determines the formatting constraints for a specified node based on
* its ancestors' formatting. In particular, if any ancestor node either
* explicitly defines whitespace preservation or ignorance, that
* whitespace strategy should be used for <code>currentNode</code> and
* all of its descendants.
*
* @param currentNode the node to investigate the ancestry of to determine
* formatting constraints
*
* @return formatting constraints defined by an ancestor
*/
private XMLFormattingConstraints getRegionConstraints(IDOMNode currentNode) {
IDOMNode iterator = currentNode;
XMLFormattingConstraints result = new XMLFormattingConstraints();
DOMRegion region = new DOMRegion();
XMLFormattingConstraints parentConstraints = new XMLFormattingConstraints();
boolean parent = true;
/* Iterate through the ancestry to find if any explicit whitespace strategy has
* been defined
*/
while(iterator != null && iterator.getNodeType() != Node.DOCUMENT_NODE) {
iterator = (IDOMNode) iterator.getParentNode();
region.domNode = iterator;
region.documentRegion = iterator.getFirstStructuredDocumentRegion();
updateFormattingConstraints(null, null, result, region);
/* If this is the parent of the current node, keep the constraints
* in case no other constraints are identified
*/
if(parent) {
parentConstraints.copyConstraints(result);
parent = false;
}
/* A parent who has defined a specific whitespace strategy was found */
if(XMLFormattingConstraints.PRESERVE.equals(result.getWhitespaceStrategy()) || XMLFormattingConstraints.DEFAULT.equals(result.getWhitespaceStrategy()))
return result;
}
return parentConstraints;
}
// private XMLFormattingConstraints getRegionConstraints(IDOMNode currentNode) {
// IDOMNode iterator = (IDOMNode) currentNode.getParentNode();
// XMLFormattingConstraints result = new XMLFormattingConstraints();
// DOMRegion region = new DOMRegion();
//
// /* Iterate through the ancestry to find if any explicit whitespace strategy has
// * been defined
// */
// while(iterator != null && iterator.getNodeType() != Node.DOCUMENT_NODE) {
// region.domNode = iterator;
// region.documentRegion = iterator.getFirstStructuredDocumentRegion();
//
// updateFormattingConstraints(null, null, result, region);
//
// /* A parent who has defined a specific whitespace strategy was found */
// if(XMLFormattingConstraints.PRESERVE == result.getWhitespaceStrategy() || XMLFormattingConstraints.DEFAULT == result.getWhitespaceStrategy())
// return result;
//
// iterator = (IDOMNode) iterator.getParentNode();
// }
//
// return null;
// }
/**
* Formats the given xml content region
*
* @param textEdit
* @param formatRange
* @param parentConstraints
* @param currentDOMRegion
* @param previousRegion
*/
private void formatContent(TextEdit textEdit, Position formatRange, XMLFormattingConstraints parentConstraints, DOMRegion currentDOMRegion, IStructuredDocumentRegion previousRegion) {
IStructuredDocumentRegion currentRegion = currentDOMRegion.documentRegion;
String fullText = currentDOMRegion.domNode.getSource();
// check if in preserve space mode, if so, don't touch anything but
// make sure to update available line width
String whitespaceMode = parentConstraints.getWhitespaceStrategy();
if (XMLFormattingConstraints.PRESERVE.equals(whitespaceMode)) {
int availableLineWidth = parentConstraints.getAvailableLineWidth();
availableLineWidth = updateLineWidthWithLastLine(fullText, availableLineWidth);
// update available line width in constraints
parentConstraints.setAvailableLineWidth(availableLineWidth);
// A text node can contain multiple structured document regions - sync the documentRegion
// with the last region of the node since the text from all regions was formatted
currentDOMRegion.documentRegion = currentDOMRegion.domNode.getLastStructuredDocumentRegion();
return;
}
// if content is just whitespace and there's something after it
// just skip over this region because region will take care of it
boolean isAllWhitespace = ((IDOMText) currentDOMRegion.domNode).isElementContentWhitespace();
IStructuredDocumentRegion nextDocumentRegion = null;
if (isAllWhitespace) {
parentConstraints.setAvailableLineWidth(fPreferences.getMaxLineWidth());
nextDocumentRegion = currentRegion.getNext();
if (nextDocumentRegion != null)
return;
}
// special handling if text follows an entity or cdata region
if (!XMLFormattingConstraints.COLLAPSE.equals(whitespaceMode) && previousRegion != null) {
String previouRegionType = previousRegion.getType();
if (DOMRegionContext.XML_ENTITY_REFERENCE.equals(previouRegionType) || DOMRegionContext.XML_CDATA_TEXT.equals(previouRegionType))
whitespaceMode = XMLFormattingConstraints.COLLAPSE;
}
// also, special handling if text is before an entity or cdata region
if (!XMLFormattingConstraints.COLLAPSE.equals(whitespaceMode)) {
// get next document region if dont already have it
if (nextDocumentRegion == null)
nextDocumentRegion = currentRegion.getNext();
if (nextDocumentRegion != null) {
String nextRegionType = nextDocumentRegion.getType();
if (DOMRegionContext.XML_ENTITY_REFERENCE.equals(nextRegionType) || DOMRegionContext.XML_CDATA_TEXT.equals(nextRegionType))
whitespaceMode = XMLFormattingConstraints.COLLAPSE;
}
}
formatTextInContent(textEdit, parentConstraints, currentRegion, fullText, whitespaceMode);
// A text node can contain multiple structured document regions - sync the documentRegion
// with the last region of the node since the text from all regions was formatted
currentDOMRegion.documentRegion = currentDOMRegion.domNode.getLastStructuredDocumentRegion();
}
private void formatEmptyStartTagWithNoAttr(TextEdit textEdit, XMLFormattingConstraints constraints, IStructuredDocumentRegion currentDocumentRegion, IStructuredDocumentRegion previousDocumentRegion, int availableLineWidth, String indentStrategy, String whitespaceStrategy, ITextRegion currentTextRegion) {
// get preference if there should be a space or not between tag
// name and empty tag close
// <tagName />
boolean oneSpaceInTagName = getFormattingPreferences().getSpaceBeforeEmptyCloseTag();
// calculate available line width
int tagNameLineWidth = currentTextRegion.getTextLength() + 3;
if (oneSpaceInTagName) {
// add one more to account for space before empty tag close
++tagNameLineWidth;
}
availableLineWidth -= tagNameLineWidth;
if (XMLFormattingConstraints.INLINE.equals(indentStrategy)) {
// if was inlining, need to check if out of available line
// width
if (availableLineWidth < 0) {
// need to indent if possible
int lineWidth = indentIfPossible(textEdit, constraints, currentDocumentRegion, previousDocumentRegion, whitespaceStrategy, indentStrategy, true);
// update available line width
if (lineWidth > 0)
availableLineWidth = lineWidth - tagNameLineWidth;
else
availableLineWidth -= tagNameLineWidth;
}
else {
// no need to indent
// just make sure to delete previous whitespace if
// needed
if ((DOMRegionContext.XML_CONTENT.equals(previousDocumentRegion.getType())) && (previousDocumentRegion.getFullText().trim().length() == 0)) {
availableLineWidth = collapseSpaces(textEdit, previousDocumentRegion.getStartOffset(), availableLineWidth, previousDocumentRegion.getFullText());
}
}
}
// delete any trail spaces after tag name
int textLength = currentTextRegion.getTextLength();
int regionLength = currentTextRegion.getLength();
boolean thereAreSpaces = textLength < regionLength;
if (!oneSpaceInTagName && thereAreSpaces) {
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
}
else if(oneSpaceInTagName) {
insertSpaceAndCollapse(textEdit, currentDocumentRegion, availableLineWidth, currentTextRegion);
}
constraints.setAvailableLineWidth(availableLineWidth);
}
/**
* Formats an end tag
*
* @param textEdit
* @param currentRegion
* @param textRegions
*/
private void formatEndTag(TextEdit textEdit, Position formatRange, XMLFormattingConstraints constraints, DOMRegion currentDOMRegion, IStructuredDocumentRegion previousDocumentRegion) {
IStructuredDocumentRegion currentDocumentRegion = currentDOMRegion.documentRegion;
String whitespaceStrategy = constraints.getWhitespaceStrategy();
String indentStrategy = constraints.getIndentStrategy();
// do not format space before start tag if preserving spaces
if (whitespaceStrategy != XMLFormattingConstraints.PRESERVE) {
// format like indent strategy says
if (XMLFormattingConstraints.INDENT.equals(indentStrategy) || XMLFormattingConstraints.NEW_LINE.equals(indentStrategy)) {
int availableLineWidth = indentIfPossible(textEdit, constraints, currentDocumentRegion, previousDocumentRegion, whitespaceStrategy, indentStrategy, false);
constraints.setAvailableLineWidth(availableLineWidth);
}
}
// format the end tag itself
formatWithinEndTag(textEdit, constraints, currentDocumentRegion, previousDocumentRegion);
}
/**
* Formats the given region (and all its children) contained in domRegion.
*
* @param edit
* edits required to format
* @param formatRange
* document range to format (only format content within this
* range)
* @param parentConstraints
* @param domRegion
* assumes dom node & region are not null
* @param previousRegion
* could be null
* @return Returns the last region formatted
*/
private DOMRegion formatRegion(TextEdit edit, Position formatRange, XMLFormattingConstraints parentConstraints, DOMRegion domRegion, IStructuredDocumentRegion previousRegion) {
IStructuredDocumentRegion currentRegion = domRegion.documentRegion;
String regionType = currentRegion.getType();
if (DOMRegionContext.XML_TAG_NAME.equals(regionType)) {
ITextRegion textRegion = currentRegion.getFirstRegion();
String textRegionType = textRegion.getType();
if (DOMRegionContext.XML_TAG_OPEN.equals(textRegionType)) {
domRegion = formatStartTag(edit, formatRange, parentConstraints, domRegion, previousRegion);
}
else if (DOMRegionContext.XML_END_TAG_OPEN.equals(textRegionType)) {
formatEndTag(edit, formatRange, parentConstraints, domRegion, previousRegion);
}
}
else if (DOMRegionContext.XML_CONTENT.equals(regionType) || domRegion.domNode.getNodeType() == Node.TEXT_NODE) {
formatContent(edit, formatRange, parentConstraints, domRegion, previousRegion);
}
else if (DOMRegionContext.XML_COMMENT_TEXT.equals(regionType)) {
formatComment(edit, formatRange, parentConstraints, domRegion, previousRegion);
}
else {
// unknown, so just leave alone for now but make sure to update
// available line width
String fullText = currentRegion.getFullText();
int width = updateLineWidthWithLastLine(fullText, parentConstraints.getAvailableLineWidth());
parentConstraints.setAvailableLineWidth(width);
}
return domRegion;
}
/**
* Formats the domRegion and all of its children and siblings
*
* @param edit
* @param domRegion
* @param parentConstraints
* @param formatRange
*/
private void formatSiblings(TextEdit edit, DOMRegion domRegion, XMLFormattingConstraints parentConstraints, Position formatRange) {
IStructuredDocumentRegion previousRegion = null;
IStructuredDocumentRegion currentRegion = domRegion.documentRegion;
IDOMNode currentDOMNode = domRegion.domNode;
while (currentDOMNode != null && currentRegion != null && formatRange.overlapsWith(currentRegion.getStartOffset(), currentRegion.getLength()) && (fProgressMonitor == null || !fProgressMonitor.isCanceled())) {
domRegion.documentRegion = currentRegion;
domRegion.domNode = currentDOMNode;
// need to make sure current document region and current
// dom node match up
if (currentDOMNode.getFirstStructuredDocumentRegion().equals(currentRegion)) {
// format this document region/node, formatRegion will
// return the last node/region formatted
domRegion = formatRegion(edit, formatRange, parentConstraints, domRegion, previousRegion);
}
else {
// TODO: need to figure out what to do if they don't
// match up
}
previousRegion = domRegion.documentRegion;
// get the next sibling information
if (domRegion.domNode != null)
currentDOMNode = (IDOMNode) domRegion.domNode.getNextSibling();
else
currentDOMNode = null;
currentRegion = previousRegion.getNext();
}
}
/**
* Formats a start tag
*
* @param textEdit
* @param currentRegion
* @param textRegions
*/
private DOMRegion formatStartTag(TextEdit textEdit, Position formatRange, XMLFormattingConstraints parentConstraints, DOMRegion currentDOMRegion, IStructuredDocumentRegion previousDocumentRegion) {
// determine proper indent by referring to parent constraints,
// previous node, and current node
IStructuredDocumentRegion currentDocumentRegion = currentDOMRegion.documentRegion;
IDOMNode currentDOMNode = currentDOMRegion.domNode;
// create a constraint for this tag
XMLFormattingConstraints thisConstraints = new XMLFormattingConstraints();
XMLFormattingConstraints childrenConstraints = new XMLFormattingConstraints();
updateFormattingConstraints(parentConstraints, thisConstraints, childrenConstraints, currentDOMRegion);
if(XMLFormattingConstraints.DEFAULT.equals(childrenConstraints.getWhitespaceStrategy()))
childrenConstraints.setWhitespaceStrategy((new XMLFormattingPreferences()).getElementWhitespaceStrategy());
String whitespaceStrategy = thisConstraints.getWhitespaceStrategy();
String indentStrategy = thisConstraints.getIndentStrategy();
int availableLineWidth = thisConstraints.getAvailableLineWidth();
// format space before start tag
// do not format space before start tag if preserving spaces
if (!XMLFormattingConstraints.PRESERVE.equals(whitespaceStrategy)) {
// format like indent strategy says
if (XMLFormattingConstraints.INDENT.equals(indentStrategy) || XMLFormattingConstraints.NEW_LINE.equals(indentStrategy)) {
availableLineWidth = indentIfPossible(textEdit, thisConstraints, currentDocumentRegion, previousDocumentRegion, whitespaceStrategy, indentStrategy, true);
if (availableLineWidth > 0)
thisConstraints.setAvailableLineWidth(availableLineWidth);
}
}
// format the start tag itself
boolean tagEnded = formatWithinTag(textEdit, thisConstraints, currentDocumentRegion, previousDocumentRegion);
// format children
if (!tagEnded) {
// update childConstraints with thisConstraint's indentLevel &
// availableLineWidth
childrenConstraints.setIndentLevel(thisConstraints.getIndentLevel());
childrenConstraints.setAvailableLineWidth(thisConstraints.getAvailableLineWidth());
previousDocumentRegion = currentDocumentRegion;
IDOMNode childDOMNode = (IDOMNode) currentDOMNode.getFirstChild();
IStructuredDocumentRegion nextRegion = currentDocumentRegion.getNext();
boolean passedFormatRange = false;
// as long as there is one child
if (childDOMNode != null && nextRegion != null) {
while (childDOMNode != null && nextRegion != null && !passedFormatRange && (fProgressMonitor == null || !fProgressMonitor.isCanceled())) {
DOMRegion childDOMRegion = new DOMRegion();
childDOMRegion.documentRegion = nextRegion;
childDOMRegion.domNode = childDOMNode;
if (childDOMNode.getFirstStructuredDocumentRegion().equals(nextRegion)) {
// format children. pass in child constraints
childDOMRegion = formatRegion(textEdit, formatRange, childrenConstraints, childDOMRegion, previousDocumentRegion);
}
else {
// TODO: what happens if they dont match up?
}
// update childDOMRegion with next dom/region node
if (childDOMRegion.domNode != null) {
childDOMNode = (IDOMNode) childDOMRegion.domNode.getNextSibling();
}
else {
childDOMNode = null;
}
previousDocumentRegion = childDOMRegion.documentRegion;
nextRegion = previousDocumentRegion.getNext();
if (nextRegion != null)
passedFormatRange = !formatRange.overlapsWith(nextRegion.getStartOffset(), nextRegion.getLength());
}
}
else {
// there were no children, so keep end tag inlined
childrenConstraints.setWhitespaceStrategy(XMLFormattingConstraints.COLLAPSE);
childrenConstraints.setIndentStrategy(XMLFormattingConstraints.INLINE);
}
if (!passedFormatRange) {
// update the dom region with the last formatted region/dom
// node should be end tag and this tag's DOMNode
currentDOMRegion.documentRegion = nextRegion;
currentDOMRegion.domNode = currentDOMNode;
// end tag's indent level should be same as start tag's
childrenConstraints.setIndentLevel(thisConstraints.getIndentLevel());
// format end tag
boolean formatEndTag = false;
if (nextRegion != null && currentDOMNode != null) {
ITextRegionList rs = nextRegion.getRegions();
if (rs.size() > 1) {
ITextRegion r = rs.get(0);
if (r != null && DOMRegionContext.XML_END_TAG_OPEN.equals(r.getType())) {
r = rs.get(1);
if (r != null && DOMRegionContext.XML_TAG_NAME.equals(r.getType())) {
String tagName = nextRegion.getText(r);
if (tagName != null && tagName.equals(currentDOMNode.getNodeName()))
formatEndTag = true;
}
}
}
}
if (formatEndTag)
formatEndTag(textEdit, formatRange, childrenConstraints, currentDOMRegion, previousDocumentRegion);
else {
// missing end tag so return last formatted document
// region
currentDOMRegion.documentRegion = previousDocumentRegion;
}
}
else {
// passed format range before could finish, so update dom
// region to last known formatted region
currentDOMRegion.documentRegion = nextRegion;
currentDOMRegion.domNode = childDOMNode;
}
// update parent constraint since this is what is passed back
parentConstraints.setAvailableLineWidth(childrenConstraints.getAvailableLineWidth());
}
else {
// update available line width
parentConstraints.setAvailableLineWidth(thisConstraints.getAvailableLineWidth());
}
return currentDOMRegion;
}
private void formatStartTagWithNoAttr(TextEdit textEdit, XMLFormattingConstraints constraints, IStructuredDocumentRegion currentDocumentRegion, IStructuredDocumentRegion previousDocumentRegion, int availableLineWidth, String indentStrategy, String whitespaceStrategy, ITextRegion currentTextRegion) {
// calculate available line width
int tagNameLineWidth = currentTextRegion.getTextLength() + 2;
availableLineWidth -= tagNameLineWidth;
if (XMLFormattingConstraints.INLINE.equals(indentStrategy)) {
// if was inlining, need to check if out of available line
// width
if (availableLineWidth < 0) {
// need to indent if possible
int lineWidth = indentIfPossible(textEdit, constraints, currentDocumentRegion, previousDocumentRegion, whitespaceStrategy, indentStrategy, true);
// update available line width
if (lineWidth > 0)
availableLineWidth = lineWidth - tagNameLineWidth;
else
availableLineWidth -= tagNameLineWidth;
}
else {
// no need to indent
// just make sure to delete previous whitespace if
// needed
if (previousDocumentRegion != null) {
if (DOMRegionContext.XML_CONTENT.equals(previousDocumentRegion.getType())) {
String previousDocumentRegionText = previousDocumentRegion.getFullText();
if (previousDocumentRegionText.trim().length() == 0) {
availableLineWidth = collapseSpaces(textEdit, previousDocumentRegion.getStartOffset(), availableLineWidth, previousDocumentRegionText);
}
}
}
}
}
// delete any trail spaces after tag name
if (currentTextRegion.getTextLength() < currentTextRegion.getLength()) {
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
}
constraints.setAvailableLineWidth(availableLineWidth);
}
/**
* Format the text in xml content
*
* @param textEdit
* @param parentConstraints
* @param currentRegion
* @param fullText
* @param whitespaceMode
*/
private void formatTextInContent(TextEdit textEdit, XMLFormattingConstraints parentConstraints, IStructuredDocumentRegion currentRegion, String fullText, String whitespaceMode) {
int availableLineWidth = parentConstraints.getAvailableLineWidth();
// determine indentation
boolean forceInitialIndent = false;
int indentLevel = parentConstraints.getIndentLevel() + 1;
String indentMode = parentConstraints.getIndentStrategy();
if (XMLFormattingConstraints.INDENT.equals(indentMode)) {
forceInitialIndent = true;
}
if (XMLFormattingConstraints.NEW_LINE.equals(indentMode)) {
indentLevel = parentConstraints.getIndentLevel();
forceInitialIndent = true;
}
int fullTextOffset = 0;
char[] fullTextArray = fullText.toCharArray();
while (fullTextOffset < fullTextArray.length) {
// gather all whitespaces
String whitespaceRun = getCharacterRun(fullTextArray, fullTextOffset, true);
if (whitespaceRun.length() > 0) {
// offset where whitespace starts
int whitespaceStart = fullTextOffset;
// update current offset in fullText
fullTextOffset += whitespaceRun.length();
// gather following word
String characterRun = getCharacterRun(fullTextArray, fullTextOffset, false);
int characterRunLength = characterRun.length();
if (characterRunLength > 0) {
// indent if word is too long or forcing initial
// indent
availableLineWidth -= characterRunLength;
// offset where indent/collapse will happen
int startOffset = currentRegion.getStartOffset() + whitespaceStart;
if (forceInitialIndent || (availableLineWidth <= 0)) {
// indent if not already indented
availableLineWidth = indentIfNotAlreadyIndented(textEdit, currentRegion, indentLevel, startOffset, whitespaceRun);
// remember to subtract word length
availableLineWidth -= characterRunLength;
forceInitialIndent = false; // initial indent done
}
else {
// just collapse spaces, but adjust for any indenting that may result from preserving line delimiters
if (whitespaceStart == 0 && XMLFormattingConstraints.IGNOREANDTRIM.equals(whitespaceMode)) {
// if ignore, trim
DeleteEdit deleteTrailing = new DeleteEdit(startOffset, whitespaceRun.length());
textEdit.addChild(deleteTrailing);
}
else if(XMLFormattingConstraints.REPLACE.equals(whitespaceMode))
availableLineWidth = replaceSpaces(textEdit, startOffset, availableLineWidth, whitespaceRun);
else
availableLineWidth = collapseAndIndent(textEdit, startOffset, availableLineWidth, indentLevel, whitespaceRun, currentRegion);
}
fullTextOffset += characterRunLength;
}
else {
// handle trailing whitespace
int whitespaceOffset = currentRegion.getStartOffset() + whitespaceStart;
if (XMLFormattingConstraints.REPLACE.equals(whitespaceMode))
availableLineWidth = replaceSpaces(textEdit, whitespaceOffset, availableLineWidth, whitespaceRun);
else if (XMLFormattingConstraints.IGNOREANDTRIM.equals(whitespaceMode)) {
// always trim
DeleteEdit deleteTrailing = new DeleteEdit(whitespaceOffset, whitespaceRun.length());
textEdit.addChild(deleteTrailing);
}
else if(getFormattingPreferences().getClearAllBlankLines()) {
if (XMLFormattingConstraints.IGNORE.equals(whitespaceMode)) {
// if ignore, trim
DeleteEdit deleteTrailing = new DeleteEdit(whitespaceOffset, whitespaceRun.length());
textEdit.addChild(deleteTrailing);
}
else {
// if collapse, leave a space. but what if end up
// wanting to add indent? then need to delete space
// added and add indent instead
availableLineWidth = collapseSpaces(textEdit, whitespaceOffset, availableLineWidth, whitespaceRun);
}
}
}
}
else {
// gather word
String characterRun = getCharacterRun(fullTextArray, fullTextOffset, false);
int characterRunLength = characterRun.length();
if (characterRunLength > 0) {
// indent if word is too long or forcing initial
// indent
// [243091] - characterRunLength should only be subtracted once or text formatting wraps prematurely
// availableLineWidth = availableLineWidth - characterRunLength;
if ((XMLFormattingConstraints.IGNORE.equals(whitespaceMode) || XMLFormattingConstraints.IGNOREANDTRIM.equals(whitespaceMode)) && (forceInitialIndent || (availableLineWidth <= 0))) {
// indent if not already indented
availableLineWidth = indentIfNotAlreadyIndented(textEdit, currentRegion, indentLevel, currentRegion.getStartOffset(), whitespaceRun);
// remember to subtract word length
availableLineWidth -= characterRunLength;
forceInitialIndent = false; // initial indent done
}
else {
// just collapse spaces
availableLineWidth -= characterRunLength;
}
fullTextOffset += characterRunLength;
}
}
}
// update available line width
parentConstraints.setAvailableLineWidth(availableLineWidth);
}
private void formatWithinEndTag(TextEdit textEdit, XMLFormattingConstraints constraints, IStructuredDocumentRegion currentDocumentRegion, IStructuredDocumentRegion previousDocumentRegion) {
String indentStrategy = constraints.getIndentStrategy();
String whitespaceStrategy = constraints.getWhitespaceStrategy();
int availableLineWidth = constraints.getAvailableLineWidth();
ITextRegionList textRegions = currentDocumentRegion.getRegions();
int currentTextRegionIndex = 1;
ITextRegion currentTextRegion = textRegions.get(currentTextRegionIndex);
String currentType = currentTextRegion.getType();
// tag name should always be the first text region
if (DOMRegionContext.XML_TAG_NAME.equals(currentType)) {
ITextRegion nextTextRegion = textRegions.get(currentTextRegionIndex + 1);
// Bug 221279 - Some non well-formed documents will not contribute a next region
if (nextTextRegion != null && DOMRegionContext.XML_TAG_CLOSE.equals(nextTextRegion.getType())) {
// calculate available line width
int tagNameLineWidth = currentTextRegion.getTextLength() + 3;
availableLineWidth -= tagNameLineWidth;
if (XMLFormattingConstraints.INLINE.equals(indentStrategy)) {
// if was inlining, need to check if out of available line
// width - Whitespace may have been corrected in the text content
if (availableLineWidth < 0 && XMLFormattingConstraints.IGNORE.equals(whitespaceStrategy)) {
// need to deindent if possible
int lineWidth = indentIfPossible(textEdit, constraints, currentDocumentRegion, previousDocumentRegion, whitespaceStrategy, indentStrategy, false);
// update available line width
if (lineWidth > 0)
availableLineWidth = lineWidth - tagNameLineWidth;
}
else {
// no need to indent
// just make sure to delete previous whitespace if
// needed
if (previousDocumentRegion != null) {
if (DOMRegionContext.XML_CONTENT.equals(previousDocumentRegion.getType())) {
String previousDocumentRegionText = previousDocumentRegion.getFullText();
if (previousDocumentRegionText.trim().length() == 0) {
availableLineWidth = collapseSpaces(textEdit, previousDocumentRegion.getStartOffset(), availableLineWidth, previousDocumentRegionText);
}
}
}
}
}
// delete any trail spaces after tag name
if (currentTextRegion.getTextLength() < currentTextRegion.getLength()) {
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
}
}
}
else {
// end tag has unexpected stuff, so just leave it alone
}
constraints.setAvailableLineWidth(availableLineWidth);
}
/**
* Formats the contents within a tag like tag name and attributes
*
* @param textEdit
* @param currentDocumentRegion
* @param textRegions
* contains at least 3 regions
* @return true if tag was ended, false otherwise
*/
private boolean formatWithinTag(TextEdit textEdit, XMLFormattingConstraints constraints, IStructuredDocumentRegion currentDocumentRegion, IStructuredDocumentRegion previousDocumentRegion) {
int availableLineWidth = constraints.getAvailableLineWidth();
String indentStrategy = constraints.getIndentStrategy();
String whitespaceStrategy = constraints.getWhitespaceStrategy();
int indentLevel = constraints.getIndentLevel();
ITextRegionList textRegions = currentDocumentRegion.getRegions();
int currentTextRegionIndex = 1;
ITextRegion currentTextRegion = textRegions.get(currentTextRegionIndex);
String currentType = currentTextRegion.getType();
// tag name should always be the first text region
if (DOMRegionContext.XML_TAG_NAME.equals(currentType)) {
ITextRegion nextTextRegion = textRegions.get(currentTextRegionIndex + 1);
String nextType = (nextTextRegion != null) ? nextTextRegion.getType() : null;
if (DOMRegionContext.XML_TAG_CLOSE.equals(nextType)) {
// already at tag close
formatStartTagWithNoAttr(textEdit, constraints, currentDocumentRegion, previousDocumentRegion, availableLineWidth, indentStrategy, whitespaceStrategy, currentTextRegion);
return false;
}
else if (DOMRegionContext.XML_EMPTY_TAG_CLOSE.equals(nextType)) {
// already at empty tag close
formatEmptyStartTagWithNoAttr(textEdit, constraints, currentDocumentRegion, previousDocumentRegion, availableLineWidth, indentStrategy, whitespaceStrategy, currentTextRegion);
return true;
}
else {
availableLineWidth -= (currentTextRegion.getTextLength() + 2);
boolean alignFinalBracket = getFormattingPreferences().getAlignFinalBracket();
boolean oneSpaceInTagName = getFormattingPreferences().getSpaceBeforeEmptyCloseTag();
boolean indentMultipleAttribute = getFormattingPreferences().getIndentMultipleAttributes();
// indicates if tag spanned more than one line
boolean spanMoreThan1Line = false;
// indicates if all attributes should be indented
boolean indentAllAttributes = false;
if (indentMultipleAttribute) {
int attributesCount = 0;
int i = 2;
while (i < textRegions.size() && attributesCount < 2) {
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(textRegions.get(i).getType()))
++attributesCount;
}
indentAllAttributes = (attributesCount > 1);
}
while ((currentTextRegionIndex + 1) < textRegions.size()) {
nextTextRegion = textRegions.get(currentTextRegionIndex + 1);
nextType = nextTextRegion.getType();
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(nextType)) {
boolean indentAttribute = indentAllAttributes;
if (!indentAttribute)
indentAttribute = shouldIndentBeforeAttribute(constraints, textRegions, availableLineWidth, currentTextRegionIndex, currentTextRegion, nextTextRegion);
if (indentAttribute) {
availableLineWidth = indentIfNotAlreadyIndented(textEdit, indentLevel + 1, currentDocumentRegion, currentTextRegion);
spanMoreThan1Line = true;
}
else {
// otherwise, insertSpaceAndCollapse
insertSpaceAndCollapse(textEdit, currentDocumentRegion, availableLineWidth, currentTextRegion);
// update available line width
availableLineWidth -= (currentTextRegion.getTextLength() + 1);
}
}
else if (DOMRegionContext.XML_TAG_CLOSE.equals(nextType)) {
// if need to align bracket on next line, indent
if (alignFinalBracket && spanMoreThan1Line) {
availableLineWidth = indentIfNotAlreadyIndented(textEdit, indentLevel, currentDocumentRegion, currentTextRegion);
--availableLineWidth; // for tag close itself
}
else {
// otherwise, just delete space before tag close
if (currentTextRegion.getTextLength() < currentTextRegion.getLength()) {
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
availableLineWidth -= (currentTextRegion.getTextLength() + 1);
}
}
// update line width
constraints.setAvailableLineWidth(availableLineWidth);
return false;
}
else if (DOMRegionContext.XML_EMPTY_TAG_CLOSE.equals(nextType)) {
int textLength = currentTextRegion.getTextLength();
int regionLength = currentTextRegion.getLength();
boolean thereAreSpaces = textLength < regionLength;
if (!oneSpaceInTagName && thereAreSpaces) {
// delete any trail spaces after tag name
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
availableLineWidth -= (currentTextRegion.getTextLength() + 2);
}
// insert a space and collapse ONLY IF it's specified
else if (oneSpaceInTagName) {
insertSpaceAndCollapse(textEdit, currentDocumentRegion, availableLineWidth, currentTextRegion);
availableLineWidth -= (currentTextRegion.getTextLength() + 3);
}
// update line width
constraints.setAvailableLineWidth(availableLineWidth);
return true;
}
else {
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(currentType) && DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(nextType)) {
if (currentTextRegion.getTextLength() < currentTextRegion.getLength()) {
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
}
// update available width
availableLineWidth -= currentTextRegion.getTextLength();
}
else if (DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(currentType) && DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(nextType)) {
if (currentTextRegion.getTextLength() < currentTextRegion.getLength()) {
deleteTrailingSpaces(textEdit, currentTextRegion, currentDocumentRegion);
}
// update available width
availableLineWidth -= currentTextRegion.getTextLength();
}
else {
// otherwise, insertSpaceAndCollapse
insertSpaceAndCollapse(textEdit, currentDocumentRegion, availableLineWidth, currentTextRegion);
// update available line width
availableLineWidth -= (currentTextRegion.getTextLength() + 1);
}
}
currentTextRegion = nextTextRegion;
currentType = nextType;
++currentTextRegionIndex;
}
}
}
// update line width
constraints.setAvailableLineWidth(availableLineWidth);
return false;
}
/**
* Format an XML comment structured document region.
*/
private void formatComment(TextEdit textEdit, Position formatRange, XMLFormattingConstraints parentConstraints, DOMRegion currentDOMRegion, IStructuredDocumentRegion previousRegion) {
IStructuredDocumentRegion currentRegion = currentDOMRegion.documentRegion;
int lineWidth = parentConstraints.getAvailableLineWidth() - currentRegion.getFullText().length();
// Don't format if we're not exceeding the available line width, or if the whitespace
// strategy is to preserve whitespace - But update line width.
if(currentRegion == null || XMLFormattingConstraints.PRESERVE.equals(parentConstraints.getWhitespaceStrategy()) || !fPreferences.getFormatCommentText()) {
parentConstraints.setAvailableLineWidth(lineWidth);
return;
}
Iterator it = currentRegion.getRegions().iterator();
ITextRegion previous = null;
if (previousRegion == null)
previousRegion = currentRegion.getPrevious();
// Iterate over each text region of the comment
Node parent = currentDOMRegion.domNode.getParentNode();
while(it.hasNext()) {
ITextRegion text = (ITextRegion) it.next();
String type = text.getType();
if (type == DOMRegionContext.XML_COMMENT_OPEN) {
int indentLevel = (parent != null && parent.getNodeType() == Node.DOCUMENT_NODE) ? 0 : 1;
int width = formatCommentStart(textEdit, parentConstraints, indentLevel, currentRegion, previousRegion, text);
parentConstraints.setAvailableLineWidth(width);
}
else if (type == DOMRegionContext.XML_COMMENT_TEXT) {
int indentLevel = (parent != null && parent.getNodeType() == Node.DOCUMENT_NODE) ? -1 : parentConstraints.getIndentLevel();
formatCommentContent(textEdit, parentConstraints, indentLevel, currentRegion, previous, text);
}
previous = text;
}
}
private void formatCommentContent(TextEdit textEdit, XMLFormattingConstraints parentConstraints, int indentLevel, IStructuredDocumentRegion currentRegion, ITextRegion previous, ITextRegion region) {
int lineWidth = parentConstraints.getAvailableLineWidth() - currentRegion.getFullText(previous).length();
// If there's more text than line width available, format
String text = currentRegion.getFullText(region);
compressContent(textEdit, currentRegion, currentRegion.getStartOffset(region), indentLevel + 1, lineWidth, text);
}
private void compressContent(TextEdit textEdit, IStructuredDocumentRegion region, int startOffset, int indentLevel, int lineWidth, String text) {
int length = text.length();
int start = 0, end = 0;
char c = 0;
int resultLength = 0;
boolean joinLines = fPreferences.getJoinCommentLines();
boolean onOwnLine = false;
String indent = getIndentString(indentLevel + 1);
for (int i = 0; i < length; i++) {
c = text.charAt(i);
// Compress whitespace unless its a line delimiter and formatting does not permit joining lines
if (Character.isWhitespace(c)) {
if ((c != '\r' && c!= '\n') || joinLines) {
// Just came off of a word
if (start == end) {
start = end = i;
}
end++;
resultLength++;
}
else {
// correct the indent of this line
lineWidth = fPreferences.getMaxLineWidth();
resultLength = 0;
onOwnLine = true;
// Compress any whitespace before the line delimiter
if (start != end) {
int replaceLength = end - start;
textEdit.addChild(new ReplaceEdit(start + startOffset, replaceLength, EMPTY));
start = end = i;
}
}
}
else {
// Transitioned to a new word
if (start != end) {
int replaceLength = end - start;
if (onOwnLine) {
// If content is on its own line, replace leading whitespace with proper indent
textEdit.addChild(new ReplaceEdit(start + startOffset, replaceLength, indent));
resultLength -= (replaceLength - indent.length());
onOwnLine = false;
}
else if (!(replaceLength == 1 && text.charAt(start) == ' ')) {
textEdit.addChild(new ReplaceEdit(start + startOffset, replaceLength, SPACE));
resultLength -= (replaceLength - 1);
}
start = end = i;
// Make sure the word starts on a new line
if (resultLength > lineWidth) {
lineWidth = fPreferences.getMaxLineWidth();
resultLength = 0;
textEdit.addChild(new InsertEdit(start + startOffset, getLineDelimiter(region) + indent));
}
}
// Word is immediately after line delimiters, indent appropriately
if (onOwnLine) {
textEdit.addChild(new InsertEdit(i + startOffset, indent));
onOwnLine = false;
}
resultLength++;
}
}
// Clean up any dangling whitespace
int replaceLength = end - start;
indent = getIndentString(indentLevel);
if (replaceLength == 0) { // No trailing whitespace
textEdit.addChild(new InsertEdit(length + startOffset, (onOwnLine) ? indent : SPACE));
}
else {
String whitespace = text.substring(start);
String replacement = (onOwnLine) ? indent : SPACE;
if (!whitespace.equals(replacement)) {
textEdit.addChild(new ReplaceEdit(start + startOffset, replaceLength, replacement));
}
}
}
private int formatCommentStart(TextEdit textEdit, XMLFormattingConstraints parentConstraints, int indentLevel, IStructuredDocumentRegion currentRegion, IStructuredDocumentRegion previousRegion, ITextRegion region) {
int lineWidth = parentConstraints.getAvailableLineWidth();
if (previousRegion.getType() == DOMRegionContext.XML_CONTENT) {
String previousText = previousRegion.getFullText();
String trailingWhitespace = getTrailingWhitespace(previousText);
String delimiters = extractLineDelimiters(trailingWhitespace, previousRegion);
if (delimiters != null && delimiters.length() > 0){// && previousText.length() == trailingWhitespace.length()) {
// Format the comment if it's on a new line
int offset = previousRegion.getEnd() - trailingWhitespace.length();
lineWidth = indentIfNotAlreadyIndented(textEdit, currentRegion, parentConstraints.getIndentLevel() + indentLevel, offset, trailingWhitespace);
}
}
return lineWidth;
}
/**
* Returns either a String of whitespace or characters depending on
* forWhitespace
*
* @param fullTextArray
* the text array to look in
* @param textOffset
* the start offset to start searching
* @param forWhitespace
* true if should return whitespaces, false otherwise
* @return a String of either all whitespace or all characters. Never
* returns null
*/
private String getCharacterRun(char[] fullTextArray, int textOffset, boolean forWhitespace) {
StringBuffer characterRun = new StringBuffer();
boolean nonCharacterFound = false;
while (textOffset < fullTextArray.length && !nonCharacterFound) {
char c = fullTextArray[textOffset];
boolean isWhitespace = Character.isWhitespace(c);
if ((forWhitespace && isWhitespace) || (!forWhitespace && !isWhitespace))
characterRun.append(c);
else
nonCharacterFound = true;
++textOffset;
}
return characterRun.toString();
}
private String getTrailingWhitespace(String text) {
StringBuffer whitespaceRun = new StringBuffer();
int index = text.length() - 1;
while(index >= 0) {
char c = text.charAt(index--);
if (Character.isWhitespace(c))
whitespaceRun.insert(0, c);
else
break;
}
return whitespaceRun.toString();
}
private String getIndentString(int indentLevel) {
StringBuffer indentString = new StringBuffer();
String indent = getFormattingPreferences().getOneIndent();
for (int i = 0; i < indentLevel; ++i) {
indentString.append(indent);
}
return indentString.toString();
}
protected XMLFormattingPreferences getFormattingPreferences() {
if (fPreferences == null)
fPreferences = new XMLFormattingPreferences();
return fPreferences;
}
protected void setFormattingPreferences(XMLFormattingPreferences preferences) {
fPreferences = preferences;
}
/**
* Indent if whitespaceRun does not already contain an indent
*
* @param textEdit
* @param indentLevel
* @param indentStartOffset
* @param maxAvailableLineWidth
* @param whitespaceRun
* @return new available line width up to where indented
*/
private int indentIfNotAlreadyIndented(TextEdit textEdit, IStructuredDocumentRegion currentRegion, int indentLevel, int indentStartOffset, String whitespaceRun) {
int maxAvailableLineWidth = getFormattingPreferences().getMaxLineWidth();
int availableLineWidth;
String indentString = getIndentString(indentLevel);
String lineDelimiter = getLineDelimiter(currentRegion);
String newLineAndIndent = lineDelimiter + indentString;
TextEdit indentation = null;
// if not already correctly indented
if (!newLineAndIndent.equals(whitespaceRun)) {
if (getFormattingPreferences().getClearAllBlankLines()) {
if (whitespaceRun != null) {
// replace existing whitespace run
indentation = new ReplaceEdit(indentStartOffset, whitespaceRun.length(), newLineAndIndent);
}
else {
// just insert correct indent
indentation = new InsertEdit(indentStartOffset, newLineAndIndent);
}
}
// Keep the empty lines
else {
// just insert correct indent
if(whitespaceRun == null)
indentation = new InsertEdit(indentStartOffset, newLineAndIndent);
// Need to preserve the number of empty lines, but still indent on the current line properly
else {
String existingDelimiters = extractLineDelimiters(whitespaceRun, currentRegion);
if(existingDelimiters != null && existingDelimiters.length() > 0) {
String formatted = existingDelimiters + indentString;
// Don't perform a replace if the formatted string is the same as the existing whitespaceRun
if(!formatted.equals(whitespaceRun))
indentation = new ReplaceEdit(indentStartOffset, whitespaceRun.length(), formatted);
}
// No blank lines to preserve - correct the indent
else
indentation = new ReplaceEdit(indentStartOffset, whitespaceRun.length(), newLineAndIndent);
}
}
}
if(indentation != null)
textEdit.addChild(indentation);
// update line width
availableLineWidth = maxAvailableLineWidth - indentString.length();
return availableLineWidth;
}
private int indentIfNotAlreadyIndented(TextEdit textEdit, int indentLevel, IStructuredDocumentRegion currentDocumentRegion, ITextRegion currentTextRegion) {
// indent if not already indented
int textLength = currentTextRegion.getTextLength();
int regionLength = currentTextRegion.getLength();
int indentStartOffset = currentDocumentRegion.getTextEndOffset(currentTextRegion);
String fullText = currentDocumentRegion.getFullText(currentTextRegion);
String whitespaceRun = fullText.substring(textLength, regionLength);
// update line width
int availableLineWidth = indentIfNotAlreadyIndented(textEdit, currentDocumentRegion, indentLevel, indentStartOffset, whitespaceRun);
return availableLineWidth;
}
private int indentIfPossible(TextEdit textEdit, XMLFormattingConstraints thisConstraints, IStructuredDocumentRegion currentDocumentRegion, IStructuredDocumentRegion previousDocumentRegion, String whitespaceStrategy, String indentStrategy, boolean addIndent) {
int availableLineWidth = -1;
// if there is no previous document region, there is no need to indent
// because we're at beginning of document
if (previousDocumentRegion == null)
return availableLineWidth;
// only indent if ignoring whitespace or if collapsing and
// there was a whitespace character before this region
boolean canIndent = false;
String previousRegionFullText = null;
String previousRegionType = null;
if ((XMLFormattingConstraints.IGNORE.equals(whitespaceStrategy)) || XMLFormattingConstraints.IGNOREANDTRIM.equals(whitespaceStrategy)) {
// if ignoring, need to check if previous region was cdata
previousRegionType = previousDocumentRegion.getType();
if (DOMRegionContext.XML_CDATA_TEXT.equals(previousRegionType))
canIndent = false;
else
canIndent = true;
}
else if (XMLFormattingConstraints.COLLAPSE.equals(whitespaceStrategy)) {
// if collapsing, need to check if previous region ended in a
// whitespace
previousRegionType = previousDocumentRegion.getType();
if (DOMRegionContext.XML_CONTENT.equals(previousRegionType)) {
previousRegionFullText = previousDocumentRegion.getFullText();
int length = previousRegionFullText.length();
if (length > 1)
canIndent = Character.isWhitespace(previousRegionFullText.charAt(length - 1));
}
}
if (canIndent) {
int indentStartOffset = currentDocumentRegion.getStartOffset();
String whitespaceRun = null;
// get previous region type if it was not previously retrieved
if (previousRegionType == null)
previousRegionType = previousDocumentRegion.getType();
// get previous region's text if it was not previously retrieved
if (previousRegionFullText == null && DOMRegionContext.XML_CONTENT.equals(previousRegionType))
previousRegionFullText = previousDocumentRegion.getFullText();
// if previous region was only whitespace, this may
// already be indented, so need to make sure
if ((previousRegionFullText != null) && (previousRegionFullText.trim().length() == 0)) {
indentStartOffset = previousDocumentRegion.getStartOffset();
whitespaceRun = previousRegionFullText;
}
if ((previousRegionFullText != null) && (whitespaceRun == null) && !getFormattingPreferences().getClearAllBlankLines()) {
whitespaceRun = getTrailingWhitespace(previousRegionFullText);
indentStartOffset = previousDocumentRegion.getEndOffset() - whitespaceRun.length();
}
int indentLevel = thisConstraints.getIndentLevel();
if (addIndent && XMLFormattingConstraints.INDENT.equals(indentStrategy)) {
++indentLevel;
thisConstraints.setIndentLevel(indentLevel);
}
// indent if not already indented
availableLineWidth = indentIfNotAlreadyIndented(textEdit, currentDocumentRegion, indentLevel, indentStartOffset, whitespaceRun);
}
return availableLineWidth;
}
/**
* Allow exactly one whitespace in currentTextRegion. If there are more,
* collapse to one. If there are none, insert one.
*
* @param textEdit
* @param currentDocumentRegion
* @param availableLineWidth
* @param currentTextRegion
*/
private void insertSpaceAndCollapse(TextEdit textEdit, IStructuredDocumentRegion currentDocumentRegion, int availableLineWidth, ITextRegion currentTextRegion) {
int textLength = currentTextRegion.getTextLength();
int regionLength = currentTextRegion.getLength();
boolean thereAreSpaces = textLength < regionLength;
int spacesStartOffset = currentDocumentRegion.getStartOffset(currentTextRegion) + textLength;
if (thereAreSpaces) {
String fullTagName = currentDocumentRegion.getFullText(currentTextRegion);
String whitespaceRun = fullTagName.substring(textLength, regionLength);
collapseSpaces(textEdit, spacesStartOffset, availableLineWidth, whitespaceRun);
}
else {
// insert a space
InsertEdit insertEdit = new InsertEdit(spacesStartOffset, SPACE);
textEdit.addChild(insertEdit);
}
}
private boolean shouldIndentBeforeAttribute(XMLFormattingConstraints constraints, ITextRegionList textRegions, int availableLineWidth, int currentTextRegionIndex, ITextRegion currentTextRegion, ITextRegion nextTextRegion) {
boolean indentAttribute = false;
// look ahead to see if going to hit max line width
// something attrName
int currentWidth = currentTextRegion.getTextLength() + nextTextRegion.getTextLength() + 1;
if (currentWidth > availableLineWidth)
indentAttribute = true;
else {
if ((currentTextRegionIndex + 2) < textRegions.size()) {
// still okay, so try next region
// something attrName=
ITextRegion textRegion = textRegions.get(currentTextRegionIndex + 2);
if (DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(textRegion.getType())) {
++currentWidth;
if (currentWidth > availableLineWidth)
indentAttribute = true;
else {
if ((currentTextRegionIndex + 3) < textRegions.size()) {
// still okay, so try next region
// something attrName=attrValue
textRegion = textRegions.get(currentTextRegionIndex + 3);
if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(textRegion.getType())) {
currentWidth = +textRegion.getTextLength();
if (currentWidth > availableLineWidth)
indentAttribute = true;
}
}
}
}
}
}
return indentAttribute;
}
/**
* Given the provided information (parentConstraints & currentDOMRegion),
* update the formatting constraints (for this & child)
*
* @param parentConstraints
* can be null
* @param thisConstraints
* can be null
* @param childConstraints
* can be null
* @param currentDOMRegion
* cannot be null
*/
protected void updateFormattingConstraints(XMLFormattingConstraints parentConstraints, XMLFormattingConstraints thisConstraints, XMLFormattingConstraints childConstraints, DOMRegion currentDOMRegion) {
IStructuredDocumentRegion currentRegion = currentDOMRegion.documentRegion;
IDOMNode currentNode = currentDOMRegion.domNode;
// default to whatever parent's constraint said to do
if (parentConstraints != null) {
if (thisConstraints != null) {
thisConstraints.copyConstraints(parentConstraints);
}
if (childConstraints != null) {
childConstraints.copyConstraints(parentConstraints);
// if whitespace strategy was only a hint, null it out so
// defaults are taken instead
if (parentConstraints.isWhitespaceStrategyAHint())
childConstraints.setWhitespaceStrategy(null);
}
}
// set up constraints for direct children of document root
Node parentNode = currentNode.getParentNode();
if (parentNode != null && parentNode.getNodeType() == Node.DOCUMENT_NODE) {
if (thisConstraints != null) {
thisConstraints.setWhitespaceStrategy(XMLFormattingConstraints.IGNORE);
thisConstraints.setIndentStrategy(XMLFormattingConstraints.NEW_LINE);
thisConstraints.setIndentLevel(0);
}
if (childConstraints != null) {
childConstraints.setWhitespaceStrategy(null);
childConstraints.setIndentStrategy(null);
childConstraints.setIndentLevel(0);
}
}
// other conditions to check when setting up child constraints
if (childConstraints != null) {
XMLFormattingPreferences preferences = getFormattingPreferences();
// if we're at document root, child tags should always just start
// on a new line and have an indent level of 0
if (currentNode.getNodeType() == Node.DOCUMENT_NODE) {
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.IGNORE);
childConstraints.setIndentStrategy(XMLFormattingConstraints.NEW_LINE);
childConstraints.setIndentLevel(0);
}
else {
// BUG108074 & BUG84688 - preserve whitespace in xsl:text &
// xsl:attribute
String nodeNamespaceURI = currentNode.getNamespaceURI();
if (XSL_NAMESPACE.equals(nodeNamespaceURI)) {
String nodeName = ((Element) currentNode).getLocalName();
if (XSL_ATTRIBUTE.equals(nodeName) || XSL_TEXT.equals(nodeName)) {
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.PRESERVE);
}
}
else {
// search within current tag for xml:space attribute
ITextRegionList textRegions = currentRegion.getRegions();
int i = 0;
boolean xmlSpaceFound = false;
boolean preserveFound = false;
while (i < textRegions.size() && !xmlSpaceFound) {
ITextRegion textRegion = textRegions.get(i);
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(textRegion.getType())) {
String regionText = currentRegion.getText(textRegion);
if (XML_SPACE.equals(regionText)) {
if ((i + 1) < textRegions.size()) {
++i;
textRegion = textRegions.get(i);
if (DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(textRegion.getType()) && ((i + 1) < textRegions.size())) {
++i;
textRegion = textRegions.get(i);
regionText = currentRegion.getText(textRegion);
if (PRESERVE.equals(regionText) || PRESERVE_QUOTED.equals(regionText)) {
preserveFound = true;
}
}
}
xmlSpaceFound = true;
}
}
++i;
}
if (xmlSpaceFound) {
if (preserveFound) {
// preserve was found so set the strategy
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.PRESERVE);
}
else {
// xml:space was found but it was not collapse, so
// use default whitespace strategy
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.DEFAULT);
}
}
else {
// how to hande nodes that have nonwhitespace text
// content
NodeList nodeList = currentNode.getChildNodes();
int length = nodeList.getLength();
int index = 0;
boolean textNodeFound = false;
// BUG214516 - If the parent constraint is to preserve whitespace, child constraints should
// still reflect the parent constraints
while (index < length && !textNodeFound && parentConstraints != null && !XMLFormattingConstraints.PRESERVE.equals(parentConstraints.getWhitespaceStrategy())) {
Node childNode = nodeList.item(index);
if (childNode.getNodeType() == Node.TEXT_NODE) {
textNodeFound = !((IDOMText) childNode).isElementContentWhitespace();
}
++index;
}
if (textNodeFound) {
if (length > 1) {
// more in here than just text, so consider
// this mixed content
childConstraints.setWhitespaceStrategy(preferences.getMixedWhitespaceStrategy());
childConstraints.setIndentStrategy(preferences.getMixedIndentStrategy());
}
else {
// there's only text
childConstraints.setWhitespaceStrategy(preferences.getTextWhitespaceStrategy());
childConstraints.setIndentStrategy(preferences.getTextIndentStrategy());
}
childConstraints.setIsWhitespaceStrategyAHint(true);
childConstraints.setIsIndentStrategyAHint(true);
}
// try referring to content model for information on
// whitespace & indent strategy
ModelQueryAdapter adapter = (ModelQueryAdapter) ((IDOMDocument) currentNode.getOwnerDocument()).getAdapterFor(ModelQueryAdapter.class);
CMElementDeclaration elementDeclaration = (CMElementDeclaration) adapter.getModelQuery().getCMNode(currentNode);
if (elementDeclaration != null) {
// follow whitespace strategy preference for
// pcdata content
int contentType = elementDeclaration.getContentType();
String facetValue = null;
if(elementDeclaration.getDataType() != null)
facetValue = (String) elementDeclaration.getDataType().getProperty(PROPERTY_WHITESPACE_FACET);
if(facetValue != null) {
if(PRESERVE.equals(facetValue))
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.PRESERVE);
// For XSD types, "collapse" corresponds to the IGNOREANDTRIM strategy
else if(COLLAPSE.equals(facetValue))
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.IGNOREANDTRIM);
else if(REPLACE.equals(facetValue))
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.REPLACE);
}
else if (contentType == CMElementDeclaration.PCDATA && parentConstraints != null && !XMLFormattingConstraints.PRESERVE.equals(parentConstraints.getWhitespaceStrategy())) {
childConstraints.setWhitespaceStrategy(preferences.getPCDataWhitespaceStrategy());
}
else if (contentType == CMElementDeclaration.ELEMENT && parentConstraints != null && !XMLFormattingConstraints.PRESERVE.equals(parentConstraints.getWhitespaceStrategy())) {
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.IGNORE);
childConstraints.setIndentStrategy(XMLFormattingConstraints.INDENT);
childConstraints.setIsWhitespaceStrategyAHint(true);
childConstraints.setIsIndentStrategyAHint(true);
}
else {
// look for xml:space in content model
CMNamedNodeMap cmAttributes = elementDeclaration.getAttributes();
// Not needed - we're looking for xml:space
//CMNamedNodeMapImpl allAttributes = new CMNamedNodeMapImpl(cmAttributes);
//List nodes = ModelQueryUtil.getModelQuery(currentNode.getOwnerDocument()).getAvailableContent((Element) currentNode, elementDeclaration, ModelQuery.INCLUDE_ATTRIBUTES);
//for (int k = 0; k < nodes.size(); k++) {
// CMNode cmnode = (CMNode) nodes.get(k);
// if (cmnode.getNodeType() == CMNode.ATTRIBUTE_DECLARATION) {
// allAttributes.put(cmnode);
// }
//}
//cmAttributes = allAttributes;
// Check implied values from the DTD way.
CMAttributeDeclaration attributeDeclaration = (CMAttributeDeclaration) cmAttributes.getNamedItem(XML_SPACE);
if (attributeDeclaration != null) {
// CMAttributeDeclaration found, check
// it
// out.
//BUG214516/196544 - Fixed NPE that was caused by an attr having
// a null attr type
String defaultValue = null;
CMDataType attrType = attributeDeclaration.getAttrType();
if (attrType != null) {
if ((attrType.getImpliedValueKind() != CMDataType.IMPLIED_VALUE_NONE) && attrType.getImpliedValue() != null)
defaultValue = attrType.getImpliedValue();
else if ((attrType.getEnumeratedValues() != null) && (attrType.getEnumeratedValues().length > 0)) {
defaultValue = attrType.getEnumeratedValues()[0];
}
}
// xml:space="preserve" means preserve
// space,
// everything else means back to
// default.
if (PRESERVE.equals(defaultValue))
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.PRESERVE);
else
childConstraints.setWhitespaceStrategy(XMLFormattingConstraints.DEFAULT);
}
// If the node has no attributes, inherit the parents whitespace strategy
else {
if (parentConstraints != null)
childConstraints.setWhitespaceStrategy(parentConstraints.getWhitespaceStrategy());
else
childConstraints.setWhitespaceStrategy(null);
}
}
}
}
}
}
// set default values according to preferences
if (childConstraints.getWhitespaceStrategy() == null) {
childConstraints.setWhitespaceStrategy(preferences.getElementWhitespaceStrategy());
}
if (childConstraints.getIndentStrategy() == null) {
childConstraints.setIndentStrategy(preferences.getElementIndentStrategy());
}
}
}
/**
* Calculates the current available line width given fullText.
*
* @param fullText
* @param availableLineWidth
* @param maxAvailableLineWidth
* @return
*/
private int updateLineWidthWithLastLine(String fullText, int availableLineWidth) {
int maxAvailableLineWidth = getFormattingPreferences().getMaxLineWidth();
int lineWidth = availableLineWidth;
if (fullText != null) {
int textLength = fullText.length();
// update available line width
// find last newline
int lastLFOffset = fullText.lastIndexOf('\n');
int lastCROffset = fullText.lastIndexOf('\r');
// all text was on 1 line
if (lastLFOffset == -1 && lastCROffset == -1) {
// just subtract text length from current
// available line width
lineWidth -= fullText.length();
}
else {
// calculate available line width of last line
int lastNewLine = Math.max(lastLFOffset, lastCROffset);
lineWidth = maxAvailableLineWidth - (textLength - lastNewLine);
}
}
return lineWidth;
}
private String getLineDelimiter(IStructuredDocumentRegion currentRegion) {
IStructuredDocument doc = currentRegion.getParentDocument();
int line = doc.getLineOfOffset(currentRegion.getStartOffset());
String lineDelimiter = doc.getLineDelimiter();
try {
if (line > 0) {
lineDelimiter = doc.getLineDelimiter(line - 1);
}
}
catch (BadLocationException e) {
// log for now, unless we find reason not to
Logger.log(Logger.INFO, e.getMessage());
}
// BUG115716: if cannot get line delimiter from current line, just
// use default line delimiter
if (lineDelimiter == null)
lineDelimiter = doc.getLineDelimiter();
return lineDelimiter;
}
private String extractLineDelimiters(String base, IStructuredDocumentRegion currentRegion) {
String lineDelimiter = getLineDelimiter(currentRegion);
StringBuffer sb = new StringBuffer();
for(int index = 0; index < base.length();) {
index = base.indexOf(lineDelimiter, index);
if(index++ >= 0)
sb.append(lineDelimiter);
else
break;
}
return sb.toString();
}
void setProgressMonitor(IProgressMonitor monitor) {
fProgressMonitor = monitor;
}
}