/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.parser.prettyprinterv2; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.Map.Entry; import org.python.pydev.core.IGrammarVersionProvider; import org.python.pydev.core.IIndentPrefs; import org.python.pydev.core.MisconfigurationException; import org.python.pydev.core.log.Log; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.ast.argumentsType; import org.python.pydev.parser.jython.ast.commentType; import com.aptana.shared_core.structure.Tuple; /** * The initial pretty printer approach consisted of going to a scope and then printing things * in that scope as it walked the structure, but this approach doesn't seem to work well * because of comments, as it depends too much on how the parsing was done and the comments * found (and javacc just spits them out and the parser tries to put them in good places, but * this is often not what happens) * * So, a different approach will be tested: * Instead of doing everything in a single pass, we'll traverse the structure once to create * a new (flat) structure, in a 2nd step that structure will be filled with comments and in * a final step, that intermediary structure will be actually written. * * This will also enable the parsing to be simpler (and faster) as it'll not have to move comments * around to try to find a suitable position. */ public class PrettyPrinterV2 { private IPrettyPrinterPrefs prefs; private final int LEVEL_PARENS = 0; //() private final int LEVEL_BRACKETS = 1; //[] private final int LEVEL_BRACES = 2; //{} public static PrettyPrinterPrefsV2 createDefaultPrefs(IGrammarVersionProvider versionProvider, IIndentPrefs indentPrefs, String endLineDelim) { if (versionProvider == null) { versionProvider = new IGrammarVersionProvider() { public int getGrammarVersion() throws MisconfigurationException { return IGrammarVersionProvider.LATEST_GRAMMAR_VERSION; } }; } PrettyPrinterPrefsV2 prettyPrinterPrefs = new PrettyPrinterPrefsV2(endLineDelim, indentPrefs.getIndentationString(), versionProvider); prettyPrinterPrefs.setSpacesAfterComma(1); prettyPrinterPrefs.setSpacesBeforeComment(1); prettyPrinterPrefs.setLinesAfterMethod(1); prettyPrinterPrefs.setLinesAfterClass(2); prettyPrinterPrefs.setLinesAfterSuite(1); return prettyPrinterPrefs; } public PrettyPrinterV2(IPrettyPrinterPrefs prefs) { this.prefs = prefs; } public static String printArguments(IGrammarVersionProvider versionProvider, argumentsType args) { String newLine = "\n"; String indent = " "; PrettyPrinterPrefsV2 prefsV2 = new PrettyPrinterPrefsV2(newLine, indent, versionProvider); prefsV2.setSpacesAfterComma(1); PrettyPrinterV2 printerV2 = new PrettyPrinterV2(prefsV2); SimpleNode newArgs = args.createCopy(); String result = ""; try { result = printerV2.print(newArgs); } catch (Exception e) { Log.log(e); } while (result.endsWith("\n") || result.endsWith("\r")) { result = result.substring(0, result.length() - 1); } return result; } //Used while parsing for (maintained across lines) private final int[] LEVELS = new int[] { 0, 0, 0 }; private int statementLevel = 0; WriterEraserV2 writerEraserV2; WriteStateV2 writeStateV2; Set<Entry<Integer, PrettyPrinterDocLineEntry>> entrySet; List<Tuple<PrettyPrinterDocLineEntry, String>> previousLines; List<LinePartRequireMark> requireMarks = new ArrayList<LinePartRequireMark>(); //Restarted for each line boolean lastWasComment; boolean writtenComment; boolean savedLineIndent; int indentDiff; /** * This is the method that manages to call everything else correctly to print the ast. */ public String print(SimpleNode ast) throws IOException { PrettyPrinterDocV2 doc = new PrettyPrinterDocV2(); PrettyPrinterVisitorV2 visitor = new PrettyPrinterVisitorV2(prefs, doc); if (ast instanceof argumentsType) { visitor.pushTupleNeedsParens(); } try { visitor.visitNode(ast); } catch (Exception e) { Log.log(e); return ""; } writerEraserV2 = new WriterEraserV2(); writeStateV2 = new WriteStateV2(writerEraserV2, prefs); //Now that the doc is filled, let's make a string from it. entrySet = doc.linesToColAndContents.entrySet(); previousLines = new ArrayList<Tuple<PrettyPrinterDocLineEntry, String>>(); doc.validateRequireMarks(); List<Tuple<ILinePart, PrettyPrinterDocLineEntry>> commentsSkipped = new ArrayList<Tuple<ILinePart, PrettyPrinterDocLineEntry>>(); for (Entry<Integer, PrettyPrinterDocLineEntry> entry : entrySet) { PrettyPrinterDocLineEntry line = entry.getValue(); List<ILinePart> sortedParts = line.getSortedParts(); indentDiff = line.getIndentDiff(); savedLineIndent = false; List<ILinePart2> sortedPartsWithILinePart2 = getLineParts2(sortedParts); lastWasComment = false; writtenComment = false; if (sortedParts.size() == 0) { continue; } if (sortedPartsWithILinePart2.size() == 1) { //Ok, we need a special treatment for lines that only contain comments. //As it doesn't belong in the actual AST (it's just spit out in the middle of the parsing), //it can happen that it doesn't belong in the current indentation (and rather to the last indentation //found), so, we have to go on and check how we should indent it based on the previous line(s) ILinePart linePart = sortedPartsWithILinePart2.get(0); if (linePart.getToken() instanceof commentType && linePart instanceof ILinePart2) { String indentWritten = handleSingleLineComment((ILinePart2) linePart, line, commentsSkipped); if (indentWritten != null) { saveLineIndent(line, indentWritten); } } } for (ILinePart linePart : sortedParts) { writeLinePart(linePart, commentsSkipped, line); } if (!savedLineIndent) { saveLineIndent(line); } if (statementLevel != 0 && !lastWasComment) { if (!isInLevel()) { continue;//don't write the new line if in a statement and not within parenthesis. } } writeStateV2.writeNewLine(); int newLinesRequired = line.getNewLinesRequired(); if (newLinesRequired != 0) { for (int i = 0; i < newLinesRequired; i++) { writeStateV2.writeNewLine(); } } } return writerEraserV2.getBuffer().toString(); } private void saveLineIndent(PrettyPrinterDocLineEntry line) { savedLineIndent = true; previousLines.add(new Tuple<PrettyPrinterDocLineEntry, String>(line, writeStateV2.getIndentString())); } private void saveLineIndent(PrettyPrinterDocLineEntry line, String indentWritten) { savedLineIndent = true; previousLines.add(new Tuple<PrettyPrinterDocLineEntry, String>(line, indentWritten)); } private void writeLinePart(ILinePart linePart, List<Tuple<ILinePart, PrettyPrinterDocLineEntry>> commentsSkipped, PrettyPrinterDocLineEntry line) throws IOException { boolean isSlash = false; if (linePart instanceof ILinePart2 && !writtenComment) { String tok = ((ILinePart2) linePart).getString(); if (tok.charAt(0) == ';') { writeStateV2.writeNewLine(); savedLineIndent = true; //don't save line indent return; } else if (tok.charAt(0) == '\\') { if (isInLevel()) { savedLineIndent = true; //don't save line indent return; } isSlash = true; } else if (tok.charAt(0) == '@') { writeStateV2.requireNextNewLine(); } if (linePart.getToken() instanceof commentType) { if (statementLevel > 0 && !isInLevel()) { commentsSkipped.add(new Tuple<ILinePart, PrettyPrinterDocLineEntry>(linePart, line)); savedLineIndent = true; //don't save line indent return; } writeStateV2.writeSpacesBeforeComment(); } boolean written = false; //Note: on a write, if the last thing was a new line, it'll indent. if (tok.length() == 1) { Tuple<Integer, Boolean> newLevel = updateLevels(tok); if (newLevel != null) { if (!savedLineIndent) { saveLineIndent(line); } if (newLevel.o2) { writeStateV2.write(prefs.getReplacement(tok)); writeStateV2.indent(); written = true; } else { if (indentDiff == 0) { writeStateV2.dedent(); } writeStateV2.write(prefs.getReplacement(tok)); if (indentDiff != 0) { writeStateV2.dedent(); } written = true; } } } if (!written) { written = true; writeStateV2.write(prefs.getReplacement(tok)); } if (isSlash) { writeStateV2.writeNewLine(); } if (linePart.getToken() instanceof commentType) { writeStateV2.requireNextNewLine(); lastWasComment = true; } else { lastWasComment = false; } } else if (linePart instanceof ILinePartIndentMark) { ILinePartIndentMark indentMark = (ILinePartIndentMark) linePart; if (!savedLineIndent) { saveLineIndent(line); } if (indentMark.isIndent()) { if (indentMark.getRequireNewLineOnIndent()) { writeStateV2.requireNextNewLineOrComment(); } writeStateV2.indent(); indentDiff--; } else { writeStateV2.dedent(); indentDiff++; } } else if (linePart instanceof ILinePartStatementMark) { ILinePartStatementMark statementMark = (ILinePartStatementMark) linePart; if (statementMark.isStart()) { if (statementLevel == 0) { writeStateV2.requireNextNewLineOrComment(); } statementLevel++; } else { statementLevel--; } } if ((statementLevel == 0 || isInLevel()) && commentsSkipped != null && commentsSkipped.size() > 0) { savedLineIndent = true; //We don't want to save line indents at this point. for (Tuple<ILinePart, PrettyPrinterDocLineEntry> tup : commentsSkipped) { writeLinePart(tup.o1, null, tup.o2); } commentsSkipped.clear(); } } /** * @return all the line parts that implement ILinePart2 */ private List<ILinePart2> getLineParts2(List<ILinePart> sortedParts) { List<ILinePart2> sortedPartsWithILinePart2 = new ArrayList<ILinePart2>(); for (ILinePart p : sortedParts) { if (p instanceof ILinePart2) { sortedPartsWithILinePart2.add((ILinePart2) p); } } return sortedPartsWithILinePart2; } /** * Handles a single line comment, putting it in the correct indentation. * @param line * @param commentsSkipped * @return the indent used or null if it wasn't written. */ private String handleSingleLineComment(ILinePart2 linePart, PrettyPrinterDocLineEntry line, List<Tuple<ILinePart, PrettyPrinterDocLineEntry>> commentsSkipped) throws IOException { String indent = null; if (statementLevel > 0 && !isInLevel()) { commentsSkipped.add(new Tuple<ILinePart, PrettyPrinterDocLineEntry>(linePart, line)); savedLineIndent = true; //don't save line indent writtenComment = true; //make it as if we've written it return indent; } ILinePart2 iLinePart2 = (ILinePart2) linePart; commentType commentType = (commentType) linePart.getToken(); int col = commentType.beginColumn; if (col == 1) { //yes, our indexing starts at 1. lastWasComment = true; writtenComment = true; writeStateV2.writeRaw(iLinePart2.getString()); indent = ""; } else { Tuple<PrettyPrinterDocLineEntry, String> found = null; //Let's go backward in the lines to see one that matches the current indentation. ListIterator<Tuple<PrettyPrinterDocLineEntry, String>> it = previousLines .listIterator(previousLines.size()); while (it.hasPrevious() && found == null) { Tuple<PrettyPrinterDocLineEntry, String> previous = it.previous(); int firstCol = previous.o1.getFirstCol(); if (firstCol != -1) { if (firstCol == col) { found = previous; } } } if (found != null) { lastWasComment = true; writtenComment = true; writeStateV2.writeRaw(found.o2); writeStateV2.writeRaw(iLinePart2.getString()); indent = found.o2; } } return indent; } /** * @return true if we're within parenthesis, brackets or braces */ private boolean isInLevel() { for (int i = 0; i < 3; i++) { if (this.LEVELS[i] != 0) { return true; } } return false; } /** * Updates the level for parenthesis, brackets and braces based on the passed token and returns the new level and whether * it was increased (or null if nothing happened). */ private Tuple<Integer, Boolean> updateLevels(String tok) { int use = -1; boolean increaseLevel = true; switch (tok.charAt(0)) { case '(': case ')': use = this.LEVEL_PARENS; break; case '[': case ']': use = this.LEVEL_BRACKETS; break; case '{': case '}': use = this.LEVEL_BRACES; break; } ; if (use != -1) { switch (tok.charAt(0)) { case ']': case ')': case '}': increaseLevel = false; } ; if (increaseLevel) { this.LEVELS[use]++; } else { this.LEVELS[use]--; } return new Tuple<Integer, Boolean>(LEVELS[use], increaseLevel); } else { return null; } } @Override public String toString() { return "PrettyPrinterV2[\n" + this.writeStateV2 + "\n]"; } }