diff options
Diffstat (limited to 'python/src/com/jetbrains/python/codeInsight/imports')
5 files changed, 151 insertions, 59 deletions
diff --git a/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java b/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java index 1c09ee24f7dd..d33e97c1e7f9 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java @@ -21,14 +21,14 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ProjectRootManager; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.QualifiedName; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.codeInsight.PyCodeInsightSettings; import com.jetbrains.python.documentation.DocStringUtil; import com.jetbrains.python.psi.*; -import com.intellij.psi.util.QualifiedName; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; @@ -36,6 +36,8 @@ import org.jetbrains.annotations.Nullable; import java.util.List; +import static com.jetbrains.python.psi.PyUtil.sure; + /** * Does the actual job of adding an import statement into a file. * User: dcheryasov @@ -47,6 +49,34 @@ public class AddImportHelper { private AddImportHelper() { } + public static void addLocalImportStatement(@NotNull PyElement element, @NotNull String name) { + final PyElementGenerator generator = PyElementGenerator.getInstance(element.getProject()); + final LanguageLevel languageLevel = LanguageLevel.forElement(element); + + final PsiElement anchor = getLocalInsertPosition(element); + final PsiElement parentElement = sure(anchor).getParent(); + if (parentElement != null) { + parentElement.addBefore(generator.createImportStatement(languageLevel, name, null), anchor); + } + } + + public static void addLocalFromImportStatement(@NotNull PyElement element, @NotNull String qualifier, @NotNull String name) { + final PyElementGenerator generator = PyElementGenerator.getInstance(element.getProject()); + final LanguageLevel languageLevel = LanguageLevel.forElement(element); + + final PsiElement anchor = getLocalInsertPosition(element); + final PsiElement parentElement = sure(anchor).getParent(); + if (parentElement != null) { + parentElement.addBefore(generator.createFromImportStatement(languageLevel, qualifier, name, null), anchor); + } + + } + + @Nullable + public static PsiElement getLocalInsertPosition(@NotNull PyElement anchor) { + return PsiTreeUtil.getParentOfType(anchor, PyStatement.class, false); + } + public enum ImportPriority { BUILTIN, THIRD_PARTY, PROJECT } @@ -81,7 +111,8 @@ public class AddImportHelper { // maybe we arrived at the doc comment stmt; skip over it, too else if (!skippedOverImports && !skippedOverDoc && file instanceof PyFile) { PsiElement doc_elt = - DocStringUtil.findDocStringExpression((PyElement)file); // this gives the literal; its parent is the expr seeker may have encountered + DocStringUtil + .findDocStringExpression((PyElement)file); // this gives the literal; its parent is the expr seeker may have encountered if (doc_elt != null && doc_elt.getParent() == feeler) { feeler = feeler.getNextSibling(); seeker = feeler; // skip over doc even if there's nothing below it @@ -147,19 +178,13 @@ public class AddImportHelper { * @param file where to operate * @param name which to import (qualified is OK) * @param asName optional name for 'as' clause + * @return whether import statement was actually added */ public static boolean addImportStatement(PsiFile file, String name, @Nullable String asName, ImportPriority priority) { - String as_clause; - if (asName == null) { - as_clause = ""; - } - else { - as_clause = " as " + asName; - } if (!(file instanceof PyFile)) { return false; } - List<PyImportElement> existingImports = ((PyFile)file).getImportTargets(); + final List<PyImportElement> existingImports = ((PyFile)file).getImportTargets(); for (PyImportElement element : existingImports) { final QualifiedName qName = element.getImportedQName(); if (qName != null && name.equals(qName.toString())) { @@ -171,7 +196,7 @@ public class AddImportHelper { final PyElementGenerator generator = PyElementGenerator.getInstance(file.getProject()); final LanguageLevel languageLevel = LanguageLevel.forElement(file); - final PyImportStatement importNodeToInsert = generator.createImportStatementFromText(languageLevel, "import " + name + as_clause); + final PyImportStatement importNodeToInsert = generator.createImportStatement(languageLevel, name, asName); try { file.addBefore(importNodeToInsert, getInsertPosition(file, name, priority)); } @@ -180,6 +205,7 @@ public class AddImportHelper { } return true; } + /** * Adds an "import ... from ..." statement below other top-level imports. * @@ -189,20 +215,20 @@ public class AddImportHelper { * @param asName optional name for 'as' clause */ public static void addImportFromStatement(PsiFile file, String from, String name, @Nullable String asName, ImportPriority priority) { - String asClause = asName == null ? "" : " as " + asName; - - final PyFromImportStatement importNodeToInsert = PyElementGenerator.getInstance(file.getProject()).createFromText( - LanguageLevel.forElement(file), PyFromImportStatement.class, "from " + from + " import " + name + asClause); + final PyElementGenerator generator = PyElementGenerator.getInstance(file.getProject()); + final LanguageLevel languageLevel = LanguageLevel.forElement(file); + final PyFromImportStatement nodeToInsert = generator.createFromImportStatement(languageLevel, from, name, asName); try { if (InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) { - final PsiElement element = file.addBefore(importNodeToInsert, getInsertPosition(file, from, priority)); + final PsiElement element = file.addBefore(nodeToInsert, getInsertPosition(file, from, priority)); PsiElement whitespace = element.getNextSibling(); - if (!(whitespace instanceof PsiWhiteSpace)) + if (!(whitespace instanceof PsiWhiteSpace)) { whitespace = PsiParserFacade.SERVICE.getInstance(file.getProject()).createWhiteSpaceFromText(" >>> "); + } file.addBefore(whitespace, element); } else { - file.addBefore(importNodeToInsert, getInsertPosition(file, from, priority)); + file.addBefore(nodeToInsert, getInsertPosition(file, from, priority)); } } catch (IncorrectOperationException e) { @@ -228,7 +254,7 @@ public class AddImportHelper { } } final PyElementGenerator generator = PyElementGenerator.getInstance(file.getProject()); - PyImportElement importElement = generator.createImportElement(LanguageLevel.forElement(file), name); + final PyImportElement importElement = generator.createImportElement(LanguageLevel.forElement(file), name); existingImport.add(importElement); return true; } @@ -239,7 +265,8 @@ public class AddImportHelper { public static void addImport(final PsiNamedElement target, final PsiFile file, final PyElement element) { final boolean useQualified = !PyCodeInsightSettings.getInstance().PREFER_FROM_IMPORT; - final PsiFileSystemItem toImport = target instanceof PsiFileSystemItem ? ((PsiFileSystemItem)target).getParent() : target.getContainingFile(); + final PsiFileSystemItem toImport = + target instanceof PsiFileSystemItem ? ((PsiFileSystemItem)target).getParent() : target.getContainingFile(); final ImportPriority priority = getImportPriority(file, toImport); final QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(target, element); if (qName == null) return; diff --git a/python/src/com/jetbrains/python/codeInsight/imports/AutoImportQuickFix.java b/python/src/com/jetbrains/python/codeInsight/imports/AutoImportQuickFix.java index 47c9ca9f8ee5..ce3c07932eb4 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/AutoImportQuickFix.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/AutoImportQuickFix.java @@ -29,6 +29,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileSystemItem; import com.intellij.psi.PsiReference; +import com.intellij.psi.util.QualifiedName; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyBundle; import com.jetbrains.python.codeInsight.PyCodeInsightSettings; @@ -36,7 +37,6 @@ import com.jetbrains.python.psi.PyElement; import com.jetbrains.python.psi.PyFunction; import com.jetbrains.python.psi.PyImportElement; import com.jetbrains.python.psi.PyQualifiedExpression; -import com.intellij.psi.util.QualifiedName; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -138,7 +138,7 @@ public class AutoImportQuickFix implements LocalQuickFix, HighPriorityAction { myImports.size() > 1, ImportCandidateHolder.getQualifiedName(name, myImports.get(0).getPath(), myImports.get(0).getImportElement()) ); - final ImportFromExistingAction action = new ImportFromExistingAction(myNode, myImports, name, myUseQualifiedImport); + final ImportFromExistingAction action = new ImportFromExistingAction(myNode, myImports, name, myUseQualifiedImport, false); action.onDone(new Runnable() { public void run() { myExpended = true; @@ -166,11 +166,16 @@ public class AutoImportQuickFix implements LocalQuickFix, HighPriorityAction { if (!FileModificationService.getInstance().prepareFileForWrite(file)) return; if (ImportFromExistingAction.isResolved(myReference)) return; // act - ImportFromExistingAction action = new ImportFromExistingAction(myNode, myImports, getNameToImport(), myUseQualifiedImport); + ImportFromExistingAction action = createAction(); action.execute(); // assume that action runs in WriteAction on its own behalf myExpended = true; } + @NotNull + protected ImportFromExistingAction createAction() { + return new ImportFromExistingAction(myNode, myImports, getNameToImport(), myUseQualifiedImport, false); + } + public void sortCandidates() { Collections.sort(myImports); } @@ -203,4 +208,27 @@ public class AutoImportQuickFix implements LocalQuickFix, HighPriorityAction { } return false; } + + @NotNull + public AutoImportQuickFix forLocalImport() { + return new AutoImportQuickFix(myNode, myReference, myUseQualifiedImport) { + @NotNull + @Override + public String getName() { + return super.getName() + " locally"; + } + + @NotNull + @Override + public String getFamilyName() { + return "import locally"; + } + + @NotNull + @Override + protected ImportFromExistingAction createAction() { + return new ImportFromExistingAction(myNode, myImports, getNameToImport(), myUseQualifiedImport, true); + } + }; + } } diff --git a/python/src/com/jetbrains/python/codeInsight/imports/ImportCandidateHolder.java b/python/src/com/jetbrains/python/codeInsight/imports/ImportCandidateHolder.java index b60348d34938..e75df5b9a0d0 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/ImportCandidateHolder.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/ImportCandidateHolder.java @@ -24,18 +24,25 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileSystemItem; -import com.jetbrains.python.psi.*; import com.intellij.psi.util.QualifiedName; +import com.jetbrains.python.psi.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * An immutable holder of information for one auto-import candidate. - * User: dcheryasov - * Date: Apr 23, 2009 4:17:50 PM + * <p/> + * There can be do different flavors of such candidates: + * <ul> + * <li>Candidates based on existing imports in module. In this case {@link #getImportElement()} must return not {@code null}.</li> + * <li>Candidates not yet imported. In this case {@link #getPath()} must return not {@code null}.</li> + * </ul> + * <p/> + * + * @author dcheryasov */ // visibility is intentionally package-level -class ImportCandidateHolder implements Comparable { +class ImportCandidateHolder implements Comparable<ImportCandidateHolder> { private final PsiElement myImportable; private final PyImportElement myImportElement; private final PsiFileSystemItem myFile; @@ -43,15 +50,19 @@ class ImportCandidateHolder implements Comparable { /** * Creates new instance. - * @param importable an element that could be imported either from import element or from file. - * @param file the file which is the source of the importable + * + * @param importable an element that could be imported either from import element or from file. + * @param file the file which is the source of the importable (module for symbols, containing directory for modules and packages) * @param importElement an existing import element that can be a source for the importable. - * @param path import path for the file, as a qualified name (a.b.c) + * @param path import path for the file, as a qualified name (a.b.c) + * For top-level imported symbols it's <em>qualified name of containing module</em> (or package for __init__.py). + * For modules and packages it should be <em>qualified name of their parental package</em> + * (empty for modules and packages located at source roots). + * + * @see com.jetbrains.python.codeInsight.imports.PythonReferenceImporter#proposeImportFix */ - public ImportCandidateHolder( - @NotNull PsiElement importable, @NotNull PsiFileSystemItem file, - @Nullable PyImportElement importElement, @Nullable QualifiedName path - ) { + public ImportCandidateHolder(@NotNull PsiElement importable, @NotNull PsiFileSystemItem file, + @Nullable PyImportElement importElement, @Nullable QualifiedName path) { myFile = file; myImportable = importable; myImportElement = importElement; @@ -59,18 +70,22 @@ class ImportCandidateHolder implements Comparable { assert importElement != null || path != null; // one of these must be present } + @NotNull public PsiElement getImportable() { return myImportable; } + @Nullable public PyImportElement getImportElement() { return myImportElement; } + @NotNull public PsiFileSystemItem getFile() { return myFile; } + @Nullable public QualifiedName getPath() { return myPath; } @@ -78,15 +93,17 @@ class ImportCandidateHolder implements Comparable { /** * Helper method that builds an import path, handling all these "import foo", "import foo as bar", "from bar import foo", etc. * Either importPath or importSource must be not null. - * @param name what is ultimately imported. + * + * @param name what is ultimately imported. * @param importPath known path to import the name. - * @param source known ImportElement to import the name; its 'as' clause is used if present. + * @param source known ImportElement to import the name; its 'as' clause is used if present. * @return a properly qualified name. */ - public static String getQualifiedName(String name, QualifiedName importPath, PyImportElement source) { - StringBuilder sb = new StringBuilder(); + @NotNull + public static String getQualifiedName(@NotNull String name, @Nullable QualifiedName importPath, @Nullable PyImportElement source) { + final StringBuilder sb = new StringBuilder(); if (source != null) { - PsiElement parent = source.getParent(); + final PsiElement parent = source.getParent(); if (parent instanceof PyFromImportStatement) { sb.append(name); } @@ -95,7 +112,7 @@ class ImportCandidateHolder implements Comparable { } } else { - if (importPath.getComponentCount() > 0) { + if (importPath != null && importPath.getComponentCount() > 0) { sb.append(importPath).append("."); } sb.append(name); @@ -103,8 +120,9 @@ class ImportCandidateHolder implements Comparable { return sb.toString(); } - public String getPresentableText(String myName) { - StringBuilder sb = new StringBuilder(getQualifiedName(myName, myPath, myImportElement)); + @NotNull + public String getPresentableText(@NotNull String myName) { + final StringBuilder sb = new StringBuilder(getQualifiedName(myName, myPath, myImportElement)); PsiElement parent = null; if (myImportElement != null) { parent = myImportElement.getParent(); @@ -113,13 +131,15 @@ class ImportCandidateHolder implements Comparable { sb.append(((PyFunction)myImportable).getParameterList().getPresentableText(false)); } else if (myImportable instanceof PyClass) { - PyClass[] supers = ((PyClass)myImportable).getSuperClasses(); + final PyClass[] supers = ((PyClass)myImportable).getSuperClasses(); if (supers.length > 0) { sb.append("("); // ", ".join(x.getName() for x in getSuperClasses()) - String[] super_names = new String[supers.length]; - for (int i=0; i < supers.length; i += 1) super_names[i] = supers[i].getName(); - sb.append(StringUtil.join(super_names, ", ")); + final String[] superNames = new String[supers.length]; + for (int i = 0; i < supers.length; i += 1) { + superNames[i] = supers[i].getName(); + } + sb.append(StringUtil.join(superNames, ", ")); sb.append(")"); } } @@ -135,10 +155,9 @@ class ImportCandidateHolder implements Comparable { return sb.toString(); } - public int compareTo(Object o) { - ImportCandidateHolder rhs = (ImportCandidateHolder) o; - int lRelevance = getRelevance(); - int rRelevance = rhs.getRelevance(); + public int compareTo(@NotNull ImportCandidateHolder rhs) { + final int lRelevance = getRelevance(); + final int rRelevance = rhs.getRelevance(); if (rRelevance != lRelevance) { return rRelevance - lRelevance; } @@ -150,7 +169,7 @@ class ImportCandidateHolder implements Comparable { } int getRelevance() { - Project project = myImportable.getProject(); + final Project project = myImportable.getProject(); final PsiFile psiFile = myImportable.getContainingFile(); final VirtualFile vFile = psiFile == null ? null : psiFile.getVirtualFile(); if (vFile == null) return 0; diff --git a/python/src/com/jetbrains/python/codeInsight/imports/ImportFromExistingAction.java b/python/src/com/jetbrains/python/codeInsight/imports/ImportFromExistingAction.java index 58254d999a90..e7a8151bf52d 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/ImportFromExistingAction.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/ImportFromExistingAction.java @@ -53,6 +53,7 @@ public class ImportFromExistingAction implements QuestionAction { String myName; boolean myUseQualifiedImport; private Runnable myOnDoneCallback; + private final boolean myImportLocally; /** * @param target element to become qualified as imported. @@ -60,12 +61,13 @@ public class ImportFromExistingAction implements QuestionAction { * @param name relevant name ot the target element (e.g. of identifier in an expression). * @param useQualified if True, use qualified "import modulename" instead of "from modulename import ...". */ - public ImportFromExistingAction(@NotNull PyElement target, @NotNull List<ImportCandidateHolder> sources, String name, - boolean useQualified) { + public ImportFromExistingAction(@NotNull PyElement target, @NotNull List<ImportCandidateHolder> sources, @NotNull String name, + boolean useQualified, boolean importLocally) { myTarget = target; mySources = sources; myName = name; myUseQualifiedImport = useQualified; + myImportLocally = importLocally; } public void onDone(Runnable callback) { @@ -151,25 +153,41 @@ public class ImportFromExistingAction implements QuestionAction { if (manager.isInjectedFragment(file)) { file = manager.getTopLevelFile(myTarget); } + // We are trying to import top-level module or package which thus cannot be qualified if (isRoot(item.getFile())) { - AddImportHelper.addImportStatement(file, myName, null, priority); + if (myImportLocally) { + AddImportHelper.addLocalImportStatement(myTarget, myName); + } else { + AddImportHelper.addImportStatement(file, myName, null, priority); + } } else { - String qualifiedName = item.getPath().toString(); + final String qualifiedName = item.getPath().toString(); if (myUseQualifiedImport) { String nameToImport = qualifiedName; if (item.getImportable() instanceof PsiFileSystemItem) { nameToImport += "." + myName; } - AddImportHelper.addImportStatement(file, nameToImport, null, priority); + if (myImportLocally) { + AddImportHelper.addLocalImportStatement(myTarget, nameToImport); + } + else { + AddImportHelper.addImportStatement(file, nameToImport, null, priority); + } myTarget.replace(gen.createExpressionFromText(LanguageLevel.forElement(myTarget), qualifiedName + "." + myName)); } else { - AddImportHelper.addImportFrom(file, myTarget, qualifiedName, myName, null, priority); + if (myImportLocally) { + AddImportHelper.addLocalFromImportStatement(myTarget, qualifiedName, myName); + } + else { + AddImportHelper.addImportFromStatement(file, qualifiedName, myName, null, priority); + } } } } + private void addToExistingImport(PyImportElement src) { final PyElementGenerator gen = PyElementGenerator.getInstance(myTarget.getProject()); // did user choose 'import' or 'from import'? diff --git a/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java b/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java index 5d24e3b89332..d5fd5a64495e 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java @@ -92,7 +92,7 @@ public class PyImportOptimizer implements ImportOptimizer { for (PyImportElement importElement : importStatement.getImportElements()) { myMissorted = true; PsiElement toImport = importElement.resolve(); - final PyImportStatement splitImport = myGenerator.createImportStatementFromText(langLevel, "import " + importElement.getText()); + final PyImportStatement splitImport = myGenerator.createImportStatement(langLevel, importElement.getText(), null); prioritize(splitImport, toImport); } } |