blob: f66099137cd56bb3c6db6f0c911f812d8234e3d9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 xored software, Inc. 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:
* xored software, Inc. - initial API and Implementation (Alex Panchenko)
*******************************************************************************/
package org.eclipse.dltk.formatter;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.dltk.formatter.internal.ExcludeRegionList;
import org.eclipse.dltk.utils.TextUtils;
import org.eclipse.jface.text.IRegion;
public class FormatterWriter implements IFormatterWriter {
private final StringBuilder writer = new StringBuilder();
private final StringBuilder indent = new StringBuilder();
private final StringBuilder trimmedSpaces = new StringBuilder();
/**
* @since 2.0
*/
protected final StringBuilder callbackBuffer = new StringBuilder();
private final StringBuilder emptyLines = new StringBuilder();
private boolean lineStarted = false;
private char lastChar = 0;
private int lineNumber = 0;
private final List<IFormatterCallback> newLineCallbacks = new ArrayList<IFormatterCallback>();
private final String lineDelimiter;
private final IFormatterDocument document;
private final IFormatterIndentGenerator indentGenerator;
private int linesPreserve = -1;
private int wrapLength = -1;
private boolean preserveSpaces = true;
private boolean skipNextNewLine = false;
private boolean canAppendToPreviousLine = false;
private boolean trimTrailingSpaces = true;
private boolean trimBlankLines = true;
private boolean keepLines = false;
/**
* @param lineDelimiter
* @since 2.0
*/
public FormatterWriter(IFormatterDocument document, String lineDelimiter,
IFormatterIndentGenerator indentGenerator) {
this.document = document;
this.lineDelimiter = lineDelimiter;
this.indentGenerator = indentGenerator;
}
@Override
public void ensureLineStarted(IFormatterContext context) {
if (!lineStarted) {
startLine(context);
}
}
@Override
public void write(IFormatterContext context, int startOffset, int endOffset) {
if (!excludes.isExcluded(startOffset, endOffset)) {
if (endOffset > startOffset) {
write(context, document.get(startOffset, endOffset));
}
} else {
final IRegion[] regions = excludes.selectValidRanges(startOffset,
endOffset);
for (int i = 0; i < regions.length; ++i) {
write(context, document.get(regions[i]));
}
}
}
@Override
public void writeText(IFormatterContext context, String text) {
if (text.length() != 0) {
skipNextNewLine = false;
}
if (lineStarted) {
trimTrailingSpaces(false);
}
write(context, text);
}
private void trimTrailingSpaces(boolean keepTrimmed) {
int length = writer.length();
while (length > 0 && FormatterUtils.isSpace(writer.charAt(length - 1))) {
--length;
}
if (keepTrimmed) {
trimmedSpaces.append(writer, length, writer.length());
}
if (length < writer.length()) {
writer.setLength(length);
}
}
@Override
public void writeLineBreak(IFormatterContext context) {
if (lineStarted && !keepLines) {
write(context, lineDelimiter);
assert (!lineStarted);
skipNextNewLine = true;
}
}
@Override
public void skipNextLineBreaks(IFormatterContext context) {
skipNextLineBreaks(context, true);
}
@Override
public void skipNextLineBreaks(IFormatterContext context, boolean value) {
if (!keepLines) {
skipNextNewLine = value;
}
}
@Override
public void appendToPreviousLine(IFormatterContext context, String text) {
if (!lineStarted && canAppendToPreviousLine) {
skipNextNewLine = false;
emptyLines.setLength(0);
indent.setLength(0);
int len = writer.length();
if (len > 0) {
if (writer.charAt(len - 1) == '\n') {
--len;
if (len > 0 && writer.charAt(len - 1) == '\r') {
--len;
}
} else if (writer.charAt(len - 1) == '\r') {
--len;
}
writer.setLength(len);
if (text.length() == 0) {
writer.append(trimmedSpaces);
trimmedSpaces.setLength(0);
} else {
writer.append(text);
}
lineStarted = true;
}
}
}
@Override
public void disableAppendToPreviousLine() {
canAppendToPreviousLine = false;
}
protected void write(IFormatterContext context, String text) {
if (!context.isWrapping()) {
for (int i = 0; i < text.length(); ++i) {
write(context, text.charAt(i));
}
} else {
int offset;
int start = findLineStart();
if (lineStarted) {
offset = calculateOffset(start);
} else {
offset = 0;
}
int savedLineNumber = lineNumber;
for (int i = 0; i < text.length(); ++i) {
final char ch = text.charAt(i);
if (lineStarted && !FormatterUtils.isSpace(ch)
&& !FormatterUtils.isLineSeparator(ch)) {
if (savedLineNumber != lineNumber) {
start = findLineStart();
offset = calculateOffset(start);
savedLineNumber = lineNumber;
}
if (wrapLength > 0 && offset > wrapLength) {
int begin = start;
while (begin < writer.length()
&& FormatterUtils.isSpace(writer.charAt(begin))) {
++begin;
}
if (begin < writer.length()
&& writer.charAt(begin) == '#') {
++begin;
}
while (begin < writer.length()
&& FormatterUtils.isSpace(writer.charAt(begin))) {
++begin;
}
int wordBegin = writer.length();
while (wordBegin > begin
&& !FormatterUtils.isSpace(writer
.charAt(wordBegin - 1))) {
--wordBegin;
}
int prevWordEnd = wordBegin;
while (prevWordEnd > begin
&& FormatterUtils.isSpace(writer
.charAt(prevWordEnd - 1))) {
--prevWordEnd;
}
if (prevWordEnd > begin) {
writer.replace(prevWordEnd, wordBegin,
lineDelimiter + "# "); //$NON-NLS-1$
start = prevWordEnd + lineDelimiter.length();
offset = calculateOffset(start);
}
}
}
write(context, ch);
++offset;
}
}
}
private int calculateOffset(int pos) {
int offset = 0;
while (pos < writer.length()) {
char ch = writer.charAt(pos++);
if (ch == '\t') {
final int tabSize = indentGenerator.getTabSize();
offset = (offset + tabSize - 1) / tabSize * tabSize;
} else {
++offset;
}
}
return offset;
}
private int findLineStart() {
int pos = writer.length();
while (pos > 0
&& !FormatterUtils.isLineSeparator(writer.charAt(pos - 1))) {
--pos;
}
return pos;
}
/**
* @param context
* @param charAt
*/
protected void write(IFormatterContext context, char ch) {
if (ch == '\n' || ch == '\r') {
if (lineStarted) {
trimmedSpaces.setLength(0);
if (trimTrailingSpaces) {
trimTrailingSpaces(true);
}
writer.append(ch);
lineStarted = false;
if (!newLineCallbacks.isEmpty()) {
executeNewLineCallbacks(context);
assert newLineCallbacks.isEmpty();
}
} else if (ch == '\n' && lastChar == '\r') {
if (emptyLines.length() == 0) {
writer.append(ch); // windows EOL = "\r\n"
} else {
emptyLines.append(ch);
}
} else {
if (!trimBlankLines) {
emptyLines.append(indent);
}
indent.setLength(0);
emptyLines.append(ch);
}
} else if (!lineStarted) {
if (Character.isWhitespace(ch)) {
indent.append(ch);
} else {
startLine(context);
writer.append(ch);
}
} else {
if (!preserveSpaces && context.isIndenting()
&& !context.isComment() && FormatterUtils.isSpace(ch)) {
if (writer.charAt(writer.length() - 1) != ' ') {
writer.append(' ');
}
} else {
writer.append(ch);
}
}
lastChar = ch;
}
private void executeNewLineCallbacks(IFormatterContext context) {
final IFormatterRawWriter callbackWriter = new IFormatterRawWriter() {
@Override
public void writeIndent(IFormatterContext context) {
FormatterWriter.this.writeIndent(context, callbackBuffer);
}
@Override
public void writeText(IFormatterContext context, String text) {
callbackBuffer.append(text);
}
};
IFormatterCallback[] copy = newLineCallbacks
.toArray(new IFormatterCallback[newLineCallbacks.size()]);
newLineCallbacks.clear();
for (IFormatterCallback callback : copy) {
callback.call(context, callbackWriter);
}
}
private void startLine(IFormatterContext context) {
if (callbackBuffer.length() != 0) {
writer.append(callbackBuffer);
callbackBuffer.setLength(0);
}
if (context.getBlankLines() >= 0) {
if (writer.length() != 0) {
for (int i = 0; i < context.getBlankLines(); ++i) {
writer.append(lineDelimiter);
}
}
context.resetBlankLines();
} else if (emptyLines.length() != 0) {
writeEmptyLines();
}
skipNextNewLine = false;
emptyLines.setLength(0);
if (context.isIndenting()) {
writeIndent(context);
} else {
writer.append(indent);
}
indent.setLength(0);
lineStarted = true;
++lineNumber;
canAppendToPreviousLine = true;
}
private void writeEmptyLines() {
if (skipNextNewLine) {
int i = 0;
if (emptyLines.charAt(i) == '\r') {
++i;
if (i < emptyLines.length() && emptyLines.charAt(i) == '\n') {
++i;
}
} else if (emptyLines.charAt(i) == '\n') {
++i;
}
if (i > 0) {
emptyLines.delete(0, i);
}
}
if (linesPreserve >= 0 && linesPreserve < Integer.MAX_VALUE
&& TextUtils.countLines(emptyLines) > linesPreserve) {
writer.append(TextUtils.selectHeadLines(emptyLines, linesPreserve));
} else {
writer.append(emptyLines);
}
}
/**
* @param context
*/
protected void writeIndent(IFormatterContext context) {
writeIndent(context, writer);
}
/**
* @since 2.0
*/
protected void writeIndent(IFormatterContext context, StringBuilder buffer) {
indentGenerator.generateIndent(context.getIndent(), buffer);
}
public String getOutput() {
return writer.toString();
}
private final ExcludeRegionList excludes = new ExcludeRegionList();
@Override
public void excludeRegion(IRegion region) {
excludes.excludeRegion(region);
}
@Override
public void addNewLineCallback(IFormatterCallback callback) {
newLineCallbacks.add(callback);
}
public void flush(IFormatterContext context) {
if (!newLineCallbacks.isEmpty()) {
if (lineStarted) {
writer.append(lineDelimiter);
lineStarted = false;
}
executeNewLineCallbacks(context);
assert newLineCallbacks.isEmpty();
}
if (callbackBuffer.length() != 0) {
writer.append(callbackBuffer);
callbackBuffer.setLength(0);
}
if (emptyLines.length() != 0) {
writeEmptyLines();
emptyLines.setLength(0);
}
}
/**
* @since 2.0
*/
public int getLinesPreserve() {
return linesPreserve;
}
/**
* @param value
*/
public void setLinesPreserve(int value) {
this.linesPreserve = value;
}
/**
* @return the wrapLength
*/
public int getWrapLength() {
return wrapLength;
}
/**
* @param wrapLength
* the wrapLength to set
*/
public void setWrapLength(int wrapLength) {
this.wrapLength = wrapLength;
}
public boolean isPreserveSpaces() {
return preserveSpaces;
}
public void setPreserveSpaces(boolean preserveSpaces) {
this.preserveSpaces = preserveSpaces;
}
/**
* @since 2.0
*/
public boolean isTrimTrailingSpaces() {
return trimTrailingSpaces;
}
/**
* @since 2.0
*/
public void setTrimTrailingSpaces(boolean trimTrailingSpaces) {
this.trimTrailingSpaces = trimTrailingSpaces;
}
/**
* @since 2.0
*/
public boolean isTrimEmptyLines() {
return trimBlankLines;
}
/**
* @since 2.0
*/
public void setTrimEmptyLines(boolean trimEmptyLines) {
this.trimBlankLines = trimEmptyLines;
}
/**
* @param keepLines
* the keepLines to set
* @since 2.0
*/
public void setKeepLines(boolean keepLines) {
this.keepLines = keepLines;
}
/**
* @return the keepLines
* @since 2.0
*/
public boolean isKeepLines() {
return keepLines;
}
}