From bf982a42e0998f8167cd175d0020af8eb0682767 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Wed, 23 Jun 2010 21:36:16 +0400 Subject: [PATCH] class name completion adds class name to existing import statement if possible (PY-1003) --- .../actions/ImportFromExistingAction.java | 11 +--- .../PyClassNameCompletionContributor.java | 44 +++++++++++--- .../python/psi/PyElementGenerator.java | 3 +- .../psi/impl/PyElementGeneratorImpl.java | 7 ++- .../psi/impl/PyFromImportStatementImpl.java | 18 ++++++ .../className/qualified/mypackage/__init__.py | 3 + .../className/qualified/qualified.after.py | 3 + .../className/qualified/qualified.py | 1 + .../reuseExisting/mypackage/__init__.py | 6 ++ .../reuseExisting/reuseExisting.after.py | 3 + .../className/reuseExisting/reuseExisting.py | 3 + .../className/simple/mypackage/__init__.py | 3 + .../className/simple/simple.after.py | 3 + .../completion/className/simple/simple.py | 1 + .../python/PyClassNameCompletionTest.java | 59 +++++++++++++++++++ .../jetbrains/python/PythonAllTestsSuite.java | 3 +- 16 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 python/testData/completion/className/qualified/mypackage/__init__.py create mode 100644 python/testData/completion/className/qualified/qualified.after.py create mode 100644 python/testData/completion/className/qualified/qualified.py create mode 100644 python/testData/completion/className/reuseExisting/mypackage/__init__.py create mode 100644 python/testData/completion/className/reuseExisting/reuseExisting.after.py create mode 100644 python/testData/completion/className/reuseExisting/reuseExisting.py create mode 100644 python/testData/completion/className/simple/mypackage/__init__.py create mode 100644 python/testData/completion/className/simple/simple.after.py create mode 100644 python/testData/completion/className/simple/simple.py create mode 100644 python/testSrc/com/jetbrains/python/PyClassNameCompletionTest.java diff --git a/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java b/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java index fa74e3dedff5..6ac65f4abe00 100644 --- a/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java +++ b/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java @@ -10,17 +10,14 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.PopupChooserBuilder; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiNamedElement; import com.intellij.ui.SimpleColoredComponent; import com.intellij.ui.SimpleTextAttributes; import com.jetbrains.python.PyBundle; import com.jetbrains.python.psi.*; -import com.jetbrains.python.psi.resolve.ResolveImportUtil; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; -import java.util.Collections; import java.util.List; /** @@ -51,12 +48,6 @@ public class ImportFromExistingAction implements QuestionAction { myUseQualifiedImport = useQualified; } - public ImportFromExistingAction(@NotNull PyElement target, PsiNamedElement source, Editor editor, boolean useQualified) { - this(target, Collections.singletonList(new ImportCandidateHolder(source, source.getContainingFile(), null, - ResolveImportUtil.findShortestImportableName(target, source.getContainingFile().getVirtualFile()), null)), - source.getName(), editor, useQualified); - } - public void onDone(Runnable callback) { assert myOnDoneCallback == null; myOnDoneCallback = callback; @@ -120,7 +111,7 @@ public class ImportFromExistingAction implements QuestionAction { PsiElement parent = src.getParent(); if (parent instanceof PyFromImportStatement) { // add another import element right after the one we got - PsiElement new_elt = gen.createFromText(PyImportElement.class, "from foo import " + myName, new int[]{0, 6}); + PsiElement new_elt = gen.createImportElement(myName); PyUtil.addListNode(parent, new_elt, null, false, true); } else { // just 'import' diff --git a/python/src/com/jetbrains/python/codeInsight/PyClassNameCompletionContributor.java b/python/src/com/jetbrains/python/codeInsight/PyClassNameCompletionContributor.java index 05482457d9d4..bdf101a65a7d 100644 --- a/python/src/com/jetbrains/python/codeInsight/PyClassNameCompletionContributor.java +++ b/python/src/com/jetbrains/python/codeInsight/PyClassNameCompletionContributor.java @@ -4,22 +4,27 @@ import com.intellij.codeInsight.completion.*; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Iconable; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.PsiReference; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.ProjectScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.util.ProcessingContext; -import com.jetbrains.python.actions.ImportFromExistingAction; -import com.jetbrains.python.psi.PyClass; -import com.jetbrains.python.psi.PyElement; +import com.jetbrains.python.actions.AddImportHelper; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyQualifiedName; +import com.jetbrains.python.psi.resolve.ResolveImportUtil; import com.jetbrains.python.psi.stubs.PyClassNameIndex; import org.jetbrains.annotations.NotNull; import java.util.Collection; +import java.util.List; import static com.intellij.patterns.PlatformPatterns.psiElement; @@ -60,15 +65,40 @@ public class PyClassNameCompletionContributor extends CompletionContributor { } private static class PyClassNameInsertHandler implements InsertHandler { - public void handleInsert(InsertionContext context, LookupElement item) { + public void handleInsert(final InsertionContext context, final LookupElement item) { final PsiReference ref = context.getFile().findReferenceAt(context.getTailOffset() - 1); if (ref == null || ref.resolve() == item.getObject()) { // no import statement needed return; } - PyElement element = (PyElement) ref.getElement(); - boolean useQualified = !PyCodeInsightSettings.getInstance().PREFER_FROM_IMPORT; - new ImportFromExistingAction(element, (PsiNamedElement) item.getObject(), context.getEditor(), useQualified).execute(); + new WriteCommandAction(context.getProject(), context.getFile()) { + @Override + protected void run(Result result) throws Throwable { + addImport((PsiNamedElement) item.getObject(), context.getFile(), (PyElement) ref.getElement()); + } + }.execute(); + } + } + + private static void addImport(final PsiNamedElement target, final PsiFile file, final PyElement element) { + final boolean useQualified = !PyCodeInsightSettings.getInstance().PREFER_FROM_IMPORT; + final String path = ResolveImportUtil.findShortestImportableName(element, target.getContainingFile().getVirtualFile()); + final PyElementGenerator elementGenerator = PyElementGenerator.getInstance(file.getProject()); + if (useQualified) { + AddImportHelper.addImportStatement(file, path, null); + element.replace(elementGenerator.createExpressionFromText(path + "." + target.getName())); + } + else { + final List existingImports = ((PyFile)file).getFromImports(); + for (PyFromImportStatement existingImport : existingImports) { + final PyQualifiedName qName = existingImport.getImportSourceQName(); + if (qName != null && qName.toString().equals(path)) { + PyImportElement importElement = elementGenerator.createImportElement(target.getName()); + existingImport.add(importElement); + return; + } + } + AddImportHelper.addImportFromStatement(file, path, target.getName(), null); } } diff --git a/python/src/com/jetbrains/python/psi/PyElementGenerator.java b/python/src/com/jetbrains/python/psi/PyElementGenerator.java index 06b1305fe4c4..35ef87e86c20 100644 --- a/python/src/com/jetbrains/python/psi/PyElementGenerator.java +++ b/python/src/com/jetbrains/python/psi/PyElementGenerator.java @@ -5,7 +5,6 @@ import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiWhiteSpace; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -47,6 +46,8 @@ public abstract class PyElementGenerator { public abstract PyImportStatement createImportStatementFromText(String text); + public abstract PyImportElement createImportElement(String name); + public abstract T createFromText(Class aClass, final String text); /** diff --git a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java index 2d39745fda9d..b59b8f8f7e7b 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java @@ -2,12 +2,10 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileFactory; -import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.tree.TokenSet; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyTokenTypes; @@ -194,6 +192,11 @@ public class PyElementGeneratorImpl extends PyElementGenerator { return (PyImportStatement)dummyFile.getFirstChild(); } + @Override + public PyImportElement createImportElement(String name) { + return createFromText(PyImportElement.class, "from foo import " + name, new int[]{0, 6}); + } + static final int[] FROM_ROOT = new int[]{0}; public T createFromText(Class aClass, final String text) { diff --git a/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java index caa2d5b52fa2..7cbf12d88fa8 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java @@ -3,6 +3,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiElement; import com.intellij.psi.ResolveState; +import com.intellij.psi.TokenType; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.tree.TokenSet; import com.intellij.util.ArrayFactory; @@ -140,6 +141,23 @@ public class PyFromImportStatementImpl extends PyBaseElementImpl \ No newline at end of file diff --git a/python/testData/completion/className/reuseExisting/mypackage/__init__.py b/python/testData/completion/className/reuseExisting/mypackage/__init__.py new file mode 100644 index 000000000000..9d643a2bbdac --- /dev/null +++ b/python/testData/completion/className/reuseExisting/mypackage/__init__.py @@ -0,0 +1,6 @@ +class Xyzzy: + pass + +class Shazam: + pass + \ No newline at end of file diff --git a/python/testData/completion/className/reuseExisting/reuseExisting.after.py b/python/testData/completion/className/reuseExisting/reuseExisting.after.py new file mode 100644 index 000000000000..cf18f6e003d4 --- /dev/null +++ b/python/testData/completion/className/reuseExisting/reuseExisting.after.py @@ -0,0 +1,3 @@ +from mypackage import Xyzzy, Shazam + +Shazam \ No newline at end of file diff --git a/python/testData/completion/className/reuseExisting/reuseExisting.py b/python/testData/completion/className/reuseExisting/reuseExisting.py new file mode 100644 index 000000000000..29ce395cdd37 --- /dev/null +++ b/python/testData/completion/className/reuseExisting/reuseExisting.py @@ -0,0 +1,3 @@ +from mypackage import Xyzzy + +Shaz \ No newline at end of file diff --git a/python/testData/completion/className/simple/mypackage/__init__.py b/python/testData/completion/className/simple/mypackage/__init__.py new file mode 100644 index 000000000000..f87b9b33a94d --- /dev/null +++ b/python/testData/completion/className/simple/mypackage/__init__.py @@ -0,0 +1,3 @@ +class Shazam: + pass + \ No newline at end of file diff --git a/python/testData/completion/className/simple/simple.after.py b/python/testData/completion/className/simple/simple.after.py new file mode 100644 index 000000000000..63ae6a7e8b12 --- /dev/null +++ b/python/testData/completion/className/simple/simple.after.py @@ -0,0 +1,3 @@ +from mypackage import Shazam + +Shazam \ No newline at end of file diff --git a/python/testData/completion/className/simple/simple.py b/python/testData/completion/className/simple/simple.py new file mode 100644 index 000000000000..361cceaa9312 --- /dev/null +++ b/python/testData/completion/className/simple/simple.py @@ -0,0 +1 @@ +Shaz \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyClassNameCompletionTest.java b/python/testSrc/com/jetbrains/python/PyClassNameCompletionTest.java new file mode 100644 index 000000000000..357196902512 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/PyClassNameCompletionTest.java @@ -0,0 +1,59 @@ +package com.jetbrains.python; + +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.codeInsight.completion.CompletionType; +import com.jetbrains.python.codeInsight.PyCodeInsightSettings; +import com.jetbrains.python.fixtures.PyLightFixtureTestCase; + +/** + * @author yole + */ +public class PyClassNameCompletionTest extends PyLightFixtureTestCase { + private boolean myOldAutocompleteValue; + + @Override + protected String getTestDataPath() { + return PythonTestUtil.getTestDataPath() + "/completion/className/"; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final CodeInsightSettings codeInsightSettings = CodeInsightSettings.getInstance(); + myOldAutocompleteValue = codeInsightSettings.AUTOCOMPLETE_ON_CLASS_NAME_COMPLETION; + codeInsightSettings.AUTOCOMPLETE_ON_CLASS_NAME_COMPLETION = true; + } + + @Override + protected void tearDown() throws Exception { + CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CLASS_NAME_COMPLETION = myOldAutocompleteValue; + super.tearDown(); + } + + public void testSimple() throws Exception { + doTest(); + } + + public void testReuseExisting() throws Exception { + doTest(); + } + + public void testQualified() throws Exception { + final PyCodeInsightSettings settings = PyCodeInsightSettings.getInstance(); + boolean oldValue = settings.PREFER_FROM_IMPORT; + settings.PREFER_FROM_IMPORT = false; + try { + doTest(); + } + finally { + settings.PREFER_FROM_IMPORT = oldValue; + } + } + + private void doTest() throws Exception { + myFixture.copyDirectoryToProject(getTestName(true), ""); + myFixture.configureFromTempProjectFile(getTestName(true) + ".py"); + myFixture.complete(CompletionType.CLASS_NAME); + myFixture.checkResultByFile(getTestName(true) + "/" + getTestName(true) + ".after.py"); + } +} diff --git a/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java b/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java index d1e8e9efd707..5c160aa3c2f4 100644 --- a/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java +++ b/python/testSrc/com/jetbrains/python/PythonAllTestsSuite.java @@ -54,7 +54,8 @@ public class PythonAllTestsSuite { PyOptimizeImportsTest.class, PySmartEnterTest.class, PyStatementMoverTest.class, - PyIntroduceVariableTest.class + PyIntroduceVariableTest.class, + PyClassNameCompletionTest.class }; public static TestSuite suite() {