/** * 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. */ /* * Created on Jul 19, 2004 * * @author Fabio Zadrozny */ package org.python.pydev.editor.codefolding; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel; import org.eclipse.ui.IPropertyListener; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.docutils.PySelection.DocIterator; import org.python.pydev.core.log.Log; import org.python.pydev.core.performanceeval.OptimizationRelatedConstants; import org.python.pydev.editor.PyEdit; import org.python.pydev.editor.model.IModelListener; import org.python.pydev.parser.ErrorDescription; import org.python.pydev.parser.jython.ISpecialStr; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.ast.ClassDef; import org.python.pydev.parser.jython.ast.For; import org.python.pydev.parser.jython.ast.FunctionDef; import org.python.pydev.parser.jython.ast.If; import org.python.pydev.parser.jython.ast.Import; import org.python.pydev.parser.jython.ast.ImportFrom; import org.python.pydev.parser.jython.ast.Str; import org.python.pydev.parser.jython.ast.TryExcept; import org.python.pydev.parser.jython.ast.TryFinally; import org.python.pydev.parser.jython.ast.While; import org.python.pydev.parser.jython.ast.With; import org.python.pydev.parser.jython.ast.commentType; import org.python.pydev.parser.jython.ast.excepthandlerType; import org.python.pydev.parser.jython.ast.suiteType; import org.python.pydev.parser.visitors.scope.ASTEntry; import org.python.pydev.parser.visitors.scope.ASTEntryWithChildren; import org.python.pydev.parser.visitors.scope.CodeFoldingVisitor; import org.python.pydev.plugin.PydevPlugin; import org.python.pydev.plugin.preferences.PydevPrefs; import com.aptana.shared_core.structure.Tuple; /** * @author Fabio Zadrozny * * This class is used to set the code folding markers. * * Changed 15/09/07 to include more folding elements */ public class CodeFoldingSetter implements IModelListener, IPropertyListener { private PyEdit editor; public CodeFoldingSetter(PyEdit editor) { this.editor = editor; } /* * (non-Javadoc) * * @see org.python.pydev.editor.model.IModelListener#modelChanged(org.python.pydev.editor.model.AbstractNode) */ public synchronized void modelChanged(final SimpleNode root2) { ProjectionAnnotationModel model = (ProjectionAnnotationModel) editor .getAdapter(ProjectionAnnotationModel.class); if (model == null) { //we have to get the model to do it... so, start a thread and try until get it... //this had to be done because sometimes we get here and we still are unable to get the //projection annotation model. (there should be a better way, but this solves it... //even if it looks like a hack...) Thread t = new Thread() { public void run() { ProjectionAnnotationModel modelT = null; for (int i = 0; i < 10 && modelT == null; i++) { //we will try it for 10 secs... try { sleep(100); } catch (InterruptedException e) { Log.log(e); } modelT = (ProjectionAnnotationModel) editor.getAdapter(ProjectionAnnotationModel.class); if (modelT != null) { addMarksToModel(root2, modelT); break; } } } }; t.setPriority(Thread.MIN_PRIORITY); t.setName("CodeFolding - get annotation model"); t.start(); } else { addMarksToModel(root2, model); } } /** * Given the ast, create the needed marks and set them in the passed model. */ @SuppressWarnings("unchecked") private synchronized void addMarksToModel(SimpleNode root2, ProjectionAnnotationModel model) { try { if (model != null) { ArrayList<PyProjectionAnnotation> existing = new ArrayList<PyProjectionAnnotation>(); //get the existing annotations Iterator<PyProjectionAnnotation> iter = model.getAnnotationIterator(); while (iter != null && iter.hasNext()) { PyProjectionAnnotation element = iter.next(); existing.add(element); } //now, remove the annotations not used and add the new ones needed IDocument doc = editor.getDocument(); if (doc != null) { //this can happen if we change the input of the editor very quickly. List<FoldingEntry> marks = getMarks(doc, root2); Map<ProjectionAnnotation, Position> annotationsToAdd; if (marks.size() > OptimizationRelatedConstants.MAXIMUM_NUMBER_OF_CODE_FOLDING_MARKS) { annotationsToAdd = new HashMap<ProjectionAnnotation, Position>(); } else { annotationsToAdd = getAnnotationsToAdd(marks, model, existing); } model.replaceAnnotations(existing.toArray(new Annotation[existing.size()]), annotationsToAdd); } } } catch (Exception e) { Log.log(e); } } /** * To add a mark, we have to do the following: * * Get the current node to add and find the next that is on the same indentation or on an indentation that is lower * than the current (this will mark the end of the selection). * * If we don't find that, the end of the selection is the end of the file. */ private Map<ProjectionAnnotation, Position> getAnnotationsToAdd(List<FoldingEntry> nodes, ProjectionAnnotationModel model, List<PyProjectionAnnotation> existing) { Map<ProjectionAnnotation, Position> annotationsToAdd = new HashMap<ProjectionAnnotation, Position>(); try { for (FoldingEntry element : nodes) { if (element.startLine < element.endLine - 1) { Tuple<ProjectionAnnotation, Position> tup = getAnnotationToAdd(element, element.startLine, element.endLine, model, existing); if (tup != null) { annotationsToAdd.put(tup.o1, tup.o2); } } } } catch (BadLocationException e) { } catch (NullPointerException e) { } return annotationsToAdd; } /** * @return an annotation that should be added (or null if that entry already has an annotation * added for it). */ private Tuple<ProjectionAnnotation, Position> getAnnotationToAdd(FoldingEntry node, int start, int end, ProjectionAnnotationModel model, List<PyProjectionAnnotation> existing) throws BadLocationException { try { IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); int offset = document.getLineOffset(start); int endOffset = offset; try { endOffset = document.getLineOffset(end); } catch (Exception e) { //sometimes when we are at the last line, the command above will not work very well IRegion lineInformation = document.getLineInformation(end); endOffset = lineInformation.getOffset() + lineInformation.getLength(); } Position position = new Position(offset, endOffset - offset); return getAnnotationToAdd(position, node, model, existing); } catch (BadLocationException x) { //this could happen } return null; } /** * We have to be careful not to remove existing annotations because if this happens, previous code folding is not correct. */ private Tuple<ProjectionAnnotation, Position> getAnnotationToAdd(Position position, FoldingEntry node, ProjectionAnnotationModel model, List<PyProjectionAnnotation> existing) { for (Iterator<PyProjectionAnnotation> iter = existing.iterator(); iter.hasNext();) { PyProjectionAnnotation element = iter.next(); Position existingPosition = model.getPosition(element); if (existingPosition.equals(position)) { //ok, do nothing to this annotation (neither remove nor add, as it already exists in the correct place). existing.remove(element); return null; } } return new Tuple<ProjectionAnnotation, Position>(new PyProjectionAnnotation(node.getAstEntry()), position); } /* * (non-Javadoc) * * @see org.eclipse.ui.IPropertyListener#propertyChanged(java.lang.Object, int) */ public void propertyChanged(Object source, int propId) { if (propId == PyEditProjection.PROP_FOLDING_CHANGED) { modelChanged(editor.getAST()); } } /** * To get the marks, we work a little with the ast and a little with the doc... the ast is good to give us all things but the comments, * and the doc will give us the comments. * * @return a list of entries, ordered by their appearance in the document. * * Also, there should be no overlap for any of the entries */ public static List<FoldingEntry> getMarks(IDocument doc, SimpleNode ast) { List<FoldingEntry> ret = new ArrayList<FoldingEntry>(); CodeFoldingVisitor visitor = CodeFoldingVisitor.create(ast); //(re) insert annotations. List<Class> elementList = new ArrayList<Class>(); IPreferenceStore prefs = getPreferences(); if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_IMPORTS)) { elementList.add(Import.class); elementList.add(ImportFrom.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_CLASSDEF)) { elementList.add(ClassDef.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_FUNCTIONDEF)) { elementList.add(FunctionDef.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_STRINGS)) { elementList.add(Str.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_WHILE)) { elementList.add(While.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_IF)) { elementList.add(If.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_FOR)) { elementList.add(For.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_WITH)) { elementList.add(With.class); } if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_TRY)) { elementList.add(TryExcept.class); elementList.add(TryFinally.class); } List<ASTEntry> nodes = visitor.getAsList(elementList.toArray(new Class[elementList.size()])); for (ASTEntry entry : nodes) { createFoldingEntries((ASTEntryWithChildren) entry, ret); } //and at last, get the comments if (prefs.getBoolean(PyDevCodeFoldingPrefPage.FOLD_COMMENTS)) { DocIterator it = new PySelection.DocIterator(true, new PySelection(doc, 0)); while (it.hasNext()) { String string = it.next(); if (string.trim().startsWith("#")) { int l = it.getCurrentLine() - 1; addFoldingEntry(ret, new FoldingEntry(FoldingEntry.TYPE_COMMENT, l, l + 1, new ASTEntry(null, new commentType(string)))); } } } Collections.sort(ret, new Comparator<FoldingEntry>() { public int compare(FoldingEntry o1, FoldingEntry o2) { if (o1.startLine < o2.startLine) { return -1; } if (o1.startLine > o2.startLine) { return 1; } return 0; } }); return ret; } /** * @param entry the entry that should be added * @param ret the list where the folding entry generated should be added * @param memo a memo for the nodes that already generated a folding entry (needed * for treating if..elif because the elif will be generated when the if is found, and if it's * found again later we'll want to ignore it) */ private static void createFoldingEntries(ASTEntryWithChildren entry, List<FoldingEntry> ret) { FoldingEntry foldingEntry = null; if (entry.node instanceof Import || entry.node instanceof ImportFrom) { foldingEntry = new FoldingEntry(FoldingEntry.TYPE_IMPORT, entry.node.beginLine - 1, entry.endLine, entry); } else if (entry.node instanceof ClassDef) { ClassDef def = (ClassDef) entry.node; foldingEntry = new FoldingEntry(FoldingEntry.TYPE_DEF, def.name.beginLine - 1, entry.endLine, entry); } else if (entry.node instanceof FunctionDef) { FunctionDef def = (FunctionDef) entry.node; foldingEntry = new FoldingEntry(FoldingEntry.TYPE_DEF, def.name.beginLine - 1, entry.endLine, entry); } else if (entry.node instanceof TryExcept) { foldingEntry = new FoldingEntry(FoldingEntry.TYPE_EXCEPT, entry.node.beginLine - 1, entry.endLine, entry); //Removed: we shouldn't have to rely on getting the body 'end' line at this point... that info should already come //from the CodeFoldingVisitor (so, this code must be adapted to that... also, a revision on the coding standard //must be done) TryExcept tryStmt = (TryExcept) entry.node; if (tryStmt.handlers != null) { for (excepthandlerType except : tryStmt.handlers) { foldingEntry = checkExcept(entry, ret, foldingEntry, entry.endLine, except); } } if (tryStmt.orelse != null) { foldingEntry = checkOrElse(entry, ret, foldingEntry, entry.endLine, tryStmt.orelse); } } else if (entry.node instanceof TryFinally) { //entry for the whole try..finally block TryFinally tryStmt = (TryFinally) entry.node; if (tryStmt.body != null && tryStmt.body.length > 0) { if (!(tryStmt.body[0] instanceof TryExcept) || (tryStmt.body[0].beginLine != tryStmt.beginLine)) { //Ignore the try if it is part of a try except block in the format: //try..except..finally (in the same block) foldingEntry = new FoldingEntry(FoldingEntry.TYPE_FINALLY, entry.node.beginLine - 1, entry.endLine, entry); } } if (tryStmt.finalbody != null) { if (foldingEntry != null) { //ok, add the current and set the new current to the finally block foldingEntry = checkFinally(entry, ret, foldingEntry, entry.endLine, tryStmt.finalbody, true); } else { //the current one shouldn't be added... (just the finally part) foldingEntry = new FoldingEntry(FoldingEntry.TYPE_FINALLY, entry.node.beginLine - 1, entry.endLine, entry); foldingEntry = checkFinally(entry, ret, foldingEntry, entry.endLine, tryStmt.finalbody, false); } } } else if (entry.node instanceof With) { foldingEntry = new FoldingEntry(FoldingEntry.TYPE_STATEMENT, entry.node.beginLine - 1, entry.endLine, entry); } else if (entry.node instanceof While) {//XXX start test section foldingEntry = new FoldingEntry(FoldingEntry.TYPE_STATEMENT, entry.node.beginLine - 1, entry.endLine, entry); foldingEntry = checkOrElse(entry, ret, foldingEntry, entry.endLine, ((While) entry.node).orelse); } else if (entry.node instanceof For) { foldingEntry = new FoldingEntry(FoldingEntry.TYPE_STATEMENT, entry.node.beginLine - 1, entry.endLine, entry); foldingEntry = checkOrElse(entry, ret, foldingEntry, entry.endLine, ((For) entry.node).orelse); } else if (entry.node instanceof If) {//If comes 'ok' from the CodeFoldingVisitor (no need to check for the else part) foldingEntry = new FoldingEntry(FoldingEntry.TYPE_STATEMENT, entry.node.beginLine - 1, entry.endLine, entry); } else if (entry.node instanceof Str) { if (entry.node.beginLine != entry.endLine) { foldingEntry = new FoldingEntry(FoldingEntry.TYPE_STR, entry.node.beginLine - 1, entry.endLine, entry); } } if (foldingEntry != null) { addFoldingEntry(ret, foldingEntry); } } /** * Checks an entry for its 'else' statement. If found, will add a folding entry for the previous block and * return a new entry for the 'else' part (to the end of the previous block). * * @param entry the entry that we're analyzing at this point * @param ret where the folding entry should be added * @param foldingEntry the folding entry that will be added with the contents o the full block (so, if it's a * while...else, it contains the position up to the end of the else block. * @param blockEndLine the end line of the whole block (with the else part) * @param orelse the suite with the else part * @return the same folding entry passed or a new folding entry that should be added in the place of the one passed * as a parameter */ private static FoldingEntry checkOrElse(ASTEntryWithChildren entry, List<FoldingEntry> ret, FoldingEntry foldingEntry, int blockEndLine, suiteType orelse) { return checkOrElseSuite(entry, ret, foldingEntry, blockEndLine, orelse, FoldingEntry.TYPE_ELSE, "else", true); } private static FoldingEntry checkFinally(ASTEntryWithChildren entry, List<FoldingEntry> ret, FoldingEntry foldingEntry, int blockEndLine, suiteType orelse, boolean addPrevious) { return checkOrElseSuite(entry, ret, foldingEntry, blockEndLine, orelse, FoldingEntry.TYPE_FINALLY, "finally", addPrevious); } private static FoldingEntry checkExcept(ASTEntryWithChildren entry, List<FoldingEntry> ret, FoldingEntry foldingEntry, int blockEndLine, excepthandlerType orelse) { return checkOrElseSuite(entry, ret, foldingEntry, blockEndLine, orelse, FoldingEntry.TYPE_EXCEPT, "except", true); } private static FoldingEntry checkOrElseSuite(ASTEntryWithChildren entry, List<FoldingEntry> ret, FoldingEntry foldingEntry, int blockEndLine, SimpleNode orelse, int type, String specialToken, boolean addPrevious) { if (orelse != null) { if (orelse.specialsBefore != null) { for (Object o : orelse.specialsBefore) { if (o instanceof ISpecialStr) { ISpecialStr specialStr = (ISpecialStr) o; if (specialStr.toString().equals(specialToken)) { foldingEntry.endLine = specialStr.getBeginLine() - 1; if (addPrevious) { addFoldingEntry(ret, foldingEntry); } foldingEntry = new FoldingEntry(type, specialStr.getBeginLine() - 1, blockEndLine, entry); } } } } } return foldingEntry; } public static IPreferenceStore getPreferences() { if (testingPrefs == null) { return PydevPrefs.getPreferences(); } else { if (PydevPlugin.getDefault() != null) { throw new RuntimeException("Should only get here in tests!"); } return testingPrefs; } } private static IPreferenceStore testingPrefs; /** * Used for tests * @return */ public static void setPreferences(IPreferenceStore prefs) { CodeFoldingSetter.testingPrefs = prefs; } private static void addFoldingEntry(List<FoldingEntry> ret, FoldingEntry foldingEntry) { //we only group comments and imports if (ret.size() > 0 && (foldingEntry.type == FoldingEntry.TYPE_COMMENT || foldingEntry.type == FoldingEntry.TYPE_IMPORT)) { FoldingEntry prev = ret.get(ret.size() - 1); if (prev.type == foldingEntry.type && prev.startLine < foldingEntry.startLine && prev.endLine == foldingEntry.startLine) { prev.endLine = foldingEntry.endLine; } else { ret.add(foldingEntry); } } else { ret.add(foldingEntry); } } public void errorChanged(ErrorDescription errorDesc) { //ignore the errors (we're just interested in the ast in this class) } }