/* * 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.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.codehaus.greclipse.GroovyTokenTypeBridge; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.CaseStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.ast.stmt.SwitchStatement; import org.codehaus.groovy.eclipse.core.GroovyCore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.DeleteEdit; 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 * @author kdvolder */ public class GroovyIndentation { private static boolean DEBUG = false; private void debug(String msg) { if (DEBUG) { System.out.println(msg); } } private final DefaultGroovyFormatter formatter; private final IFormatterPreferences pref; private int indentation = 0; private final int[] tempIndentation; private final LineIndentations lineInd; TextEdit indentationEdits; private final KlenkDocumentScanner tokens; public GroovyIndentation(DefaultGroovyFormatter formatter, IFormatterPreferences pref, int indentationLevel) { this.formatter = formatter; tempIndentation = new int[formatter.getProgressDocument().getNumberOfLines()]; lineInd = new LineIndentations(formatter.getProgressDocument().getNumberOfLines()); this.tokens = formatter.getTokens(); this.pref = pref; this.indentation = indentationLevel; } public TextEdit getIndentationEdits() { indentationEdits = new MultiTextEdit(); // GRECLIPSE-1478 handleMultilineMethodParameters(); try { if (formatter.isMultilineStatement(tokens.get(0))) { setAdditionalIndentation(tokens.get(0), pref.getIndentationMultiline(), false); lineInd.setMultilineToken(tokens.get(0).getLine(), tokens.get(0)); } Token token = null; for (int i = 0; i < tokens.size(); i++) { token = tokens.get(i); int offsetToken = formatter.getOffsetOfToken(token); int offsetNextToken = formatter.getOffsetOfToken(formatter .getNextTokenIncludingNLS(i)); int ttype = token.getType(); // can't use a switch here since the values are not constants if (ttype == GroovyTokenTypeBridge.LITERAL_if || ttype == GroovyTokenTypeBridge.LITERAL_while || ttype == GroovyTokenTypeBridge.LITERAL_for) { setAdditionalIndentation(formatter.getTokenAfterParenthesis(i)); } else if (ttype == GroovyTokenTypeBridge.LCURLY || ttype == GroovyTokenTypeBridge.LBRACK) { indentation++; } else if (ttype == GroovyTokenTypeBridge.LITERAL_switch) { indentendSwitchStatement(token); } else if (ttype == GroovyTokenTypeBridge.RCURLY || ttype == GroovyTokenTypeBridge.RBRACK) { indentation--; } else if (ttype == GroovyTokenTypeBridge.LITERAL_else) { int nextToken = formatter.getNextToken(i).getType(); // adding indentation when there is no opening and it is // not an "else if" construct if (nextToken != GroovyTokenTypeBridge.LCURLY && nextToken != GroovyTokenTypeBridge.LITERAL_if) { setAdditionalIndentation(formatter.getNextToken(i)); } } else if (ttype == GroovyTokenTypeBridge.EOF || ttype == GroovyTokenTypeBridge.NLS) { int nextTokenType = formatter.getNextTokenIncludingNLS(i).getType(); if (nextTokenType == GroovyTokenTypeBridge.RCURLY || nextTokenType == GroovyTokenTypeBridge.RBRACK) { tempIndentation[token.getLine()]--; } deleteWhiteSpaceBefore(token); if (ttype != GroovyTokenTypeBridge.EOF) { Token nextMultiToken = formatter.getNextTokenIncludingNLS(i); int offsetAfterNLS = offsetToken + formatter.getProgressDocument().getLineDelimiter(token.getLine() - 1).length(); if (!isEmptyLine(token.getLine()) || formatter.pref.isIndentEmptyLines()) { addEdit(new ReplaceEdit(offsetAfterNLS, (offsetNextToken - offsetAfterNLS), formatter.getLeadingGap(indentation + tempIndentation[token.getLine()]))); } lineInd.setLineIndentation(token.getLine() + 1, indentation + tempIndentation[token.getLine()]); if (formatter.isMultilineStatement(nextMultiToken)) { setAdditionalIndentation(nextMultiToken, pref.getIndentationMultiline(), false); lineInd.setMultilineToken(token.getLine(), token); } } } else if (ttype == GroovyTokenTypeBridge.ML_COMMENT) { addEdit(new ReplaceEdit(offsetToken, (offsetNextToken - offsetToken), formatMultilineComment(formatter .getProgressDocument().get(offsetToken, (offsetNextToken - offsetToken)), indentation))); } } } catch (BadLocationException e) { GroovyCore.logException("Exception thrown while determining indentation", e); } return indentationEdits; } // GRECLIPSE-1478 and GRECLIPSE-1508 add proper indentation for methods with // multiline parameters private void handleMultilineMethodParameters() { // for each real method node, add indentation for lines that contain // method parameters // that are not on the same line as the method start. ModuleNode rootNode = formatter.getProgressRootNode(); List<ClassNode> classes = rootNode.getClasses(); int indentationMultiline = pref.getIndentationMultiline(); for (ClassNode classNode : classes) { List<MethodNode> methods = classNode.getMethods(); for (MethodNode method : methods) { if (method.getEnd() > 1 && method.getParameters() != null && method.getParameters().length > 0) { Parameter[] ps = method.getParameters(); Statement code = method.getCode(); Parameter lastP = ps[ps.length - 1]; // the line start is the line that contains the opening // paren of the parameters // This is not directly in the ast, must search int maybeMethodStart = (method.getAnnotations() != null && method.getAnnotations().size() > 0) ? method .getAnnotations().get(method.getAnnotations().size() - 1).getEnd() : method.getStart(); List<Token> methodTokens = tokens.getTokens(maybeMethodStart, method.getParameters()[0].getStart()); int lineStart = method.getLineNumber(); for (int i = methodTokens.size() - 1; i >= 0; i--) { Token token = methodTokens.get(i); if (token.getType() == GroovyTokenTypeBridge.LPAREN) { lineStart = token.getLine(); break; } } int lineEnd = code != null ? code.getLineNumber() : lastP.getLastLineNumber(); if (lineStart != lineEnd) { // now determine if we need to also indent the last line // it might just be an lparen or an rcurly. In that // case, don't indent // if there is real text on it, do indent // can't compare AST nodes directly since the last // parameter will // include trailing newline if the last paren is on a // separate line // instead find the closing paren after the last param // and see if on same line int tokenIndex = tokens.findTokenFrom(lastP.getEnd()); Token lastParamToken = tokens.get(tokenIndex - 1); Token openingBracket = null; while (++tokenIndex < tokens.size()) { openingBracket = tokens.get(tokenIndex); if (openingBracket.getType() == GroovyTokenTypeBridge.LCURLY) { break; } } boolean doLastLineIndent = openingBracket != null && lastParamToken.getLine() == openingBracket.getLine(); for (int i = lineStart + 1; i < lineEnd; i++) { tempIndentation[i - 1] += indentationMultiline; } if (doLastLineIndent) { tempIndentation[lineEnd - 1] += indentationMultiline; } } } } } } /** * @param Zero-based line number in the formattedDocument * @return Whether the line in the document contains only whitespace. */ private boolean isEmptyLine(int line) { try { IDocument d = formatter.getProgressDocument(); int lineStart = d.getLineOffset(line); int lineLen = d.getLineLength(line); String lineTxt = d.get(lineStart, lineLen); boolean result = lineTxt.trim().equals(""); // debug("isEmptyLine(" + line + ") txt = '" + lineTxt + "'" + "=>" // + result); return result; } catch (BadLocationException e) { return true; // Presumably the line is outside the document so its // empty by definition } } /** * Create and add an edit to remove any whitespace (excluding newlines) * before the given token. * * @throws BadLocationException */ private void deleteWhiteSpaceBefore(Token token) throws BadLocationException { int endPos = tokens.getOffset(token); int startPos = endPos; IDocument d = formatter.getProgressDocument(); while (startPos > 0 && isTabOrSpace(d.getChar(startPos - 1))) { startPos--; } // Complication, we shouldn't do this if "indent empty lines" is true // and this is an empty line // because then the delete edit will conflict with the edit to create // the indentation. if (!formatter.pref.isIndentEmptyLines() || !isEmptyLine(formatter.getProgressDocument().getLineOfOffset(startPos))) { addEdit(new DeleteEdit(startPos, endPos - startPos)); } } private boolean isTabOrSpace(char c) { return c == ' ' || c == '\t'; } private void indentendSwitchStatement(Token token) { if (token != null) { ASTNode node = formatter.findCorrespondingNode(token); if (node instanceof SwitchStatement) { SwitchStatement switchstmt = (SwitchStatement) node; for (CaseStatement cs : switchstmt.getCaseStatements()) { indentendBlockStatement(cs.getCode(), cs.getLineNumber()); } // Hack because the default statement has wrong line infos Statement defaultstmt = switchstmt.getDefaultStatement(); int posDef = formatter.getPosOfToken(defaultstmt.getLineNumber(), defaultstmt.getColumnNumber()); if (posDef != -1) { Token def = formatter.getPreviousToken(posDef); indentendBlockStatement(switchstmt.getDefaultStatement(), def.getLine()); } } } } private void indentendBlockStatement(Statement stmt, int currentLine) { if (stmt instanceof BlockStatement) { BlockStatement defaultBlock = (BlockStatement) stmt; for (Statement sm : defaultBlock.getStatements()) { if (sm.getLineNumber() > currentLine) { for (int i = sm.getLineNumber(); i <= sm.getLastLineNumber(); i++) { tempIndentation[i - 1] += 1; } } } } } private void addEdit(TextEdit edit) { if (edit instanceof DeleteEdit && edit.getLength() == 0) { return; } if (edit instanceof ReplaceEdit && edit.getLength() == 0 && ((ReplaceEdit) edit).getText().length() < 1) { return; } if (edit instanceof InsertEdit && ((InsertEdit) edit).getText().length() < 1) { return; } if(edit != null && edit.getOffset() >= formatter.formatOffset && edit.getOffset() + edit.getLength() <= formatter.formatOffset + formatter.formatLength) { if (edit instanceof DeleteEdit) { debug("DeleteEdit: " + edit.getOffset() + ":" + edit.getLength()); debug("---------------------------"); IDocument doc = formatter.getProgressDocument(); try { debug(doc.get(0, edit.getOffset()) + "|*>" + doc.get(edit.getOffset(), edit.getLength()) + "<*|" + doc.get(edit.getOffset() + edit.getLength(), doc.getLength() - (edit.getOffset() + edit.getLength()))); } catch (BadLocationException e) { e.printStackTrace(); } debug("---------------------------"); } try { indentationEdits.addChild(edit); } catch (MalformedTreeException e) { debug("Ignored conflicting edit: " + edit); GroovyCore.logException("WARNING: Formatting ignored a conflicting text edit", e); } } } private void setAdditionalIndentation(Token token, int indent, boolean firstLineInlcuded) throws BadLocationException { if (token != null) { // Skipping (but indenting) single-line comments while (token.getType() == GroovyTokenTypeBridge.SL_COMMENT) { tempIndentation[token.getLine() - 1] += indent; token = formatter.getNextToken(formatter.getPosOfToken(token)); } if (token.getType() != GroovyTokenTypeBridge.LCURLY) { ASTNode node = formatter.findCorrespondingNode(token); if (node != null) { int lineNumber = node.getLineNumber(); if (!firstLineInlcuded) { lineNumber++; } for (; lineNumber <= node.getLastLineNumber(); lineNumber++) { if (isLastClosureArg(lineNumber - 1, node)) break; tempIndentation[lineNumber - 1] += indent; lineInd.setMultilineIndentation(lineNumber, true); } } } } } /** * Tests whether a given line (0-base index) is the start of a * "last closure" argument. * * @param line * @param node The parent node of which this might be a last closure * argument. */ private boolean isLastClosureArg(int line, ASTNode node) { try { Token token = tokens.getTokenFrom(tokens.getDocument().getLineOffset(line)); if (token == null) return false; else if ("{".equals(token.getText())) { ASTNode nestedNode = formatter.findCorrespondingNode(token); return node.getEnd() == nestedNode.getEnd(); } return false; } catch (Throwable e) { GroovyCore.logException("internal error", e); return false; } } private void setAdditionalIndentation(Token t) throws BadLocationException { setAdditionalIndentation(t, 1, true); } public LineIndentations getLineIndentations() { return lineInd; } /** * Format a multi line Comment * * @param string * the Coment to format * @param ind * the current indentation level * @return the formatted indeationed comment * @throws BadLocationException */ private String formatMultilineComment(String str, int ind) throws BadLocationException { String string = str; Matcher m = Pattern.compile("(\n|\r|\r\n)\\s*", Pattern.MULTILINE) .matcher(string); string = m.replaceAll(formatter.getNewLine() + formatter.getLeadingGap(ind) + " "); return string; } }