/** * 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.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.python.pydev.core.IPyFormatStdProvider; import org.python.pydev.core.docutils.ImportHandle; import org.python.pydev.core.docutils.ImportHandle.ImportHandleInfo; import org.python.pydev.core.docutils.PyImportsHandling; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.log.Log; import org.python.pydev.editor.actions.PyFormatStd; import org.python.pydev.plugin.preferences.PydevPrefs; import org.python.pydev.shared_core.SharedCorePlugin; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_core.structure.Tuple3; import org.python.pydev.ui.importsconf.ImportsPreferencesPage; public class ImportArranger { public boolean addNewLinesToImports = false; private final class FromImportEntries { private final List<ImportHandleInfo> containedImports = new ArrayList<>(); private final List<Tuple<String, String>> importsAndComments = new ArrayList<Tuple<String, String>>(); private final List<Tuple<String, String>> importsAndNoComments = new ArrayList<Tuple<String, String>>(); private final FastStringBuffer lastFromXXXImportWritten = new FastStringBuffer(); private final FastStringBuffer line = new FastStringBuffer(); private String from; public void add(ImportHandleInfo info) { containedImports.add(info); } private void checkForCommentsAfterImport() { //first, reorganize them in the order to be written (the ones with comments after the ones without) for (ImportHandleInfo v : FromImportEntries.this.containedImports) { List<String> importedStr = v.getImportedStr(); List<String> commentsForImports = v.getCommentsForImports(); for (int i = 0; i < importedStr.size(); i++) { String importedString = importedStr.get(i).trim(); String comment = commentsForImports.get(i).trim(); boolean isWildImport = importedString.equals("*"); if (isWildImport) { importsAndComments.clear(); importsAndNoComments.clear(); } if (comment.length() > 0) { importsAndComments.add(new Tuple<String, String>(importedString, comment)); } else { importsAndNoComments.add(new Tuple<String, String>(importedString, comment)); } if (isWildImport) { return; } } } } public void setFrom(String from) { this.from = from; } public void arrangeAndAdd(FastStringBuffer all) { // TODO: this could be clarified further but ... //ok, it's all filled, let's start rewriting it! boolean firstInLine = true; line.clear(); boolean addedParenForLine = false; //ok, write all the ones with comments after the ones without any comments (each one with comment //will be written as a new import) importsAndNoComments.addAll(importsAndComments); if (sortNamesGrouped) { Comparator<? super Tuple<String, String>> c = new Comparator<Tuple<String, String>>() { @Override public int compare(Tuple<String, String> o1, Tuple<String, String> o2) { return o1.o1.compareTo(o2.o1); } }; Collections.sort(importsAndNoComments, c); } for (int i = 0; i < importsAndNoComments.size(); i++) { Tuple<String, String> tuple = importsAndNoComments.get(i); if (firstInLine) { lastFromXXXImportWritten.clear(); lastFromXXXImportWritten.append("from "); lastFromXXXImportWritten.append(from); lastFromXXXImportWritten.append(" import "); line.append(lastFromXXXImportWritten); } else { line.append(", "); } if (multilineImports) { if (line.length() + tuple.o1.length() + tuple.o2.length() > maxCols) { String addAfter = indentStr; //we have to make the wrapping if (breakWithParenthesis) { if (!addedParenForLine) { line.insert(lastFromXXXImportWritten.length(), '('); if (StringUtils.rightTrim(line.toString()).length() > maxCols) { addAfter = indentStr + line.subSequence(lastFromXXXImportWritten.length() + 1, line.length()) .toString(); line.setLength(lastFromXXXImportWritten.length() + 1); } addedParenForLine = true; } line.append(endLineDelim); } else { line.append('\\'); line.append(endLineDelim); } all.append(line); line.clear(); line.append(addAfter); } } line.append(tuple.o1); if (addedParenForLine && i == importsAndNoComments.size()) { addedParenForLine = false; line.append(')'); } firstInLine = false; if (tuple.o2.length() > 0) { if (addedParenForLine) { addedParenForLine = false; line.append(')'); } line.append(' '); line.append(tuple.o2); line.append(endLineDelim); all.append(line); line.clear(); firstInLine = true; } } if (!firstInLine) { if (addedParenForLine) { addedParenForLine = false; line.append(')'); } line.append(endLineDelim); all.append(line); line.clear(); } } } /** * @return the maximum number of columns that may be available in a line. */ private static int getMaxCols(boolean multilineImports) { final int maxCols; if (multilineImports) { if (SharedCorePlugin.inTestMode()) { maxCols = 80; } else { IPreferenceStore chainedPrefStore = PydevPrefs.getChainedPrefStore(); maxCols = chainedPrefStore .getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN); } } else { maxCols = Integer.MAX_VALUE; } return maxCols; } /** * @return true if the imports should be split with parenthesis (instead of escaping) */ private static boolean getBreakImportsWithParenthesis(IPyFormatStdProvider edit) { String breakIportMode = ImportsPreferencesPage.getBreakIportMode(edit); boolean breakWithParenthesis = true; if (!breakIportMode.equals(ImportsPreferencesPage.BREAK_IMPORTS_MODE_PARENTHESIS)) { breakWithParenthesis = false; } return breakWithParenthesis; } protected final IDocument doc; protected final String endLineDelim; private final String indentStr; private int lineForNewImports = -1; private final boolean multilineImports; private final boolean sortNamesGrouped; private int maxCols; private final boolean breakWithParenthesis; private final boolean removeUnusedImports; private final boolean automatic; protected final IPyFormatStdProvider edit; public ImportArranger(IDocument doc, boolean removeUnusedImports, String endLineDelim, String indentStr, boolean automatic, IPyFormatStdProvider edit) { this.doc = doc; this.endLineDelim = endLineDelim; this.indentStr = indentStr; this.removeUnusedImports = removeUnusedImports; this.automatic = automatic; this.edit = edit; multilineImports = ImportsPreferencesPage.getMultilineImports(edit); sortNamesGrouped = ImportsPreferencesPage.getSortNamesGrouped(edit); breakWithParenthesis = getBreakImportsWithParenthesis(edit); maxCols = getMaxCols(multilineImports); } public void perform() { perform(ImportsPreferencesPage.getGroupImports(edit), edit); } protected void perform(boolean groupFromImports, IPyFormatStdProvider edit) { boolean executeOnlyIfChanged = automatic; perform(groupFromImports, executeOnlyIfChanged, edit); } /** * @param executeOnlyIfChanged: if 'true' initially, we'll check if something changes first. If something changes * it'll call itself again with 'false' to force the changes. */ private void perform(boolean groupFromImports, boolean executeOnlyIfChanged, IPyFormatStdProvider edit) { List<Tuple3<Integer, String, ImportHandle>> list = collectImports(); if (list.isEmpty()) { return; } int lineOfFirstOldImport = list.get(0).o1; List<Tuple<Integer, String>> linesToDelete = deleteImports(list); if (!executeOnlyIfChanged) { for (Tuple<Integer, String> tup : linesToDelete) { PySelection.deleteLine(doc, tup.o1); } } lineForNewImports = insertImportsHere(lineOfFirstOldImport); if (this.removeUnusedImports) { pruneEmptyImports(list); } sortImports(list); //now, re-add the imports String finalStr = createImportsStr(groupFromImports, list); if (executeOnlyIfChanged) { //If going automatic, let's check the contents before actually doing the organize //(and skip if the order is ok). ArrayList<String> list2 = new ArrayList<String>(); for (Tuple<Integer, String> tup : linesToDelete) { list2.add(tup.o2); } Collections.reverse(list2); String join = StringUtils.join("", list2).trim(); String other = StringUtils.replaceNewLines(finalStr, "").trim(); if (join.equals(other)) { // System.out.println("Equals"); } else { // System.out.println("Not equal!"); // System.out.println("\n\n---"); // System.out.println(join); // System.out.println("---"); // System.out.println(other); // System.out.println("---\n"); perform(groupFromImports, false, edit); } return; } try { PyFormatStd std = new PyFormatStd(); boolean throwSyntaxError = false; ISelectionProvider selectionProvider = null; int[] regionsToFormat = null; IDocument psDoc = new Document(finalStr); PySelection ps = new PySelection(psDoc); std.applyFormatAction(edit, ps, regionsToFormat, throwSyntaxError, selectionProvider); finalStr = psDoc.get(); if (addNewLinesToImports) { // Leave 2 empty new lines separating imports from code String expectedEnd = endLineDelim + endLineDelim + endLineDelim; while (!finalStr.endsWith(expectedEnd)) { finalStr += endLineDelim; } } } catch (Exception e) { Log.log(e); } PySelection.addLine(doc, endLineDelim, finalStr, lineForNewImports); } private String createImportsStr(boolean groupFromImports, List<Tuple3<Integer, String, ImportHandle>> list) { FastStringBuffer all = new FastStringBuffer(); if (!groupFromImports) { writeImports(list, all); } else { //we have to group the imports! groupAndWriteImports(list, all); } String finalStr = all.toString(); return finalStr; } private void pruneEmptyImports(List<Tuple3<Integer, String, ImportHandle>> list) { Iterator<Tuple3<Integer, String, ImportHandle>> it = list.iterator(); while (it.hasNext()) { ImportHandle ih = it.next().o3; List<ImportHandleInfo> info = ih.getImportInfo(); Iterator<ImportHandleInfo> itInfo = info.iterator(); while (itInfo.hasNext()) { if (itInfo.next().getImportedStr().isEmpty()) { itInfo.remove(); } } if (info.size() == 0) { it.remove(); } } } protected void writeImports(List<Tuple3<Integer, String, ImportHandle>> list, FastStringBuffer all) { beforeImports(all); //no grouping for (Iterator<Tuple3<Integer, String, ImportHandle>> iter = list.iterator(); iter.hasNext();) { Tuple3<Integer, String, ImportHandle> element = iter.next(); beforeImport(element, all); all.append(element.o2); all.append(endLineDelim); } afterImports(all); } protected void beforeImports(FastStringBuffer all) { } protected void afterImports(FastStringBuffer all) { } protected void beforeImport(Tuple3<Integer, String, ImportHandle> element, FastStringBuffer all) { // do nothing } protected int insertImportsHere(int lineOfFirstOldImport) { return lineOfFirstOldImport - 1; } private void groupAndWriteImports(List<Tuple3<Integer, String, ImportHandle>> list, FastStringBuffer all) { //import from to the imports that should be grouped given its 'from' TreeMap<String, FromImportEntries> importsWithFrom = new TreeMap<String, FromImportEntries>( new Comparator<String>() { @Override public int compare(String o1, String o2) { Tuple<String, String> splitted1 = StringUtils.splitOnFirst(o1, '.'); Tuple<String, String> splitted2 = StringUtils.splitOnFirst(o2, '.'); boolean isFuture1 = splitted1.o1.equals("__future__"); boolean isFuture2 = splitted2.o1.equals("__future__"); if (isFuture1 != isFuture2) { if (isFuture1) { return -1; } return 1; } return o1.compareTo(o2); } }); List<ImportHandleInfo> importsWithoutFrom = new ArrayList<ImportHandleInfo>(); fillImportStructures(list, importsWithFrom, importsWithoutFrom); Set<Entry<String, FromImportEntries>> entrySet = importsWithFrom.entrySet(); for (Entry<String, FromImportEntries> entry : entrySet) { FromImportEntries value = entry.getValue(); value.setFrom(entry.getKey()); value.checkForCommentsAfterImport(); value.arrangeAndAdd(all); } writeImportsWithoutFrom(all, importsWithoutFrom); } /** * Fills the import structure passed, so that the imports from will be grouped by the 'from' part and the regular * imports will be in a separate list. */ private void fillImportStructures(List<Tuple3<Integer, String, ImportHandle>> list, TreeMap<String, FromImportEntries> importsWithFrom, List<ImportHandleInfo> importsWithoutFrom) { //fill the info for (Tuple3<Integer, String, ImportHandle> element : list) { List<ImportHandleInfo> importInfo = element.o3.getImportInfo(); for (ImportHandleInfo importHandleInfo : importInfo) { String fromImportStr = importHandleInfo.getFromImportStrWithoutUnwantedChars(); if (fromImportStr == null) { importsWithoutFrom.add(importHandleInfo); } else { FromImportEntries lst = importsWithFrom.get(fromImportStr); if (lst == null) { lst = new FromImportEntries(); importsWithFrom.put(fromImportStr, lst); } lst.add(importHandleInfo); } } } } 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) { //When it's __future__, it has to appear before the others. List<ImportHandleInfo> info1 = o1.o3.getImportInfo(); List<ImportHandleInfo> info2 = o2.o3.getImportInfo(); boolean isFuture1 = getIsFuture(info1); boolean isFuture2 = getIsFuture(info2); if (isFuture1 && !isFuture2) { return -1; } if (!isFuture1 && isFuture2) { return 1; } return o1.o2.compareTo(o2.o2); } private boolean getIsFuture(List<ImportHandleInfo> info1) { String from1 = null; if (info1.size() > 0) { from1 = info1.get(0).getFromImportStr(); } boolean isFuture = from1 != null && from1.equals("__future__"); return isFuture; } }); } private List<Tuple<Integer, String>> deleteImports(List<Tuple3<Integer, String, ImportHandle>> list) { //sort in inverse order (for removal of the string of the document). List<Tuple<Integer, String>> linesToDelete = new ArrayList<>(); Collections.sort(list, new Comparator<Tuple3<Integer, String, ImportHandle>>() { @Override public int compare(Tuple3<Integer, String, ImportHandle> o1, Tuple3<Integer, String, ImportHandle> o2) { return o2.o1.compareTo(o1.o1); } }); //ok, now we have to delete all lines with imports. for (Iterator<Tuple3<Integer, String, ImportHandle>> iter = list.iterator(); iter.hasNext();) { Tuple3<Integer, String, ImportHandle> element = iter.next(); String s = element.o2; int max = StringUtils.countLineBreaks(s); for (int i = 0; i <= max; i++) { int lineToDel = (element.o1).intValue(); int j = lineToDel + i; linesToDelete.add(new Tuple(j, PySelection.getLine(doc, j))); } } Comparator<? super Tuple<Integer, String>> c = new Comparator<Tuple<Integer, String>>() { @Override public int compare(Tuple<Integer, String> o1, Tuple<Integer, String> o2) { return Integer.compare(o2.o1, o1.o1); //reversed compare (o2 first o1 last). } }; Collections.sort(linesToDelete, c); return linesToDelete; } final List<Tuple3<Integer, String, ImportHandle>> collectImports() { List<Tuple3<Integer, String, ImportHandle>> list = new ArrayList<Tuple3<Integer, String, ImportHandle>>(); //Gather imports in a structure we can work on. PyImportsHandling pyImportsHandling = new PyImportsHandling(doc, true, this.removeUnusedImports); for (ImportHandle imp : pyImportsHandling) { if (imp.importFound.contains("@NoMove")) { continue; } list.add(new Tuple3<Integer, String, ImportHandle>(imp.startFoundLine, imp.importFound, imp)); } return list; } /** * Write the imports that don't have a 'from' in the beggining (regular imports) */ private void writeImportsWithoutFrom(FastStringBuffer all, List<ImportHandleInfo> importsWithoutFrom) { //now, write the regular imports (no wrapping or tabbing here) for (ImportHandleInfo info : importsWithoutFrom) { List<String> importedStr = info.getImportedStr(); List<String> commentsForImports = info.getCommentsForImports(); for (int i = 0; i < importedStr.size(); i++) { all.append("import "); String importedString = importedStr.get(i); String comment = commentsForImports.get(i); all.append(importedString); if (comment.length() > 0) { all.append(' '); all.append(comment); } all.append(endLineDelim); } } } }