blob: 87b55c9d4c2f0cffaffa02a5d4fc3c878ac1e114 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004 John-Mason P. Shackelford and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* John-Mason P. Shackelford - initial API and implementation
* IBM Corporation - bug fixes
*******************************************************************************/
package org.eclipse.ant.internal.ui.editor.formatter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.ant.internal.ui.editor.model.AntElementNode;
import org.eclipse.ant.internal.ui.editor.model.AntProjectNode;
import org.eclipse.ant.internal.ui.editor.outline.AntModel;
import org.eclipse.ant.internal.ui.editor.templates.AntContext;
import org.eclipse.ant.internal.ui.model.AntUIPlugin;
import org.eclipse.ant.internal.ui.preferences.AntEditorPreferenceConstants;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.templates.TemplateBuffer;
import org.eclipse.jface.text.templates.TemplateVariable;
public class XmlDocumentFormatter {
private static class CommentReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<!--"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
int intChar;
char c;
StringBuffer node = new StringBuffer();
while (!complete && (intChar = reader.read()) != -1) {
c = (char) intChar;
node.append(c);
if (c == '>' && node.toString().endsWith("-->")) { //$NON-NLS-1$
complete = true;
}
}
return node.toString();
}
}
private static class DoctypeDeclarationReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<!"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
int intChar;
char c;
StringBuffer node = new StringBuffer();
while (!complete && (intChar = reader.read()) != -1) {
c = (char) intChar;
node.append(c);
if (c == '>') {
complete = true;
}
}
return node.toString();
}
}
private static class ProcessingInstructionReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<?"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
int intChar;
char c;
StringBuffer node = new StringBuffer();
while (!complete && (intChar = reader.read()) != -1) {
c = (char) intChar;
node.append(c);
if (c == '>' && node.toString().endsWith("?>")) { //$NON-NLS-1$
complete = true;
}
}
return node.toString();
}
}
private static abstract class TagReader {
protected Reader reader;
private String tagText;
protected abstract void clear();
public int getPostTagDepthModifier() {
return 0;
}
public int getPreTagDepthModifier() {
return 0;
}
abstract public String getStartOfTag();
public String getTagText() {
return this.tagText;
}
public boolean isTextNode() {
return false;
}
protected abstract String readTag() throws IOException;
public boolean requiresInitialIndent() {
return true;
}
public void setReader(Reader reader) throws IOException {
this.reader = reader;
this.clear();
this.tagText = readTag();
}
public boolean startsOnNewline() {
return true;
}
}
private static class TagReaderFactory {
// Warning: the order of the Array is important!
private static TagReader[] tagReaders = new TagReader[]{new CommentReader(),
new DoctypeDeclarationReader(),
new ProcessingInstructionReader(),
new XmlElementReader()};
private static TagReader textNodeReader = new TextReader();
public static TagReader createTagReaderFor(Reader reader)
throws IOException {
char[] buf = new char[10];
reader.mark(10);
reader.read(buf, 0, 10);
reader.reset();
String startOfTag = String.valueOf(buf);
for (int i = 0; i < tagReaders.length; i++) {
if (startOfTag.startsWith(tagReaders[i].getStartOfTag())) {
tagReaders[i].setReader(reader);
return tagReaders[i];
}
}
// else
textNodeReader.setReader(reader);
return textNodeReader;
}
}
private static class TextReader extends TagReader {
private boolean complete;
private boolean isTextNode;
protected void clear() {
this.complete = false;
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#getStartOfTag()
*/
public String getStartOfTag() {
return ""; //$NON-NLS-1$
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#isTextNode()
*/
public boolean isTextNode() {
return this.isTextNode;
}
protected String readTag() throws IOException {
StringBuffer node = new StringBuffer();
while (!complete) {
reader.mark(1);
int intChar = reader.read();
if (intChar == -1) break;
char c = (char) intChar;
if (c == '<') {
reader.reset();
complete = true;
} else {
node.append(c);
}
}
// if this text node is just whitespace
// remove it, except for the newlines.
if (node.length() < 1) {
this.isTextNode = false;
} else if (node.toString().trim().length() == 0) {
String whitespace = node.toString();
node = new StringBuffer();
for (int i = 0; i < whitespace.length(); i++) {
char whitespaceCharacter = whitespace.charAt(i);
if (whitespaceCharacter == '\n')
node.append(whitespaceCharacter);
}
this.isTextNode = false;
} else {
this.isTextNode = true;
}
return node.toString();
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#requiresInitialIndent()
*/
public boolean requiresInitialIndent() {
return false;
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#startsOnNewline()
*/
public boolean startsOnNewline() {
return false;
}
}
private static class XmlElementReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public int getPostTagDepthModifier() {
if (getTagText().endsWith("/>") || getTagText().endsWith("/ >")) { //$NON-NLS-1$ //$NON-NLS-2$
return 0;
} else if (getTagText().startsWith("</")) { //$NON-NLS-1$
return 0;
} else {
return +1;
}
}
public int getPreTagDepthModifier() {
if (getTagText().startsWith("</")) { //$NON-NLS-1$
return -1;
}
return 0;
}
public String getStartOfTag() {
return "<"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
StringBuffer node = new StringBuffer();
boolean insideQuote = false;
int intChar;
while (!complete && (intChar = reader.read()) != -1) {
char c = (char) intChar;
node.append(c);
// TODO logic incorrectly assumes that " is quote character
// when it could also be '
if (c == '"') {
insideQuote = !insideQuote;
}
if (c == '>' && !insideQuote) {
complete = true;
}
}
return node.toString();
}
}
private int depth;
private StringBuffer formattedXml;
private boolean lastNodeWasText;
public XmlDocumentFormatter() {
super();
depth= -1;
}
private void copyNode(Reader reader, StringBuffer out, FormattingPreferences prefs) throws IOException {
TagReader tag = TagReaderFactory.createTagReaderFor(reader);
depth = depth + tag.getPreTagDepthModifier();
if (!lastNodeWasText) {
if (tag.startsOnNewline() && !hasNewlineAlready(out)) {
out.append("\n"); //$NON-NLS-1$
}
if (tag.requiresInitialIndent()) {
out.append(indent(prefs.getCanonicalIndent()));
}
}
out.append(tag.getTagText());
depth = depth + tag.getPostTagDepthModifier();
lastNodeWasText = tag.isTextNode();
}
public void format(TemplateBuffer templateBuffer, AntContext antContext, FormattingPreferences prefs) {
String templateString= templateBuffer.getString();
IDocument fullDocument= antContext.getDocument();
int completionOffset= antContext.getCompletionOffset();
try {
//trim any starting whitespace
IRegion lineRegion= fullDocument.getLineInformationOfOffset(completionOffset);
String lineString= fullDocument.get(lineRegion.getOffset(), lineRegion.getLength());
lineString.trim();
fullDocument.replace(lineRegion.getOffset(), lineRegion.getLength(), lineString);
} catch (BadLocationException e1) {
return;
}
TemplateVariable[] variables= templateBuffer.getVariables();
int[] offsets= variablesToOffsets(variables, completionOffset);
IDocument origTemplateDoc= new Document(fullDocument.get());
try {
origTemplateDoc.replace(completionOffset, antContext.getCompletionLength(), templateString);
} catch (BadLocationException e) {
return; // don't format if the document has changed
}
IDocument templateDocument= createDocument(origTemplateDoc.get(), createPositions(offsets));
String leadingText= getLeadingText(fullDocument, antContext.getAntModel(), completionOffset);
String newTemplateString= leadingText + templateString;
int indent= computeIndent(leadingText, prefs.getTabWidth());
setInitialIndent(indent);
newTemplateString= format(newTemplateString, prefs);
try {
templateDocument.replace(completionOffset, templateString.length(), newTemplateString);
} catch (BadLocationException e) {
return;
}
offsetsToVariables(offsets, variables, completionOffset);
templateBuffer.setContent(newTemplateString, variables);
}
/**
* Returns the indent of the given string.
*
* @param line the text line
* @param tabWidth the width of the '\t' character.
*/
public static int computeIndent(String line, int tabWidth) {
int result= 0;
int blanks= 0;
int size= line.length();
for (int i= 0; i < size; i++) {
char c= line.charAt(i);
if (c == '\t') {
result++;
blanks= 0;
} else if (isIndentChar(c)) {
blanks++;
if (blanks == tabWidth) {
result++;
blanks= 0;
}
} else {
return result;
}
}
return result;
}
/**
* Indent char is a space char but not a line delimiters.
* <code>== Character.isWhitespace(ch) && ch != '\n' && ch != '\r'</code>
*/
public static boolean isIndentChar(char ch) {
return Character.isWhitespace(ch) && !isLineDelimiterChar(ch);
}
/**
* Line delimiter chars are '\n' and '\r'.
*/
public static boolean isLineDelimiterChar(char ch) {
return ch == '\n' || ch == '\r';
}
/**
* @return
*/
public String format(String documentText, FormattingPreferences prefs) {
Assert.isNotNull(documentText);
Assert.isNotNull(prefs);
Reader reader = new StringReader(documentText);
formattedXml = new StringBuffer();
if (depth == -1) {
depth = 0;
}
lastNodeWasText = false;
try {
while (true) {
reader.mark(1);
int intChar = reader.read();
reader.reset();
if (intChar != -1) {
copyNode(reader, formattedXml, prefs);
} else {
break;
}
}
reader.close();
} catch (IOException e) {
AntUIPlugin.log(e);
}
return formattedXml.toString();
}
private boolean hasNewlineAlready(StringBuffer out) {
return out.lastIndexOf("\n") == formattedXml.length() - 1 //$NON-NLS-1$
|| out.lastIndexOf("\r") == formattedXml.length() - 1; //$NON-NLS-1$
}
private String indent(String canonicalIndent) {
StringBuffer indent = new StringBuffer(30);
for (int i = 0; i < depth; i++) {
indent.append(canonicalIndent);
}
return indent.toString();
}
public void setInitialIndent(int indent) {
depth= indent;
}
private Position[] createPositions(int[] positions) {
Position[] p= null;
if (positions != null) {
p= new Position[positions.length];
for (int i= 0; i < positions.length; i++) {
p[i]= new Position(positions[i], 0);
}
}
return p;
}
private Document createDocument(String string, Position[] positions) throws IllegalArgumentException {
Document doc= new Document(string);
try {
if (positions != null) {
final String POS_CATEGORY= "tempAntFormatterCategory"; //$NON-NLS-1$
doc.addPositionCategory(POS_CATEGORY);
doc.addPositionUpdater(new DefaultPositionUpdater(POS_CATEGORY) {
protected boolean notDeleted() {
if (fOffset < fPosition.offset && (fPosition.offset + fPosition.length < fOffset + fLength)) {
fPosition.offset= fOffset + fLength; // deleted positions: set to end of remove
return false;
}
return true;
}
});
for (int i= 0; i < positions.length; i++) {
try {
doc.addPosition(POS_CATEGORY, positions[i]);
} catch (BadLocationException e) {
throw new IllegalArgumentException("Position outside of string. offset: " + positions[i].offset + ", length: " + positions[i].length + ", string size: " + string.length()); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
}
}
}
} catch (BadPositionCategoryException cannotHappen) {
// can not happen: category is correctly set up
}
return doc;
}
private int[] variablesToOffsets(TemplateVariable[] variables, int start) {
List list= new ArrayList();
for (int i= 0; i != variables.length; i++) {
int[] offsets= variables[i].getOffsets();
for (int j= 0; j != offsets.length; j++) {
list.add(new Integer(offsets[j]));
}
}
int[] offsets= new int[list.size()];
for (int i= 0; i != offsets.length; i++) {
offsets[i]= ((Integer) list.get(i)).intValue() + start;
}
Arrays.sort(offsets);
return offsets;
}
private void offsetsToVariables(int[] allOffsets, TemplateVariable[] variables, int start) {
int[] currentIndices= new int[variables.length];
for (int i= 0; i != currentIndices.length; i++) {
currentIndices[i]= 0;
}
int[][] offsets= new int[variables.length][];
for (int i= 0; i != variables.length; i++) {
offsets[i]= variables[i].getOffsets();
}
for (int i= 0; i != allOffsets.length; i++) {
int min= Integer.MAX_VALUE;
int minVariableIndex= -1;
for (int j= 0; j != variables.length; j++) {
int currentIndex= currentIndices[j];
// determine minimum
if (currentIndex == offsets[j].length)
continue;
int offset= offsets[j][currentIndex];
if (offset < min) {
min= offset;
minVariableIndex= j;
}
}
offsets[minVariableIndex][currentIndices[minVariableIndex]]= allOffsets[i] - start + 3;
currentIndices[minVariableIndex]++;
}
for (int i= 0; i != variables.length; i++) {
variables[i].setOffsets(offsets[i]);
}
}
/**
* Returns the indentation level at the position of code completion.
*/
private String getLeadingText(IDocument document, AntModel model, int completionOffset) {
AntProjectNode project= model.getProjectNode(false);
if (project == null) {
return ""; //$NON-NLS-1$
}
AntElementNode node= project.getNode(completionOffset);// - fAccumulatedChange);
if (node == null) {
return ""; //$NON-NLS-1$
}
StringBuffer buf= new StringBuffer();
buf.append(getLeadingWhitespace(node.getOffset(), document));
buf.append(createIndent());
return buf.toString();
}
/**
* Returns the indentation of the line at <code>offset</code> as a
* <code>StringBuffer</code>. If the offset is not valid, the empty string
* is returned.
*
* @param offset the offset in the document
* @return the indentation (leading whitespace) of the line in which
* <code>offset</code> is located
*/
public static StringBuffer getLeadingWhitespace(int offset, IDocument document) {
StringBuffer indent= new StringBuffer();
try {
IRegion line= document.getLineInformationOfOffset(offset);
int lineOffset= line.getOffset();
int nonWS= findEndOfWhiteSpace(document, lineOffset, lineOffset + line.getLength());
indent.append(document.get(lineOffset, nonWS - lineOffset));
return indent;
} catch (BadLocationException e) {
return indent;
}
}
/**
* Returns the first offset greater than <code>offset</code> and smaller than
* <code>end</code> whose character is not a space or tab character. If no such
* offset is found, <code>end</code> is returned.
*
* @param document the document to search in
* @param offset the offset at which searching start
* @param end the offset at which searching stops
* @return the offset in the specifed range whose character is not a space or tab
* @exception BadLocationException if position is an invalid range in the given document
*/
private static int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException {
while (offset < end) {
char c= document.getChar(offset);
if (c != ' ' && c != '\t') {
return offset;
}
offset++;
}
return end;
}
/**
* Creates a string that represents one indent (can be
* spaces or tabs..)
*
* @return one indentation
*/
public static StringBuffer createIndent() {
StringBuffer oneIndent= new StringBuffer();
IPreferenceStore pluginPrefs= AntUIPlugin.getDefault().getPreferenceStore();
pluginPrefs.getBoolean(AntEditorPreferenceConstants.FORMATTER_TAB_CHAR);
if (!pluginPrefs.getBoolean(AntEditorPreferenceConstants.FORMATTER_TAB_CHAR)) {
int tabLen= pluginPrefs.getInt(AntEditorPreferenceConstants.FORMATTER_TAB_SIZE);
for (int i= 0; i < tabLen; i++) {
oneIndent.append(' ');
}
} else {
oneIndent.append('\t'); // default
}
return oneIndent;
}
}