/**
* 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 Feb 18, 2005
*
* @author Fabio Zadrozny
*/
package org.python.pydev.editor.actions;
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.action.IAction;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.python.pydev.core.ExtensionHelper;
import org.python.pydev.core.Tuple3;
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.docutils.StringUtils;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.PyEdit;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.plugin.preferences.PydevPrefs;
import org.python.pydev.ui.importsconf.ImportsPreferencesPage;
import com.aptana.shared_core.string.FastStringBuffer;
import com.aptana.shared_core.structure.Tuple;
/**
* @author Fabio Zadrozny
*/
public class PyOrganizeImports extends PyAction {
/**
* @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
*/
@SuppressWarnings("unchecked")
public void run(IAction action) {
try {
if (!canModifyEditor()) {
return;
}
PyEdit pyEdit = getPyEdit();
PySelection ps = new PySelection(pyEdit);
String endLineDelim = ps.getEndLineDelim();
final IDocument doc = ps.getDoc();
DocumentRewriteSession session = null;
try {
if (ps.getStartLineIndex() == ps.getEndLineIndex()) {
//let's see if someone wants to make a better implementation in another plugin...
List<IOrganizeImports> participants = ExtensionHelper
.getParticipants(ExtensionHelper.PYDEV_ORGANIZE_IMPORTS);
for (IOrganizeImports organizeImports : participants) {
if (!organizeImports.beforePerformArrangeImports(ps, pyEdit)) {
return;
}
}
session = startWrite(doc);
performArrangeImports(doc, endLineDelim, pyEdit.getIndentPrefs().getIndentationString());
for (IOrganizeImports organizeImports : participants) {
organizeImports.afterPerformArrangeImports(ps, pyEdit);
}
} else {
session = startWrite(doc);
performSimpleSort(doc, endLineDelim, ps.getStartLineIndex(), ps.getEndLineIndex());
}
} finally {
if (session != null) {
endWrite(doc, session);
}
}
} catch (Exception e) {
Log.log(e);
beep(e);
}
}
/**
* Stop a rewrite session
*/
private void endWrite(IDocument doc, DocumentRewriteSession session) {
if (doc instanceof IDocumentExtension4) {
IDocumentExtension4 d = (IDocumentExtension4) doc;
d.stopRewriteSession(session);
}
}
/**
* Starts a rewrite session (keep things in a single undo/redo)
*/
private DocumentRewriteSession startWrite(IDocument doc) {
if (doc instanceof IDocumentExtension4) {
IDocumentExtension4 d = (IDocumentExtension4) doc;
return d.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED);
}
return null;
}
/**
* Actually does the action in the document.
*
* @param doc
* @param endLineDelim
*/
public static void performArrangeImports(IDocument doc, String endLineDelim, String indentStr) {
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);
int firstImport = -1;
for (ImportHandle imp : pyImportsHandling) {
list.add(new Tuple3<Integer, String, ImportHandle>(imp.startFoundLine, imp.importFound, imp));
if (firstImport == -1) {
firstImport = imp.startFoundLine;
}
}
//check if we had any import
if (firstImport == -1) {
return;
}
//sort in inverse order (for removal of the string of the document).
Collections.sort(list, new Comparator<Tuple3<Integer, String, ImportHandle>>() {
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 i = com.aptana.shared_core.string.StringUtils.countLineBreaks(s);
while (i >= 0) {
PySelection.deleteLine(doc, (element.o1).intValue());
i--;
}
}
Collections.sort(list, new Comparator<Tuple3<Integer, String, ImportHandle>>() {
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;
}
});
firstImport--; //add line after the the specified
//now, re-add the imports
FastStringBuffer all = new FastStringBuffer();
if (!ImportsPreferencesPage.getGroupImports()) {
//no grouping
for (Iterator<Tuple3<Integer, String, ImportHandle>> iter = list.iterator(); iter.hasNext();) {
Tuple3<Integer, String, ImportHandle> element = iter.next();
all.append(element.o2);
all.append(endLineDelim);
}
} else { //we have to group the imports!
//import from to the imports that should be grouped given its 'from'
TreeMap<String, List<ImportHandleInfo>> importsWithFrom = new TreeMap<String, List<ImportHandleInfo>>(
new Comparator<String>() {
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);
//preferences for multiline imports
boolean multilineImports = ImportsPreferencesPage.getMultilineImports();
int maxCols = getMaxCols(multilineImports);
//preferences for how to break imports
boolean breakWithParenthesis = getBreakImportsWithParenthesis();
Set<Entry<String, List<ImportHandleInfo>>> entrySet = importsWithFrom.entrySet();
FastStringBuffer lastFromXXXImportWritten = new FastStringBuffer();
FastStringBuffer line = new FastStringBuffer();
for (Entry<String, List<ImportHandleInfo>> entry : entrySet) {
//first, reorganize them in the order to be written (the ones with comments after the ones without)
ArrayList<Tuple<String, String>> importsAndComments = new ArrayList<Tuple<String, String>>();
ArrayList<Tuple<String, String>> importsAndNoComments = new ArrayList<Tuple<String, String>>();
fillImportFromInfo(entry, importsAndComments, importsAndNoComments);
//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);
for (int i = 0; i < importsAndNoComments.size(); i++) {
Tuple<String, String> tuple = importsAndNoComments.get(i);
if (firstInLine) {
lastFromXXXImportWritten.clear();
lastFromXXXImportWritten.append("from ");
lastFromXXXImportWritten.append(entry.getKey());
lastFromXXXImportWritten.append(" import ");
line.append(lastFromXXXImportWritten);
} else {
line.append(", ");
}
if (multilineImports) {
if (line.length() + tuple.o1.length() + tuple.o2.length() > maxCols) {
//we have to make the wrapping
if (breakWithParenthesis) {
if (!addedParenForLine) {
line.insert(lastFromXXXImportWritten.length(), '(');
addedParenForLine = true;
}
line.append(endLineDelim);
line.append(indentStr);
} else {
line.append('\\');
line.append(endLineDelim);
line.append(indentStr);
}
all.append(line);
line.clear();
}
}
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();
}
}
writeImportsWithoutFrom(endLineDelim, all, importsWithoutFrom);
}
PySelection.addLine(doc, endLineDelim, all.toString(), firstImport);
}
/**
* Write the imports that don't have a 'from' in the beggining (regular imports)
*/
private static void writeImportsWithoutFrom(String endLineDelim, 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);
}
}
}
/**
* Fills the lists passed based on the entry set, so that imports that have comments are contained in a list
* and imports without comments in another.
*/
private static void fillImportFromInfo(Entry<String, List<ImportHandleInfo>> entry,
ArrayList<Tuple<String, String>> importsAndComments, ArrayList<Tuple<String, String>> importsAndNoComments) {
for (ImportHandleInfo v : entry.getValue()) {
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;
}
}
}
}
/**
* @return true if the imports should be split with parenthesis (instead of escaping)
*/
private static boolean getBreakImportsWithParenthesis() {
String breakIportMode = ImportsPreferencesPage.getBreakIportMode();
boolean breakWithParenthesis = true;
if (!breakIportMode.equals(ImportsPreferencesPage.BREAK_IMPORTS_MODE_PARENTHESIS)) {
breakWithParenthesis = false;
}
return breakWithParenthesis;
}
/**
* @return the maximum number of columns that may be available in a line.
*/
private static int getMaxCols(boolean multilineImports) {
int maxCols = 80;
if (multilineImports) {
if (PydevPlugin.getDefault() != null) {
IPreferenceStore chainedPrefStore = PydevPrefs.getChainedPrefStore();
maxCols = chainedPrefStore
.getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN);
}
} else {
maxCols = Integer.MAX_VALUE;
}
return maxCols;
}
/**
* 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 static void fillImportStructures(List<Tuple3<Integer, String, ImportHandle>> list,
TreeMap<String, List<ImportHandleInfo>> importsWithFrom, List<ImportHandleInfo> importsWithoutFrom) {
//fill the info
for (Iterator<Tuple3<Integer, String, ImportHandle>> iter = list.iterator(); iter.hasNext();) {
Tuple3<Integer, String, ImportHandle> element = iter.next();
List<ImportHandleInfo> importInfo = element.o3.getImportInfo();
for (ImportHandleInfo importHandleInfo : importInfo) {
String fromImportStr = importHandleInfo.getFromImportStr();
if (fromImportStr == null) {
importsWithoutFrom.add(importHandleInfo);
} else {
List<ImportHandleInfo> lst = importsWithFrom.get(fromImportStr);
if (lst == null) {
lst = new ArrayList<ImportHandleInfo>();
importsWithFrom.put(fromImportStr, lst);
}
lst.add(importHandleInfo);
}
}
}
}
/**
* Performs a simple sort without taking into account the actual contents of the selection (aside from lines
* ending with '\' which are considered as a single line).
*
* @param doc the document to be sorted
* @param endLineDelim the delimiter to be used
* @param startLine the first line where the sort should happen
* @param endLine the last line where the sort should happen
*/
public static void performSimpleSort(IDocument doc, String endLineDelim, int startLine, int endLine) {
try {
ArrayList<String> list = new ArrayList<String>();
StringBuffer lastLine = null;
for (int i = startLine; i <= endLine; i++) {
String line = PySelection.getLine(doc, i);
if (lastLine != null) {
int len = lastLine.length();
if (len > 0 && lastLine.charAt(len - 1) == '\\') {
lastLine.append(endLineDelim);
lastLine.append(line);
} else {
list.add(lastLine.toString());
lastLine = new StringBuffer(line);
}
} else {
lastLine = new StringBuffer(line);
}
}
if (lastLine != null) {
list.add(lastLine.toString());
}
Collections.sort(list);
StringBuffer all = new StringBuffer();
for (Iterator iter = list.iterator(); iter.hasNext();) {
String element = (String) iter.next();
all.append(element);
if (iter.hasNext())
all.append(endLineDelim);
}
int length = doc.getLineInformation(endLine).getLength();
int endOffset = doc.getLineInformation(endLine).getOffset() + length;
int startOffset = doc.getLineInformation(startLine).getOffset();
doc.replace(startOffset, endOffset - startOffset, all.toString());
} catch (BadLocationException e) {
Log.log(e);
}
}
}