/*
* Copyright 2009-2016 the original author or authors.
*
* 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 java.util.Map.Entry;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import groovyjarjarantlr.Token;
import org.codehaus.greclipse.GroovyTokenTypeBridge;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.eclipse.core.GroovyCore;
import org.codehaus.groovy.eclipse.refactoring.core.utils.ASTTools;
import org.codehaus.groovy.eclipse.refactoring.core.utils.astScanner.ASTNodeInfo;
import org.codehaus.groovy.eclipse.refactoring.core.utils.astScanner.ASTScanner;
import org.codehaus.groovy.eclipse.refactoring.core.utils.astScanner.predicates.IncludesClosureOrListPredicate;
import org.codehaus.groovy.eclipse.refactoring.core.utils.astScanner.predicates.SourceCodePredicate;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.UndoEdit;
/**
* @author Mike Klenk
*/
public class DefaultGroovyFormatter extends GroovyFormatter {
protected IFormatterPreferences pref;
private ModuleNode rootNode;
private Document formattedDocument;
private final boolean indentOnly;
public int formatOffset, formatLength;
private KlenkDocumentScanner tokens;
private int indentationLevel = 0;
/**
* Default Formatter for the Groovy-Eclipse Plugin
*
* @param sel The current selection of the Editor
* @param doc The Document which should be formatted
* @param map default Plugin preferences, or selfmade preferences
* @param indentOnly if true, the code will only be indented but not formatted
*/
public DefaultGroovyFormatter(ITextSelection sel, IDocument doc, IFormatterPreferences pref, boolean indentOnly) {
super(sel, doc);
this.pref = pref;
this.indentOnly = indentOnly;
formatOffset = selection.getOffset();
formatLength = selection.getLength();
if (formatLength > 0 || (formatOffset > 0 && indentOnly)) {
try {
// expand selection to include start of line
int startLine = document.getLineOfOffset(formatOffset);
IRegion startLineInfo = document.getLineInformation(startLine);
int endLine = document.getLineOfOffset(formatOffset + formatLength);
IRegion endLineInfo = document.getLineInformation(endLine);
formatOffset = startLineInfo.getOffset();
formatLength = endLineInfo.getOffset() + endLineInfo.getLength() - formatOffset;
} catch (BadLocationException e) {
GroovyCore.logException("Exception when calculating offsets for formatting", e);
}
} else {
formatOffset = 0;
formatLength = document.getLength();
}
}
public DefaultGroovyFormatter(IDocument doc, IFormatterPreferences prefs, int indentationLevel) {
this(new TextSelection(0, 0), doc, prefs, true);
this.indentationLevel = indentationLevel;
}
private void initCodebase() throws Exception {
GroovyCore.trace(formattedDocument.get());
tokens = new KlenkDocumentScanner(formattedDocument);
rootNode = ASTTools.getASTNodeFromSource(formattedDocument.get());
if (rootNode == null) {
// caused by unparseable file
throw new Exception("Could not format. Problem parsing Compilation unit. Fix all syntax errors and try again.");
}
}
@Override
public TextEdit format() {
formattedDocument = new Document(document.get());
try {
if (!indentOnly) {
initCodebase();
GroovyBeautifier beautifier = new GroovyBeautifier(this, pref);
int lengthBefore = formattedDocument.getLength();
beautifier.getBeautifiEdits().apply(formattedDocument);
int lengthAfter = formattedDocument.getLength();
formatLength += lengthAfter - lengthBefore;
}
initCodebase();
GroovyIndentation indent = new GroovyIndentation(this, pref, indentationLevel);
UndoEdit undo = indent.getIndentationEdits().apply(formattedDocument);
formatLength += undo.getLength();
} catch (Exception e) {
GroovyCore.logWarning("Cannot format, probably due to compilation errors. Please fix and try again.", e);
}
if (formattedDocument.get().equals(document.get())) {
return new MultiTextEdit();
}
return new ReplaceEdit(0, document.getLength(), formattedDocument.get());
}
/**
* Searches in the corresponding AST if the given Token is a multiline
* statement. Trailing linefeeds and spaces will be ignored.
*
* @param t
* Token to search for
* @return returns true if the statement has more than one line in the
* source code
*/
public boolean isMultilineStatement(Token t) {
if (t != null) {
ASTNode node = findCorrespondingNode(t);
if (isMultilineNodeType(node)) {
IncludesClosureOrListPredicate cltest = new IncludesClosureOrListPredicate(false, t.getLine());
node.visit(cltest);
if (!cltest.getContainer()) {
String text = ASTTools.getTextofNode(node, formattedDocument);
Matcher m = Pattern.compile(".*(\n|\r\n|\r).*", Pattern.DOTALL).matcher(trimEnd(text));
return m.matches();
}
}
}
return false;
}
/**
* Returns a string without spaces / line feeds at the end.
*/
public String trimEnd(String s) {
int len = s.length();
while (len > 0) {
String w = s.substring(len - 1, len);
if (w.matches("\\s")) {
len--;
} else {
break;
}
}
return s.substring(0, len);
}
/**
* Tests if the ASTNode is a valid MultiNodeType an has multiple lines
* Statements, ClassNodes, MethodNodes and Variable Expressions are ignored
*/
private boolean isMultilineNodeType(ASTNode node) {
if (node != null && node.getLineNumber() < node.getLastLineNumber()) {
if (node instanceof ExpressionStatement) {
return true;
} else if (node instanceof ReturnStatement) {
return true;
} else if (node instanceof Statement) {
return false;
} else if (node instanceof VariableExpression) {
return false;
} else if (node instanceof AnnotatedNode) {
return false;
} else {
return true;
}
}
return false;
}
/**
* Finding a AST Node corresponding to a Token ! Warning ! there can be
* found the wrong node if two nodes have the same line / col infos.
*
* A node is considered corresponding if it has the same start line and col
* as the token.
* If multiple matches are made, then the match with the longest lenght is
* returned.
*
* @param t
* Token for which the AST Node should be found.
* @return the Node with the start position of the token and the longest
* length
*/
public ASTNode findCorrespondingNode(Token t) {
ASTScanner scanner = new ASTScanner(rootNode,
new SourceCodePredicate(t.getLine(), t.getColumn()),
formattedDocument);
scanner.startASTscan();
Entry<ASTNode, ASTNodeInfo> found = null;
if (scanner.hasMatches()) {
for (Entry<ASTNode, ASTNodeInfo> e : scanner.getMatchedNodes().entrySet()) {
if (found == null || (found.getValue().getLength() < e.getValue().getLength()))
found = e;
}
}
if (found != null) {
return found.getKey();
} else {
return null;
}
}
/**
* Like {@link #findCorrespondingClosure(Token)}, but only returns
* {@link ClosureExpression}s
*
* @param t
* @return
*/
public ClosureExpression findCorrespondingClosure(Token t) {
ASTScanner scanner = new ASTScanner(rootNode, new SourceCodePredicate(t.getLine(), t.getColumn()), formattedDocument);
scanner.startASTscan();
ClosureExpression found = null;
if (scanner.hasMatches()) {
for (Entry<ASTNode, ASTNodeInfo> e : scanner.getMatchedNodes().entrySet()) {
if (e.getKey() instanceof ClosureExpression) {
found = (ClosureExpression) e.getKey();
}
}
}
return found;
}
/**
* Return a token after many () if there is no opening {
*
* @param index
* position of the current token
* @return the token after the last closing )
*/
public Token getTokenAfterParenthesis(int index) {
int i = index;
int countParenthesis = 1;
while (tokens.get(i).getType() != GroovyTokenTypeBridge.LPAREN) {
i++;
}
i++;
while (countParenthesis > 0 && i < tokens.size()-1) {
int ttype = tokens.get(i).getType();
if (ttype == GroovyTokenTypeBridge.LPAREN) {
countParenthesis++;
} else if (ttype == GroovyTokenTypeBridge.RPAREN) {
countParenthesis--;
}
i++;
}
if (tokens.get(i).getType() == GroovyTokenTypeBridge.LCURLY || i >= tokens.size()) {
return null;
}
return getNextToken(i);
}
/**
* Returns a String of spaces / tabs according to the configuration
*
* @param intentation
* the actual indentation level in 'indentation units'
* @return
*/
public String getLeadingGap(int indent) {
int spaces = indent * pref.getIndentationSize();
return GroovyIndentationService.createIndentation(pref, spaces);
}
/**
* @return Returns the default newline for this document
* @throws BadLocationException
*/
public String getNewLine() {
return formattedDocument.getDefaultLineDelimiter();
}
/**
* Returns the position of the next token in the collection of parsed tokens
*
* @param currentPos
* position of the actual cursor
* @param includingNLS
* including newline tokens
* @return returns the next position in the collection of tokens
* or the current position if it is the last token
*/
public int getPositionOfNextToken(int cPos, boolean includingNLS) {
if (cPos == tokens.size()-1) {
return cPos;
}
int currentPos = cPos;
int type;
do {
type = tokens.get(++currentPos).getType();
} while ((type == GroovyTokenTypeBridge.WS || (type == GroovyTokenTypeBridge.NLS && !includingNLS))
&& currentPos < tokens.size() - 2);
return currentPos;
}
public Token getNextToken(int currentPos) {
return tokens.get(getPositionOfNextToken(currentPos, false));
}
public Token getNextTokenIncludingNLS(int currentPos) {
return tokens.get(getPositionOfNextToken(currentPos, true));
}
/**
* Returns the position of the previous token in the collection of parsed
* tokens
*
* @param currentPos
* position of the actual cursor
* @param includingNLS
* including newline tokens
* @return returns the position in the collection of tokens
*/
public int getPositionOfPreviousToken(int cPos, boolean includingNLS) {
int currentPos = cPos;
int type;
do {
type = tokens.get(--currentPos).getType();
} while ((type == GroovyTokenTypeBridge.NLS && !includingNLS) && currentPos >= 0);
return currentPos;
}
public Token getPreviousToken(int currentPos) {
return tokens.get(getPositionOfPreviousToken(currentPos, false));
}
public Token getPreviousTokenIncludingNLS(int currentPos) {
return tokens.get(getPositionOfPreviousToken(currentPos, true));
}
/**
* Return the offset of a given token in the active document
* @param token
* @return offset of the token
* @throws BadLocationException
*/
public int getOffsetOfToken(Token token) throws BadLocationException {
return formattedDocument.getLineOffset(token.getLine() - 1) + token.getColumn() - 1;
}
/**
* Return the offset of the end of the token text in the document
* @param token
* @return the offset in the document after the last character
* @throws BadLocationException
*/
public int getOffsetOfTokenEnd(Token token) throws BadLocationException {
int offsetToken = getOffsetOfToken(token);
int offsetNextToken = getOffsetOfToken(getNextTokenIncludingNLS(getPosOfToken(token)));
String tokenWithGap = formattedDocument.get(offsetToken, offsetNextToken - offsetToken);
return offsetToken + trimEnd(tokenWithGap).length();
}
/**
* Counts the length of a Token
* @param token
* @return length of the token
* @throws BadLocationException
*/
public int getTokenLength(Token token) throws BadLocationException {
return getOffsetOfTokenEnd(token) - getOffsetOfToken(token);
}
public Vector<Vector<Token>> getLineTokens() {
return tokens.getLineTokensVector();
}
public int getPosOfToken(Token token) throws BadLocationException {
return tokens.indexOf(token);
}
public int getPosOfToken(int tokenType, int line, int column, String tokenText) {
for (int p = 0; p < tokens.size(); p++) {
Token a = tokens.get(p);
if (a.getType() == tokenType &&
a.getColumn() == column &&
a.getLine() == line &&
a.getText().equals(tokenText))
return p;
}
return -1;
}
public int getPosOfToken(int lineNumber, int columnNumber) {
for (int p = 0; p < tokens.size(); p++) {
Token a = tokens.get(p);
if (a.getColumn() == columnNumber && a.getLine() == lineNumber)
return p;
}
return -1;
}
/**
* Get the active state of the document
* @return
*/
public IDocument getProgressDocument() {
return formattedDocument;
}
public ModuleNode getProgressRootNode() {
return rootNode;
}
public KlenkDocumentScanner getTokens() {
return tokens;
}
/**
* @param indentationLevel the indentationLevel to set
*/
public void setIndentationLevel(int indentationLevel) {
this.indentationLevel = indentationLevel;
}
public int getPosOfNextTokenOfType(int pClStart, int expectedType) {
int posClStart = pClStart;
int type;
do {
type = tokens.get(++posClStart).getType();
} while (type != expectedType);
return posClStart;
}
/**
* Computes the given indent level for the line by looking at whitespace.
* before the line starts.
* <p>
* Each tab is consider to move to the next 'tabstop' at a column position
* that is a multiple of the tab size. Finally, the total number of spaces thus
* accumulated is divided by indentSize and rounded down.
*
* @return indentation level
*/
public int computeIndentLevel(String line) {
int accumulatedSpaces = 0;
int tabSize = pref.getTabSize();
for (int currPos = 0; currPos < line.length(); currPos++) {
char c = line.charAt(currPos);
if (c != ' ' && c != '\t') {
break;
} else if (c == '\t') {
accumulatedSpaces = nextTabStop(accumulatedSpaces, tabSize);
} else if (c == ' ') {
accumulatedSpaces++;
}
}
int indentSize = pref.getIndentationSize();
return accumulatedSpaces / indentSize;
}
private int nextTabStop(int spaces, int tabSize) {
int tabs = spaces / tabSize + 1;
return tabs * tabSize;
}
}