| /******************************************************************************* |
| * 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; |
| } |
| } |