package org.eclipse.dltk.tcl.formatter.internal;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.dltk.compiler.util.Util;
import org.eclipse.dltk.formatter.IFormatterContext;
import org.eclipse.dltk.formatter.IFormatterDocument;
import org.eclipse.dltk.tcl.ast.AstPackage;
import org.eclipse.dltk.tcl.ast.ComplexString;
import org.eclipse.dltk.tcl.ast.Script;
import org.eclipse.dltk.tcl.ast.StringArgument;
import org.eclipse.dltk.tcl.ast.Substitution;
import org.eclipse.dltk.tcl.ast.TclArgument;
import org.eclipse.dltk.tcl.ast.TclArgumentList;
import org.eclipse.dltk.tcl.ast.TclCommand;
import org.eclipse.dltk.tcl.ast.VariableReference;
import org.eclipse.dltk.tcl.formatter.TclFormatterConstants;
import org.eclipse.dltk.tcl.parser.TclParserUtils;
import org.eclipse.dltk.tcl.parser.TclVisitor;
public class FormatterWorker extends TclVisitor {
private static final String BACKSLASH = "\\"; //$NON-NLS-1$
private static final String PROC_DIRECTIVE = "proc"; //$NON-NLS-1$
private static final String PACKAGE_DIRECTIVE = "package"; //$NON-NLS-1$
private final IFormatterDocument document;
private final IFormatterContext context;
private final TclFormatterWriter writer;
private int lastReaderPos = 0;
private String prevLine = Util.EMPTY_STRING;
private boolean lastComment = false;
private boolean wasPackageDirective = false;
/**
* @param document
* @param lineDelimiter
* @param indentGenerator
* @param wrapLength
*/
public FormatterWorker(TclFormatterWriter writer, IFormatterDocument document, IFormatterContext context) {
this.writer = writer;
this.document = document;
this.context = context;
}
private static boolean isBackSlashLine(final IFormatterDocument document, String line) {
return document.getBoolean(TclFormatterConstants.INDENT_AFTER_BACKSLASH)
&& (line.contains(BACKSLASH + "\n") || line.contains(BACKSLASH + "\r"));
}
private static List<String> splitComments(String input) {
List<String> result = new ArrayList<>();
List<String> lines = splitLines(input);
StringBuffer sb = new StringBuffer();
for (String line : lines) {
sb.append(line);
if ("".equals(line.trim())) {
result.add(sb.toString());
sb.setLength(0);
}
}
if (sb.length() > 0) {
result.add(sb.toString());
}
return result;
}
private static List<String> splitLines(String text) {
char[] input = text.toCharArray();
List<String> lines = new ArrayList<>();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < input.length; i++) {
sb.append(input[i]);
if (input[i] == '\r') {
if (i + 1 < input.length && input[i + 1] == '\n') {
continue;
} else {
lines.add(sb.toString());
sb.setLength(0);
}
}
if (input[i] == '\n') {
lines.add(sb.toString());
sb.setLength(0);
}
}
if (sb.length() > 0) {
lines.add(sb.toString());
}
return lines;
}
private static boolean isIf0Comment(TclCommand command) {
return "if".equals(command.getQualifiedName()) && command.getArguments().size() == 2
&& command.getArguments().get(0) instanceof StringArgument
&& "0".equals(((StringArgument) command.getArguments().get(0)).getValue())
&& command.getArguments().get(1) instanceof Script;
}
private static boolean isCommandName(TclArgument arg) {
return arg.eContainingFeature() == AstPackage.Literals.TCL_COMMAND__NAME;
}
/**
*
* @param commands
* @param document
* @return formatted code
*/
public void format(List<TclCommand> commands) {
TclParserUtils.traverse(commands, this);
if (lastReaderPos < document.getLength()) {
write(document.getLength());
}
}
@Override
public boolean visit(TclCommand command) {
context.setIndenting(true);
lastComment = false;
// 'package' directive handling
if (PACKAGE_DIRECTIVE.equals(command.getQualifiedName())) {
if (wasPackageDirective) {
context.setBlankLines(0);
}
wasPackageDirective = true;
} else if (wasPackageDirective) {
context.setBlankLines(document.getInt(TclFormatterConstants.LINES_FILE_AFTER_PACKAGE));
wasPackageDirective = false;
}
// 'proc' directive handling
if (PROC_DIRECTIVE.equals(command.getQualifiedName())) {
List<String> comments = splitComments(document.get(lastReaderPos, command.getStart()));
for (int i = 0; i < comments.size() - 1; i++) {
write(lastReaderPos + comments.get(i).length());
}
context.setBlankLines(document.getInt(TclFormatterConstants.LINES_FILE_BETWEEN_PROC));
} else if (isIf0Comment(command)) { // if 0 { comments }
lastComment = true;
write(command.getEnd());
return false;
}
write(command.getName().getEnd());
return true;
}
@Override
public void endVisit(TclCommand command) {
if (PROC_DIRECTIVE.equals(command.getQualifiedName())) {
context.setBlankLines(document.getInt(TclFormatterConstants.LINES_FILE_BETWEEN_PROC));
}
}
@Override
public boolean visit(StringArgument arg) {
if (isCommandName(arg)) {
return false;
}
writeString(arg.getStart(), arg.getEnd());
return false;
}
private void writeString(int start, int end) {
write(start);
if (!isBackSlashLine(document, prevLine)) {
try {
writer.ensureLineStarted(context);
} catch (Exception e) {
throw new UnexpectedFormatterException(e);
}
}
final boolean savedIndenting = context.isIndenting();
final int savedLinesPreserve = writer.getLinesPreserve();
writer.setLinesPreserve(-1);
context.setIndenting(false);
write(end);
context.setIndenting(savedIndenting);
writer.setLinesPreserve(savedLinesPreserve);
writer.ensureLineStarted(context);
}
@Override
public boolean visit(TclArgumentList list) {
context.incIndent();
return true;
}
@Override
public void endVisit(TclArgumentList list) {
context.decIndent();
}
@Override
public boolean visit(Script script) {
if (isCommandName(script)) {
return false;
}
if (script.getCommands().isEmpty()) {
if (document.getBoolean(TclFormatterConstants.INDENT_SCRIPT)) {
context.incIndent();
}
write(script.getContentEnd());
return true;
}
if (script.getContentStart() > script.getStart()) {
write(script.getContentStart());
if (document.getBoolean(TclFormatterConstants.INDENT_SCRIPT)) {
context.incIndent();
}
return true;
} else {
write(script.getEnd());
return false;
}
}
@Override
public void endVisit(Script script) {
write(script.getContentEnd());
if (document.getBoolean(TclFormatterConstants.INDENT_SCRIPT)) {
context.decIndent();
}
write(script.getEnd());
}
@Override
public boolean visit(Substitution substitution) {
if (isCommandName(substitution)) {
return false;
}
write(substitution.getStart());
return true;
}
@Override
public boolean visit(ComplexString string) {
if (isCommandName(string)) {
return false;
}
writeString(string.getStart(), string.getEnd());
return false; // don't process internals
}
@Override
public boolean visit(VariableReference var) {
if (isCommandName(var)) {
return false;
}
write(var.getStart());
write(var.getEnd());
return false; // don't process internals
}
protected void write(int endOffset) {
assert endOffset >= lastReaderPos;
if (lastReaderPos == endOffset) {
return;
}
boolean decIndent = false;
// Indentation after backslash option
if (isBackSlashLine(document, prevLine)) {
context.setIndenting(!lastComment);
context.incIndent();
decIndent = true;
}
final String text = document.get(lastReaderPos, endOffset);
final List<String> lines = splitLines(text);
for (String line : lines) {
final int end = lastReaderPos + line.length();
// Wrapping comments option
if (line.trim().startsWith("#")) {
final boolean savedWrapping = context.isWrapping();
context.setWrapping(document.getBoolean(TclFormatterConstants.WRAP_COMMENTS));
try {
writer.write(context, lastReaderPos, end);
} catch (Exception e) {
throw new UnexpectedFormatterException(e);
}
context.setWrapping(savedWrapping);
lastComment = true;
} else {
try {
writer.write(context, lastReaderPos, end);
} catch (Exception e) {
throw new UnexpectedFormatterException(e);
}
lastComment = false;
}
// Sets previous not blank line
prevLine = "".equals(line.trim()) ? prevLine + line : line;
lastReaderPos = end;
}
if (decIndent) {
context.decIndent();
}
}
}