/*
* Copyright (C) 2007, 2009 Martin Kempf, Reto Kleeb, Michael Klenk
*
* IFS Institute for Software, HSR Rapperswil, Switzerland
* http://ifs.hsr.ch/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.eclipse.refactoring.formatter;
import groovyjarjarantlr.Token;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.codehaus.greclipse.GroovyTokenTypeBridge;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.eclipse.core.GroovyCore;
import org.codehaus.groovy.eclipse.refactoring.PreferenceConstants;
import org.codehaus.groovy.eclipse.refactoring.core.utils.astScanner.ASTScanner;
import org.codehaus.groovy.eclipse.refactoring.core.utils.astScanner.predicates.ClosuresInCodePredicate;
import org.codehaus.groovy.eclipse.refactoring.formatter.lineWrap.CorrectLineWrap;
import org.codehaus.groovy.eclipse.refactoring.formatter.lineWrap.NextLine;
import org.codehaus.groovy.eclipse.refactoring.formatter.lineWrap.SameLine;
import org.eclipse.jdt.internal.core.util.Util;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
/**
* @author Mike Klenk mklenk@hsr.ch
*/
public class GroovyBeautifier {
private static final boolean DEBUG_EDITS = false;
public DefaultGroovyFormatter formatter;
private final IFormatterPreferences preferences;
private final Set<Token> ignoreToken;
public GroovyBeautifier(DefaultGroovyFormatter defaultGroovyFormatter, IFormatterPreferences pref) {
this.formatter = defaultGroovyFormatter;
this.preferences = pref;
ignoreToken = new HashSet<Token>();
}
public TextEdit getBeautifiEdits() throws MalformedTreeException, BadLocationException {
MultiTextEdit edits = new MultiTextEdit();
combineClosures(edits);
formatLists(edits);
correctBraces(edits);
removeUnnecessarySemicolons(edits);
formatter.getTokens().dispose();
return edits;
}
private void formatLists(MultiTextEdit edits) {
ASTScanner scanner = new ASTScanner(formatter.getProgressRootNode(), new ListInCodePredicate(),
formatter.getProgressDocument());
scanner.startASTscan();
for(ASTNode _node : scanner.getMatchedNodes().keySet()) {
ListExpression node = ((ListExpression)_node);
GroovyDocumentScanner tokens = formatter.getTokens();
Token lastToken = null;
try {
lastToken = tokens.getLastNonWhitespaceTokenBefore(node.getEnd() - 1);
if (lastToken == null || lastToken.getType() == GroovyTokenTypeBridge.STRING_CTOR_START) {
//This means we are inside a GString and we won't apply edits here so skip this
continue;
}
} catch (BadLocationException e) {
Util.log(e);
continue;
}
int nodeStart = node.getStart();
int nodeEnd = node.getEnd();
int nodeLen = nodeEnd - nodeStart;
nodeLen = correctNodeLen(nodeLen, tokens.tokens, formatter.getNewLine());
boolean isLong = nodeLen > preferences.getLongListLength();
List<Expression> exps = node.getExpressions();
// GRECLIPSE-1427 if the next token is 'as', then don't add a
// newline or remove whitespace
Token maybeAs;
try {
maybeAs = tokens.getNextToken(lastToken);
} catch (BadLocationException e) {
GroovyCore.logException("Trouble getting next token", e);
maybeAs = null;
}
boolean nextTokenAs = maybeAs != null && maybeAs.getType() == GroovyTokenTypeBridge.LITERAL_as;
if (isLong || (hasClosureElement(node) && node.getExpressions().size() > 1)) {
//Split the list
for (int i = 0; i < exps.size(); i++) {
Expression exp = exps.get(i);
Token before = tokens.getLastTokenBefore(exp.getStart());
try {
while (before.getType() != GroovyTokenTypeBridge.LBRACK && before.getType() != GroovyTokenTypeBridge.COMMA) {
before = tokens.getLastTokenBefore(before);
}
replaceWhiteSpaceAfter(edits, before, formatter.getNewLine());
} catch (BadLocationException e) {
GroovyCore.logException("Trouble formatting list", e);
}
}
if (!nextTokenAs) {
replaceWhiteSpaceAfter(edits, lastToken, formatter.getNewLine());
}
}
else {
//Compact the list
for (int i = 0; i < exps.size(); i++) {
Expression exp = exps.get(i);
Token before = tokens.getLastTokenBefore(exp.getStart());
try {
while (before.getType() != GroovyTokenTypeBridge.LBRACK && before.getType() != GroovyTokenTypeBridge.COMMA) {
before = tokens.getLastTokenBefore(before);
}
replaceWhiteSpaceAfter(edits, before, before.getType() == GroovyTokenTypeBridge.LBRACK ? "" : " ");
} catch (BadLocationException e) {
Util.log(e);
}
}
if (!nextTokenAs) {
replaceWhiteSpaceAfter(edits, lastToken,
lastToken.getType() == GroovyTokenTypeBridge.SL_COMMENT ? formatter.getNewLine() : "");
}
}
}
}
/**
* Corrects node length in case new line token consists of more than one
* character.
*
* @param nodeLen original node length
* @param tokens a list of node tokens
* @param newLine
* @return corrected node length
*/
private int correctNodeLen(int nodeLen, List<Token> tokens, String newLine) {
if (newLine.length() > 1) {
for (Token token : tokens) {
if (token.getType() == GroovyTokenTypeBridge.NLS) {
nodeLen -= newLine.length() - 1;
}
}
}
return nodeLen;
}
/**
* Create an edit that replaces whitespace tokens immediately after given
* token with a String.
*/
private void replaceWhiteSpaceAfter(MultiTextEdit edits, Token token, String replaceWith) {
GroovyDocumentScanner tokens = formatter.getTokens();
try {
int editStart = tokens.getEnd(token);
Token first = tokens.getNextToken(token); // First whitespace token (if any)
Token last = first; // First non-whitespace token
// If no white space tokens where found then first and last will be
// the same token
// (i.e. token just after the given token
while (isWhiteSpace(last.getType())) {
last = tokens.getNextToken(last);
}
replaceFromTo(editStart, tokens.getOffset(last), replaceWith, edits);
} catch (BadLocationException e) {
Util.log(e);
}
}
private boolean isWhiteSpace(int type) {
return type == GroovyTokenTypeBridge.WS || type == GroovyTokenTypeBridge.NLS;
}
private boolean hasClosureElement(ListExpression node) {
List<Expression> list = node.getExpressions();
for (int i = 0; i < list.size(); i++) {
if (list.get(i) instanceof ClosureExpression)
return true;
}
return false;
}
private void combineClosures(MultiTextEdit edits) throws BadLocationException {
ASTScanner scanner = new ASTScanner(formatter.getProgressRootNode(),new ClosuresInCodePredicate(),formatter.getProgressDocument());
scanner.startASTscan();
for(ASTNode node : scanner.getMatchedNodes().keySet()) {
ClosureExpression clExp = ((ClosureExpression)node);
int posClStart = formatter.getPosOfToken(GroovyTokenTypeBridge.LCURLY, clExp.getLineNumber(), clExp.getColumnNumber(),
"{");
if (posClStart == -1) {
// Skip... invalid (likely the closure is
// inside a GString so can't find tokens in
// there.
continue;
}
int posCLEnd = formatter.getPosOfToken(GroovyTokenTypeBridge.RCURLY, clExp.getLastLineNumber(),
clExp.getLastColumnNumber() - 1, "}");
if(posCLEnd == -1) {
int positionLastTokenOfClosure = formatter.getPosOfToken(clExp.getLastLineNumber(), clExp.getLastColumnNumber());
while (formatter.getTokens().get(positionLastTokenOfClosure).getType() != GroovyTokenTypeBridge.RCURLY) {
positionLastTokenOfClosure--;
}
posCLEnd = positionLastTokenOfClosure;
}
// Ignore closure on one Line
if(clExp.getLineNumber() == clExp.getLastLineNumber()) {
ignoreToken.add(formatter.getTokens().get(posCLEnd));
continue;
}
if (clExp.getCode() instanceof BlockStatement) {
BlockStatement codeblock = (BlockStatement) clExp.getCode();
int posParamDelim = posClStart;
if(clExp.getParameters() != null && clExp.getParameters().length > 0) {
// Position Parameters on same Line
posParamDelim = formatter.getPosOfNextTokenOfType(posClStart, GroovyTokenTypeBridge.CLOSABLE_BLOCK_OP);
replaceNLSWithSpace(edits, posClStart, posParamDelim);
}
// combine closure with only one statments with less than 5 tokens to one line
if(codeblock.getStatements().size() == 1 && (posCLEnd - posClStart) < 10) {
replaceNLSWithSpace(edits, posParamDelim, posCLEnd);
ignoreToken.add(formatter.getTokens().get(posCLEnd));
} else {
// check if there is a linebreak after the parameters
if (posParamDelim > 0
&& formatter.getNextTokenIncludingNLS(posParamDelim).getType() != GroovyTokenTypeBridge.NLS) {
addEdit(new InsertEdit(formatter.getOffsetOfTokenEnd(formatter.getTokens().get(posParamDelim)), formatter
.getNewLine()), edits);
} else {
// If there are no parameters check if the first
// statement
// is on the next line
if (posParamDelim == 0
&& formatter.getNextTokenIncludingNLS(posClStart).getType() != GroovyTokenTypeBridge.NLS) {
addEdit(new InsertEdit(formatter.getOffsetOfTokenEnd(formatter.getTokens().get(posClStart)), formatter
.getNewLine()), edits);
}
}
}
}
}
}
private void replaceNLSWithSpace(MultiTextEdit container, int startPos,
int endPos) throws BadLocationException {
Token fromToken = null; // remember first NLS token in a string of NLS
// tokens
int p = startPos + 1;
while (p < endPos) {
Token token = formatter.getTokens().get(p);
int ttype = token.getType();
if (ttype == GroovyTokenTypeBridge.NLS) {
if (fromToken == null)
fromToken = token;
} else {
if (ttype == GroovyTokenTypeBridge.SL_COMMENT) {
++p; // next token will be skipped whether it is a NLS or
// not!
}
if (fromToken != null) {
// replace NLS tokens from fromToken up to current token
replaceFromTo(fromToken, token, " ", container);
fromToken = null;
}
}
++p;
}
// don't forget to replace nls tokens at the end.
if (fromToken != null) {
Token token = formatter.getTokens().get(p);
replaceFromTo(fromToken, token, " ", container);
}
}
/**
* Create an edit that replaces text from the start of fromToken to the
* start of toToken.
* The edit is added to container.
*
* @param fromToken Where to start replacing text.
* @param toToken Where to end replacing text.
* @param with The text to replace the original text with.
* @param container The container to which the textedit is to be added.
* @throws BadLocationException
*/
private void replaceFromTo(Token fromToken, Token toToken, String with, MultiTextEdit container) throws BadLocationException {
int startEdit = formatter.getOffsetOfToken(fromToken);
int endEdit = formatter.getOffsetOfToken(toToken);
addEdit(new ReplaceEdit(startEdit, endEdit - startEdit, with), container);
}
private void replaceFromTo(int startEdit, int endEdit, String with, MultiTextEdit container) {
addEdit(new ReplaceEdit(startEdit, endEdit - startEdit, with), container);
}
private void correctBraces(MultiTextEdit edits) throws BadLocationException {
CorrectLineWrap lCurlyCorrector = null;
CorrectLineWrap rCurlyCorrector = null;
if (preferences.getBracesStart() == PreferenceConstants.SAME_LINE) {
lCurlyCorrector = new SameLine(this);
} else if (preferences.getBracesStart() == PreferenceConstants.NEXT_LINE) {
lCurlyCorrector = new NextLine(this);
}
if (preferences.getBracesEnd() == PreferenceConstants.SAME_LINE) {
rCurlyCorrector = new SameLine(this);
} else if (preferences.getBracesEnd() == PreferenceConstants.NEXT_LINE) {
rCurlyCorrector = new NextLine(this);
}
assert lCurlyCorrector != null;
assert rCurlyCorrector != null;
Token token;
KlenkDocumentScanner tokens = formatter.getTokens();
assert tokens != null;
boolean skipNextNLS = false;
for (int i = 0; i < tokens.size(); i++) {
token = tokens.get(i);
if (ignoreToken.contains(token)) {
continue;
}
int tokenType = token.getType();
if (tokenType == GroovyTokenTypeBridge.LCURLY) {
if (skipNextNLS) {
skipNextNLS = false;
break;
}
// single line closures should not be reformatted like this
ClosureExpression maybeClosure = formatter.findCorrespondingClosure(token);
if (maybeClosure == null
|| maybeClosure.getLineNumber() != maybeClosure.getLastLineNumber()) {
addEdit(lCurlyCorrector.correctLineWrap(i, token), edits);
// Ensure a newline exists after the "{" token...
ASTNode node = formatter.findCorrespondingNode(token);
if (node == null || !(node instanceof ClosureExpression || node instanceof ArgumentListExpression)) {
// this rule doesn't apply for closures which have their
// own formatting logic. Note that
// ArgumentListExpression is included because when an
// argument list expression
// is returned for a "{" this means it is a "special"
// argument list without any "()" and just one closure
// in it.
Token nextToken = tokens.getNextToken(token);
if (nextToken != null) {
int type = nextToken.getType();
if (type != GroovyTokenTypeBridge.NLS && type != GroovyTokenTypeBridge.RCURLY) {
int start = tokens.getEnd(token);
int end = tokens.getOffset(nextToken);
addEdit(new ReplaceEdit(start, end - start, formatter.getNewLine()), edits);
}
}
}
}
} else if (tokenType == GroovyTokenTypeBridge.RCURLY) {
if (skipNextNLS) {
skipNextNLS = false;
} else {
Token previousToken = tokens.getLastTokenBefore(token);
// for cases like method() {} we want the braces to stay
// where they are
if (previousToken.getType() != GroovyTokenTypeBridge.LCURLY) {
addEdit(rCurlyCorrector.correctLineWrap(i, token), edits);
}
}
} else if (tokenType == GroovyTokenTypeBridge.NLS) {
// nothing
} else if (tokenType == GroovyTokenTypeBridge.SL_COMMENT) {
skipNextNLS = true;
}
}
}
private void removeUnnecessarySemicolons(MultiTextEdit edits) throws BadLocationException {
if (preferences.isRemoveUnnecessarySemicolons()) {
GroovyFormatter semicolonRemover = new SemicolonRemover(formatter.selection, formatter.document, edits);
semicolonRemover.format();
}
}
private void addEdit(TextEdit edit,TextEdit container) {
if (edit != null && edit.getOffset() >= formatter.formatOffset &&
edit.getOffset() + edit.getLength() <= formatter.formatOffset + formatter.formatLength) {
if (DEBUG_EDITS) {
// print out where this edit is taking place
try {
IDocument doc = formatter.getProgressDocument();
System.out.println(">>> edit: " + edit);
int startLine = doc.getLineOfOffset(edit.getOffset());
int endLine = doc.getLineOfOffset(edit.getOffset() + edit.getLength());
for (int line = startLine - 1; line < endLine + 1; line++) {
if (line >= 0 && line < doc.getNumberOfLines()) {
for (int i = doc.getLineOffset(line); i < doc.getLineOffset(line) + doc.getLineLength(line); i++) {
if (i == edit.getOffset())
System.out.print("|>");
if (i == edit.getOffset() + edit.getLength())
System.out.print("<|");
System.out.print(doc.getChar(i));
}
}
}
System.out.println("<<< edit: " + edit);
} catch (BadLocationException e) {
e.printStackTrace();
}
} // Debug -- end
try {
container.addChild(edit);
}
catch (MalformedTreeException e) {
//Swallow:
// This will cause later edits that conflict with earlier ones to be ignored.
// Can use this to "prioritise" edits generated by different formatting components.
// Put the formatting components you want to have priority earlier in the call sequence.
if (DEBUG_EDITS)
System.out.println("Last edit was ignored: "+e.getMessage());
}
}
}
}