/** * Copyright (c) 2014 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.editor.actions.organize_imports; import java.io.File; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.python.pydev.core.IModule; import org.python.pydev.core.IProjectModulesManager; import org.python.pydev.core.IPyFormatStdProvider; import org.python.pydev.core.IPythonNature; import org.python.pydev.core.ISystemModulesManager; import org.python.pydev.core.MisconfigurationException; import org.python.pydev.core.PythonNatureWithoutProjectException; import org.python.pydev.core.docutils.ImportHandle; import org.python.pydev.core.log.Log; import org.python.pydev.plugin.nature.PythonNature; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple3; import org.python.pydev.ui.importsconf.ImportsPreferencesPage; public class Pep8ImportArranger extends ImportArranger { static final class DummyImportClassifier extends ImportClassifier { @Override int classify(ImportHandle imp) { String module = getModuleName(imp); if (module.equals("__future__")) { return FUTURE; } if (module.startsWith(".")) { return RELATIVE; } return OUR_CODE; } } static abstract class ImportClassifier { static final int FUTURE = 0; static final int SYSTEM = 1; static final int THIRD_PARTY = 2; static final int OUR_CODE = 3; static final int RELATIVE = 4; abstract int classify(ImportHandle imp); } private static abstract class ImportType { static final int IMPORT = 1; static final int FROM = 2; } /** * Return the imported module associated with a given * 'import ...' or 'from ... import ...' statement. */ static String getModuleName(ImportHandle imp) { String module = imp.getImportInfo().get(0).getFromImportStr(); if (module == null) { module = imp.getImportInfo().get(0).getImportedStr().get(0); } return module; } /** * Return true if the given import uses the 'from ... import ...' * syntax, and false if it uses 'import ...' */ static int getImportType(ImportHandle imp) { String module = imp.getImportInfo().get(0).getFromImportStr(); if (module != null) { return ImportType.FROM; } return ImportType.IMPORT; } static class PathImportClassifier extends ImportClassifier { private List<String> externalSourcePaths; private ISystemModulesManager manager; private IPythonNature nature; private IProjectModulesManager projectModulesManager; private Map<Object, Integer> mapToClassification = new HashMap<Object, Integer>(); PathImportClassifier(IProject project) throws MisconfigurationException, PythonNatureWithoutProjectException { PythonNature nature = PythonNature.getPythonNature(project); if (nature != null) { try { String externalProjectSourcePath = nature.getPythonPathNature().getProjectExternalSourcePath(true); externalSourcePaths = StringUtils.splitAndRemoveEmptyTrimmed(externalProjectSourcePath, '|'); manager = nature.getProjectInterpreter().getModulesManager(); projectModulesManager = (IProjectModulesManager) nature.getAstManager().getModulesManager(); this.nature = nature; } catch (CoreException e) { Log.log(e); } } } @Override int classify(ImportHandle imp) { //Cache it as it may be asked multiple times for the same element during a sort. String module = getModuleName(imp); Integer currClassification = mapToClassification.get(module); if (currClassification != null) { return currClassification; } int classification = classifyInternal(module); mapToClassification.put(module, classification); return classification; } private int classifyInternal(String module) { if (module.equals("__future__")) { return FUTURE; } if (module.startsWith(".")) { return RELATIVE; } if (nature == null) { return OUR_CODE; } IModule mod; mod = manager.getModule(module, nature, false); if (mod == null) { mod = projectModulesManager.getModuleInDirectManager(module, nature, false); if (mod != null) { File file = mod.getFile(); if (file != null) { String fileAbsolutePath = FileUtils.getFileAbsolutePath(file); int len = externalSourcePaths.size(); for (int i = 0; i < len; i++) { String path = externalSourcePaths.get(i); if (fileAbsolutePath.startsWith(path)) { return THIRD_PARTY; } } } } return OUR_CODE; } File file = mod.getFile(); //Not sure I like this approach, but couldn't come up with anything better. if (file != null && file.getAbsolutePath().contains("site-packages")) { return THIRD_PARTY; } return SYSTEM; } } final ImportClassifier classifier; public Pep8ImportArranger(IDocument doc, boolean removeUnusedImports, String endLineDelim, IProject prj, String indentStr, boolean automatic, IPyFormatStdProvider edit) { super(doc, removeUnusedImports, endLineDelim, indentStr, automatic, edit); classifier = getClassifier(prj); } private ImportClassifier getClassifier(IProject p) { if (p != null) { try { return new PathImportClassifier(p); } catch (MisconfigurationException e) { } catch (PythonNatureWithoutProjectException e) { } } return new DummyImportClassifier(); } @Override public void perform() { // if (ImportsPreferencesPage.getGroupImports()) { // perform(true); -- TODO: This mode is flawed (must be reviewed). // } else { perform(false, edit); // } } @Override protected void sortImports(List<Tuple3<Integer, String, ImportHandle>> list) { Collections.sort(list, new Comparator<Tuple3<Integer, String, ImportHandle>>() { @Override public int compare(Tuple3<Integer, String, ImportHandle> o1, Tuple3<Integer, String, ImportHandle> o2) { int class1 = classifier.classify(o1.o3); int class2 = classifier.classify(o2.o3); if (class1 != class2) { return class1 - class2; } if (ImportsPreferencesPage.getSortFromImportsFirst(edit)) { int type1 = getImportType(o1.o3); int type2 = getImportType(o2.o3); if (type1 != type2) { return type2 - type1; } } int rslt = getModuleName(o1.o3).compareTo(getModuleName(o2.o3)); if (rslt != 0) { return rslt; } return o1.o2.compareTo(o2.o2); } }); } private int classification = -1; private boolean foundDocComment = false; @Override protected void writeImports(List<Tuple3<Integer, String, ImportHandle>> list, FastStringBuffer all) { super.writeImports(list, all); if (all.startsWith(endLineDelim)) { for (int i = endLineDelim.length(); i > 0; i--) { all.deleteFirst(); } } } @Override protected void beforeImport(Tuple3<Integer, String, ImportHandle> element, FastStringBuffer all) { int c = classifier.classify(element.o3); if (c != classification) { all.append(endLineDelim); classification = c; } } @Override protected void beforeImports(FastStringBuffer all) { if (foundDocComment) { all.append(this.endLineDelim); } } @Override protected void afterImports(FastStringBuffer all) { all.append(this.endLineDelim); all.append(this.endLineDelim); } @Override protected int insertImportsHere(int lineOfFirstOldImport) { return skipOverDocComment(lineOfFirstOldImport) - 1; } /** * * This enum encapsulates the logic of the {@link ImportArranger#skipOverDocComment} method. * The order is significant, the matches method is called in order on * each value, until the value for the line in consideration is found. * @author jeremycarroll * */ private enum SkipLineType { EndDocComment { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return startDocComment.isEndDocComment(line.trim()); } @Override boolean isEndDocComment(String nextLine) { return true; } }, MidDocComment { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return !startDocComment.isDummy(); } }, SingleQuoteDocComment("'''"), DoubleQuoteDocComment("\"\"\""), BlankLine { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return line.trim().isEmpty(); } }, Comment { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return line.trim().startsWith("#"); } }, Code { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { // presupposes that others do not match! return true; } }, DummyHaventFoundStartDocComment { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return false; } @Override boolean isDummy() { return true; } }, DummyHaveFoundEndDocComment { @Override boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return false; } @Override boolean isDummy() { return true; } @Override public boolean passedDocComment() { return true; } }; final String prefix; final boolean isStartDocComment; SkipLineType(String prefix, boolean isDocComment) { this.prefix = prefix; isStartDocComment = isDocComment; } SkipLineType() { this(null, false); } SkipLineType(String prefix) { this(prefix, true); } boolean matches(String line, Pep8ImportArranger.SkipLineType startDocComment) { return line.startsWith(prefix); } boolean matchesStartAndEnd(String line) { if (prefix == null) { return false; } line = line.trim(); return line.length() >= 2 * prefix.length() && line.startsWith(prefix) && line.endsWith(prefix); } boolean isEndDocComment(String nextLine) { return isStartDocComment && nextLine.trim().endsWith(prefix); } boolean isDummy() { return false; } public boolean passedDocComment() { return false; } } private Pep8ImportArranger.SkipLineType findLineType(String line, Pep8ImportArranger.SkipLineType state) { for (Pep8ImportArranger.SkipLineType slt : SkipLineType.values()) { if (slt.matches(line, state)) { return slt; } } throw new IllegalStateException("No match"); } private int skipOverDocComment(int firstOldImportLine) { try { Pep8ImportArranger.SkipLineType parseState = SkipLineType.DummyHaventFoundStartDocComment; for (int l = firstOldImportLine; true; l++) { IRegion lineInfo = doc.getLineInformation(l); String line = doc.get(lineInfo.getOffset(), lineInfo.getLength()); Pep8ImportArranger.SkipLineType slt = findLineType(line, parseState); switch (slt) { case MidDocComment: case Comment: break; case Code: if (!parseState.passedDocComment()) { return firstOldImportLine; } else { foundDocComment = true; return l; } case BlankLine: // delete all blank lines in imports section of document addNewLinesToImports = true; l--; doc.replace(lineInfo.getOffset(), lineInfo.getLength() + endLineDelim.length(), ""); break; case DoubleQuoteDocComment: case SingleQuoteDocComment: if (slt.matchesStartAndEnd(line)) { parseState = SkipLineType.DummyHaveFoundEndDocComment; } else { parseState = slt; } break; case EndDocComment: parseState = SkipLineType.DummyHaveFoundEndDocComment; break; default: throw new IllegalStateException(slt.name() + " not expected"); } } } catch (BadLocationException e) { } return firstOldImportLine; } }