From 85c3e995a5d590023586fb05bdea68601a5603e2 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Wed, 17 Feb 2016 00:11:32 +0300 Subject: [PATCH] Django forms and formset support added * "for" django tag supports iteration * django forms and formset typing added * Tests added --- .../python/codeInsight/PyCustomMember.java | 35 +++++++++++- .../codeInsight/PyCustomMemberTypeInfo.java | 55 +++++++++++++++++++ .../nameResolver/NameResolverTools.java | 27 +++++++-- .../psi/impl/PyTargetExpressionImpl.java | 4 +- .../psi/types/PyCollectionTypeImpl.java | 17 +++++- .../jetbrains/python/fixtures/PyTestCase.java | 27 +++++++++ 6 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMemberTypeInfo.java diff --git a/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMember.java b/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMember.java index 543d635050d5..488d6038b997 100644 --- a/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMember.java +++ b/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMember.java @@ -15,6 +15,7 @@ */ package com.jetbrains.python.codeInsight; +import com.google.common.base.Preconditions; import com.intellij.extapi.psi.ASTWrapperPsiElement; import com.intellij.icons.AllIcons; import com.intellij.openapi.util.Key; @@ -27,6 +28,7 @@ import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyFunction; import com.jetbrains.python.psi.PyPsiFacade; import com.jetbrains.python.psi.PyTypedElement; +import com.jetbrains.python.psi.types.PyClassType; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; @@ -35,6 +37,9 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; /** + * Note: if you use {@link #myTypeName} to override real field, be sure to use + * {@link com.jetbrains.python.psi.types.PyOverridingClassMembersProvider} + * * @author Dennis.Ushakov */ public class PyCustomMember extends UserDataHolderBase { @@ -55,6 +60,8 @@ public class PyCustomMember extends UserDataHolderBase { * Force resolving to {@link MyInstanceElement} even if element is function */ private boolean myAlwaysResolveToCustomElement; + private Icon myIcon = AllIcons.Nodes.Method; + private PyCustomMemberTypeInfo myCustomTypeInfo; public PyCustomMember(@NotNull final String name, @Nullable final String type, final boolean resolveToInstance) { myName = name; @@ -166,7 +173,7 @@ public class PyCustomMember extends UserDataHolderBase { if (myTarget != null) { return myTarget.getIcon(0); } - return AllIcons.Nodes.Method; + return myIcon; } @Nullable @@ -241,6 +248,26 @@ public class PyCustomMember extends UserDataHolderBase { return ((MyInstanceElement)element).getThis().equals(this); } + /** + * @param icon icon to use (will be used method icon otherwise) + */ + public PyCustomMember withIcon(@NotNull final Icon icon) { + myIcon = icon; + return this; + } + + /** + * Adds custom info to type if class has {@link #myTypeName} set. + * Info could be later obtained by key. + * + * @param customInfo custom info to add + */ + public PyCustomMember withCustomTypeInfo(@NotNull final PyCustomMemberTypeInfo customInfo) { + Preconditions.checkState(myTypeName != null, "Cant add custom type info if no type provided"); + myCustomTypeInfo = customInfo; + return this; + } + private class MyInstanceElement extends ASTWrapperPsiElement implements PyTypedElement { private final PyClass myClass; private final PsiElement myContext; @@ -260,7 +287,11 @@ public class PyCustomMember extends UserDataHolderBase { return myTypeCallback.fun(myContext); } else if (myClass != null) { - return PyPsiFacade.getInstance(getProject()).createClassType(myClass, !myResolveToInstance); + final PyClassType type = PyPsiFacade.getInstance(getProject()).createClassType(myClass, !myResolveToInstance); + if (myCustomTypeInfo != null) { + myCustomTypeInfo.fill(type); + } + return type; } return null; } diff --git a/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMemberTypeInfo.java b/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMemberTypeInfo.java new file mode 100644 index 000000000000..5fda8d4d2393 --- /dev/null +++ b/python/psi-api/src/com/jetbrains/python/codeInsight/PyCustomMemberTypeInfo.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.codeInsight; + +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.UserDataHolder; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Info to add to type of custom member. + * + * @author Ilya.Kazakevich + */ +public class PyCustomMemberTypeInfo { + @NotNull + private final Map, K> myCustomInfo = new HashMap, K>(); + + public PyCustomMemberTypeInfo(@NotNull final Key key, @NotNull final K value) { + this(Collections.singleton(Pair.create(key, value))); + } + + public PyCustomMemberTypeInfo(@NotNull final Iterable, K>> customInfo) { + for (final Pair, K> pair : customInfo) { + myCustomInfo.put(pair.first, pair.second); + } + } + + public PyCustomMemberTypeInfo(@NotNull final Map, K> customInfo) { + myCustomInfo.putAll(customInfo); + } + + void fill(@NotNull final UserDataHolder typeToFill) { + for (final Map.Entry, K> entry : myCustomInfo.entrySet()) { + typeToFill.putUserData(entry.getKey(), entry.getValue()); + } + } +} diff --git a/python/psi-api/src/com/jetbrains/python/nameResolver/NameResolverTools.java b/python/psi-api/src/com/jetbrains/python/nameResolver/NameResolverTools.java index 4c1bd04080ab..ea1e3a479662 100644 --- a/python/psi-api/src/com/jetbrains/python/nameResolver/NameResolverTools.java +++ b/python/psi-api/src/com/jetbrains/python/nameResolver/NameResolverTools.java @@ -27,6 +27,7 @@ import com.intellij.psi.util.QualifiedName; import com.intellij.util.Function; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -108,8 +109,8 @@ public final class NameResolverTools { * Same as {@link #isName(PyElement, FQNamesProvider...)} for call expr, but first checks name. * Aliases not supported, but much lighter that way * - * @param call expr - * @param function names to check + * @param call expr + * @param function names to check * @return true if callee is correct */ public static boolean isCalleeShortCut(@NotNull final PyCallExpression call, @@ -142,7 +143,8 @@ public final class NameResolverTools { /** * Checks if some string contains last component one of name - * @param text test to check + * + * @param text test to check * @param names */ public static boolean isContainsName(@NotNull final String text, @NotNull final FQNamesProvider names) { @@ -153,15 +155,32 @@ public final class NameResolverTools { } return false; } + /** * Checks if some file contains last component one of name - * @param file file to check + * + * @param file file to check * @param names */ public static boolean isContainsName(@NotNull final PsiFile file, @NotNull final FQNamesProvider names) { return isContainsName(file.getText(), names); } + /** + * Check if class has parent with some name + * @param child class to check + */ + public static boolean isSubclass(@NotNull final PyClass child, + @NotNull final FQNamesProvider parentName, + @NotNull final TypeEvalContext context) { + for (final String nameToCheck : parentName.getNames()) { + if (child.isSubclass(nameToCheck, context)) { + return true; + } + } + return false; + } + /** * Looks for call of some function */ diff --git a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java index c9bf4b266bcd..5f68c45df0bd 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java @@ -312,7 +312,7 @@ public class PyTargetExpressionImpl extends PyBaseElementImplemptyMap(), context); } diff --git a/python/src/com/jetbrains/python/psi/types/PyCollectionTypeImpl.java b/python/src/com/jetbrains/python/psi/types/PyCollectionTypeImpl.java index 933df88216f9..8e6b315eb866 100644 --- a/python/src/com/jetbrains/python/psi/types/PyCollectionTypeImpl.java +++ b/python/src/com/jetbrains/python/psi/types/PyCollectionTypeImpl.java @@ -34,6 +34,16 @@ public class PyCollectionTypeImpl extends PyClassTypeImpl implements PyCollectio myElementTypes = elementTypes; } + + @Nullable + @Override + public PyType getReturnType(@NotNull final TypeEvalContext context) { + if (isDefinition()) { + return new PyCollectionTypeImpl(getPyClass(), false, myElementTypes); + } + return null; + } + @NotNull @Override public List getElementTypes(@NotNull TypeEvalContext context) { @@ -52,6 +62,11 @@ public class PyCollectionTypeImpl extends PyClassTypeImpl implements PyCollectio return new PyCollectionTypeImpl(pyClass, isDefinition, elementTypes); } + @Override + public PyClassType toInstance() { + return myIsDefinition ? new PyCollectionTypeImpl(myClass, false, myElementTypes) : this; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -81,4 +96,4 @@ public class PyCollectionTypeImpl extends PyClassTypeImpl implements PyCollectio } return result; } -} +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java index ae436c2f45d2..f9381c9fc09c 100644 --- a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java +++ b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java @@ -17,6 +17,8 @@ package com.jetbrains.python.fixtures; import com.google.common.base.Joiner; import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupEx; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ex.QuickFixWrapper; import com.intellij.execution.actions.ConfigurationContext; @@ -439,5 +441,30 @@ public abstract class PyTestCase extends UsefulTestCase { //noinspection ConstantConditions return getCommonCodeStyleSettings().getIndentOptions(); } + + /** + * When you have more than one completion variant, you may use this method providing variant to choose. + * It only works for one caret (multiple carets not supported) and since it puts tab after completion, be sure to limit + * line somehow (i.e. with comment). + *
+ * Example: "user.n[caret]." There are "name" and "nose" fields. + * By calling this function with "nose" you will end with "user.nose ". + */ + protected final void completeCaretWithMultipleVariants(@NotNull final String... desiredVariants) { + final LookupElement[] lookupElements = myFixture.completeBasic(); + final LookupEx lookup = myFixture.getLookup(); + if (lookupElements != null && lookupElements.length > 1) { + // More than one element returned, check directly because completion can't work in this case + for (final LookupElement element : lookupElements) { + final String suggestedString = element.getLookupString(); + if (Arrays.asList(desiredVariants).contains(suggestedString)) { + myFixture.getLookup().setCurrentItem(element); + lookup.setCurrentItem(element); + myFixture.completeBasicAllCarets('\t'); + return; + } + } + } + } }