mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
PY-62208 Include importable names in basic completion results
Previously, such names were visible only on so-called "extended" completion, activated when the hotkey for the basic completion was hit twice. The main reason was that collecting such variants from indexes was a slow process, and we didn't want to harm the responsiveness of completion for basic names. Now it becomes possible thanks to a number of performance optimizations: * Instead of using three separate indexes for classes, functions and variables, we use one -- PyExportedModuleAttributeIndex. By definition, it includes only top-level "importable" names, so we additionally save time by not filtering out irrelevant entries. Also, it doesn't contain private definitions starting with an underscore. It might bother some users, but given that the previous completion was used extremely rarely, and the new one is going to be visible everywhere, it seems that pruning unlikely entries as much as possible is a fare tradeoff. In the future, we might enable them back on the "extended" completion if there is a demand. Also, this index binds its keys to the project (`traceKeyHashToVirtualFileMapping`), further eliminating useless index lookups. * Thanks to the recent fixes in the platform (IJPL-265), it's now possible to simultaneously iterate over all keys in an index and request values for a given key without deadlocks, which is much faster than eagerly fetching all keys first. * While scanning through all matching entries from indexes, we terminate the lookup if the number of items exceeds the size of the lookup list. We can further reduce this number by adjusting the "ide.completion.variant.limit" registry value. * Calculating expensive "canonical" import paths (e.g. "pkg.private.Name" is importable as "pkg.Name") is offloaded to a background thread thanks to the `withExpensiveRenderer` API. We still calculate these paths synchronously, though, for names whose raw qualified names contain components starting with an underscore to decide whether these private names are publicly re-exported and, hence, should be displayed. The rest of the work has been put into reducing the number of entries on the list, e.g. * The prefix under caret is now matched from the beginning of a name, e.g. `Bar<caret>` matches `BarBaz`, but not `FooBar`. * We don't suggest imported names clashing with those already available in scope. * Some kinds of definitions are not suggested in specific contexts, e.g. functions and variables are not suggested inside patterns and type hints. * Nothing is suggested at the top-level of a class body, where dangling reference expressions or calls are not normally expected. Additionally, we don't suggest names from .pyi stubs at the moment, because it pollutes the suggestion list with entries coming from the stubs for third-party packages in Typeshed. We should probably enable them back once we are able to properly disable Typeshed entries for not installed packages. Some legacy forms of completion are left in the extended mode. In particular, qualified names of classes are offered inside string literals only in this mode. Also, module and package names are suggested only in the extended mode, because top-level packages and modules are already suggested for the basic completion by PyModuleNameCompletionContributor. A few tests in PyClassNameCompletionTest were updated or removed entirely because * we no longer suggest private names * we no longer suggest names from private modules not re-exported in a public module * we no longer suggest names clashing with those already available in scope * prefix matching policy was changed to start at the beginning of an identifier The whole feature can be disabled with the option "Suggest importable classes, functions and variables in basic completion" in settings. GitOrigin-RevId: 0787d42ce337b73b01a60f0bb7aa434fee43e659
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7efc1bb1f9
commit
52850e21d8
@@ -361,6 +361,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
<editorSmartKeysConfigurable instance="com.jetbrains.python.codeInsight.PySmartKeysOptions" id="editor.preferences.pyOptions"
|
||||
bundle="messages.PyBundle"
|
||||
key="configurable.PySmartKeysOptions.display.name"/>
|
||||
<codeCompletionConfigurable instance="com.jetbrains.python.codeInsight.completion.PythonCodeCompletionConfigurable"/>
|
||||
|
||||
|
||||
<psi.referenceContributor implementation="com.jetbrains.python.codeInsight.PyConsoleFileReferenceContributor" language="Python"
|
||||
|
||||
@@ -1511,6 +1511,11 @@ live.template.iter.description=Iterate (for ... in ...)
|
||||
live.template.main.description=if __name__ == '__main__'
|
||||
live.template.super.description='super(...)' call
|
||||
|
||||
configurable.PythonCodeCompletionConfigurable.display.name.python=Python
|
||||
configurable.PythonCodeCompletionConfigurable.border.title=Python
|
||||
configurable.PythonCodeCompletionConfigurable.checkbox.suggest.importable.names=Suggest importable classes, functions and variables in basic completion
|
||||
configurable.PythonCodeCompletionConfigurable.checkbox.suggest.importable.names.help=When disabled, such variants can be displayed by invoking the basic completion twice
|
||||
|
||||
# Parameter info
|
||||
param.info.show.less=Show less
|
||||
param.info.show.more.n.overloads=Show {0} more {1, choice, 0#overloads|1#overload}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.intellij.openapi.vfs.StandardFileSystems;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.jetbrains.python.codeInsight.PyCodeInsightSettings;
|
||||
import com.jetbrains.python.codeInsight.completion.PyModuleNameCompletionContributor;
|
||||
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
|
||||
import com.jetbrains.python.fixture.PythonCommonTestCase;
|
||||
@@ -19,10 +20,7 @@ import com.jetbrains.python.sdk.PythonSdkUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
public abstract class PythonCommonCompletionTest extends PythonCommonTestCase {
|
||||
|
||||
@@ -241,9 +239,11 @@ public abstract class PythonCommonCompletionTest extends PythonCommonTestCase {
|
||||
}
|
||||
|
||||
public void testStarImport() {
|
||||
myFixture.configureByFiles("starImport/starImport.py", "starImport/importSource.py");
|
||||
myFixture.completeBasic();
|
||||
assertSameElements(myFixture.getLookupElementStrings(), Arrays.asList("my_foo", "my_bar"));
|
||||
runWithImportableNamesInBasicCompletionDisabled(() -> {
|
||||
myFixture.configureByFiles("starImport/starImport.py", "starImport/importSource.py");
|
||||
myFixture.completeBasic();
|
||||
assertSameElements(myFixture.getLookupElementStrings(), Arrays.asList("my_foo", "my_bar"));
|
||||
});
|
||||
}
|
||||
|
||||
// PY-1211, PY-29232
|
||||
@@ -1678,10 +1678,12 @@ public abstract class PythonCommonCompletionTest extends PythonCommonTestCase {
|
||||
|
||||
// PY-8302
|
||||
public void testBeforeImport() {
|
||||
myFixture.configureByFiles("beforeImport/beforeImport.py", "beforeImport/source.py");
|
||||
myFixture.completeBasic();
|
||||
List<String> suggested = myFixture.getLookupElementStrings();
|
||||
assertDoesntContain(suggested, "my_foo", "my_bar");
|
||||
runWithImportableNamesInBasicCompletionDisabled(() -> {
|
||||
myFixture.configureByFiles("beforeImport/beforeImport.py", "beforeImport/source.py");
|
||||
myFixture.completeBasic();
|
||||
List<String> suggested = myFixture.getLookupElementStrings();
|
||||
assertDoesntContain(suggested, "my_foo", "my_bar");
|
||||
});
|
||||
}
|
||||
|
||||
// PY-8302
|
||||
@@ -1694,10 +1696,12 @@ public abstract class PythonCommonCompletionTest extends PythonCommonTestCase {
|
||||
|
||||
// PY-8302
|
||||
public void testBeforeStarImport() {
|
||||
myFixture.configureByFiles("beforeImport/beforeStarImport.py", "beforeImport/source.py");
|
||||
myFixture.completeBasic();
|
||||
List<String> suggested = myFixture.getLookupElementStrings();
|
||||
assertDoesntContain(suggested, "my_foo", "my_bar");
|
||||
runWithImportableNamesInBasicCompletionDisabled(() -> {
|
||||
myFixture.configureByFiles("beforeImport/beforeStarImport.py", "beforeImport/source.py");
|
||||
myFixture.completeBasic();
|
||||
List<String> suggested = myFixture.getLookupElementStrings();
|
||||
assertDoesntContain(suggested, "my_foo", "my_bar");
|
||||
});
|
||||
}
|
||||
|
||||
// PY-8302
|
||||
@@ -2167,6 +2171,101 @@ public abstract class PythonCommonCompletionTest extends PythonCommonTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableNamesNotSuggestedImmediatelyInsideClassBody() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableNamesSuggestedInsideOtherStatementsInsideClassBody() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableNamesNotSuggestedImmediatelyInsideMatchStatement() {
|
||||
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
|
||||
doMultiFileTest();
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableFunctionsAndVariablesNotSuggestedInsideTypeHints() {
|
||||
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
|
||||
doMultiFileTest();
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableFunctionsFromTypingSuggestedInsideTypeHints() {
|
||||
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
|
||||
doMultiFileTest();
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableVariablesFromTypingSuggestedInsideTypeHints() {
|
||||
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
|
||||
doMultiFileTest();
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testImportableFunctionsAndVariablesNotSuggestedInsidePatterns() {
|
||||
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
|
||||
myFixture.copyDirectoryToProject(getTestName(true), "");
|
||||
myFixture.configureByFile("a.py");
|
||||
myFixture.complete(CompletionType.BASIC, 1);
|
||||
List<String> variants = myFixture.getLookupElementStrings();
|
||||
// TODO Use regular doMultiFileTest once PY-73173 is fixed
|
||||
assertDoesntContain(variants, "unique_var", "unique_func");
|
||||
assertContainsElements(variants, "unique_class");
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testNotReExportedNamesFromPrivateModulesNotSuggested() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testReExportedNamesFromPrivateModulesAreSuggested() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testAlreadyImportedNamesNotSuggestedTwice() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testAlreadyImportedNamesNotSuggestedTwiceInsidePatterns() {
|
||||
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
|
||||
myFixture.copyDirectoryToProject(getTestName(true), "");
|
||||
myFixture.configureByFile("a.py");
|
||||
myFixture.complete(CompletionType.BASIC, 1);
|
||||
List<String> variants = myFixture.getLookupElementStrings();
|
||||
// TODO Use regular doMultiFileTest once PY-73173 is fixed
|
||||
assertEquals(1, Collections.frequency(variants, "MyClass"));
|
||||
});
|
||||
}
|
||||
|
||||
// PY-62208
|
||||
public void testTooCommonImportableNamesNotSuggested() {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
private static void runWithImportableNamesInBasicCompletionDisabled(@NotNull Runnable action) {
|
||||
PyCodeInsightSettings settings = PyCodeInsightSettings.getInstance();
|
||||
boolean old = settings.INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION;
|
||||
settings.INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION = false;
|
||||
try {
|
||||
action.run();
|
||||
}
|
||||
finally {
|
||||
settings.INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION = old;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull String getTestDataPath() {
|
||||
return super.getTestDataPath() + "/completion";
|
||||
|
||||
@@ -7,135 +7,234 @@ import com.intellij.codeInsight.completion.InsertHandler;
|
||||
import com.intellij.codeInsight.completion.PrioritizedLookupElement;
|
||||
import com.intellij.codeInsight.lookup.LookupElement;
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder;
|
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation;
|
||||
import com.intellij.codeInsight.lookup.LookupElementRenderer;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.util.Conditions;
|
||||
import com.intellij.openapi.util.registry.Registry;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiErrorElement;
|
||||
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.stubs.StubIndex;
|
||||
import com.intellij.psi.stubs.StubIndexKey;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.psi.util.QualifiedName;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.TimeoutUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.codeInsight.PyCodeInsightSettings;
|
||||
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
|
||||
import com.jetbrains.python.psi.search.PySearchUtilBase;
|
||||
import com.jetbrains.python.psi.stubs.PyClassNameIndex;
|
||||
import com.jetbrains.python.psi.stubs.PyFunctionNameIndex;
|
||||
import com.jetbrains.python.psi.stubs.PyVariableNameIndex;
|
||||
import com.jetbrains.python.psi.stubs.PyExportedModuleAttributeIndex;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import com.jetbrains.python.pyi.PyiFileType;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
|
||||
/**
|
||||
* Adds completion variants for Python classes, functions and variables.
|
||||
*/
|
||||
public final class PyClassNameCompletionContributor extends PyExtendedCompletionContributor {
|
||||
public final class PyClassNameCompletionContributor extends PyImportableNameCompletionContributor {
|
||||
// See https://plugins.jetbrains.com/plugin/18465-sputnik
|
||||
private static final boolean TRACING_WITH_SPUTNIK_ENABLED = false;
|
||||
private static final Logger LOG = Logger.getInstance(PyClassNameCompletionContributor.class);
|
||||
private static final Set<String> TOO_COMMON_NAMES = Set.of("main", "test");
|
||||
|
||||
public PyClassNameCompletionContributor() {
|
||||
if (TRACING_WITH_SPUTNIK_ENABLED) {
|
||||
//noinspection UseOfSystemOutOrSystemErr
|
||||
System.out.println("\01hr('Importable names completion')");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
|
||||
final PsiFile originalFile = parameters.getOriginalFile();
|
||||
final PsiElement element = parameters.getPosition();
|
||||
final PsiElement parent = element.getParent();
|
||||
final ScopeOwner originalScope = ScopeUtil.getScopeOwner(parameters.getOriginalPosition());
|
||||
final Condition<PsiElement> fromAnotherScope = e -> ScopeUtil.getScopeOwner(e) != originalScope;
|
||||
if (!PyCodeInsightSettings.getInstance().INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION && !parameters.isExtendedCompletion()) {
|
||||
return;
|
||||
}
|
||||
PsiFile originalFile = parameters.getOriginalFile();
|
||||
PsiElement position = parameters.getPosition();
|
||||
PyReferenceExpression refExpr = as(position.getParent(), PyReferenceExpression.class);
|
||||
PyTargetExpression targetExpr = as(position.getParent(), PyTargetExpression.class);
|
||||
boolean insideUnqualifiedReference = refExpr != null && !refExpr.isQualified();
|
||||
boolean insidePattern = targetExpr != null && position.getParent().getParent() instanceof PyCapturePattern;
|
||||
boolean insideStringLiteralInExtendedCompletion = position instanceof PyStringElement && parameters.isExtendedCompletion();
|
||||
if (!(insideUnqualifiedReference || insidePattern || insideStringLiteralInExtendedCompletion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
addVariantsFromIndex(result,
|
||||
originalFile,
|
||||
PyClassNameIndex.KEY,
|
||||
parent instanceof PyStringLiteralExpression ? getStringLiteralInsertHandler() : getImportingInsertHandler(),
|
||||
// TODO: implement autocompletion for inner classes
|
||||
Conditions.and(fromAnotherScope, PyUtil::isTopLevel),
|
||||
PyClass.class,
|
||||
createClassElementHandler(originalFile));
|
||||
// Directly inside the class body scope, it's rarely needed to have expression statements
|
||||
// TODO apply the same logic for completion of importable module and package names
|
||||
if (refExpr != null &&
|
||||
(isDirectlyInsideClassBody(refExpr) || isInsideErrorElement(refExpr))) {
|
||||
return;
|
||||
}
|
||||
// TODO Use another method to collect already visible names
|
||||
// Candidates: PyExtractMethodValidator, IntroduceValidator.isDefinedInScope
|
||||
PsiReference refUnderCaret = refExpr != null ? refExpr.getReference() :
|
||||
targetExpr != null ? targetExpr.getReference() :
|
||||
null;
|
||||
Set<String> namesInScope = refUnderCaret == null ? Collections.emptySet() : StreamEx.of(refUnderCaret.getVariants())
|
||||
.select(LookupElement.class)
|
||||
.map(LookupElement::getLookupString)
|
||||
.toSet();
|
||||
Project project = originalFile.getProject();
|
||||
TypeEvalContext typeEvalContext = TypeEvalContext.codeCompletion(project, originalFile);
|
||||
int maxVariants = Registry.intValue("ide.completion.variant.limit");
|
||||
Counters counters = new Counters();
|
||||
StubIndex stubIndex = StubIndex.getInstance();
|
||||
TimeoutUtil.run(() -> {
|
||||
GlobalSearchScope scope = createScope(originalFile);
|
||||
Set<QualifiedName> alreadySuggested = new HashSet<>();
|
||||
StubIndex.getInstance().processAllKeys(PyExportedModuleAttributeIndex.KEY, elementName -> {
|
||||
ProgressManager.checkCanceled();
|
||||
counters.scannedNames++;
|
||||
if (TOO_COMMON_NAMES.contains(elementName)) return true;
|
||||
if (!result.getPrefixMatcher().isStartMatch(elementName)) return true;
|
||||
return stubIndex.processElements(PyExportedModuleAttributeIndex.KEY, elementName, project, scope, PyElement.class, exported -> {
|
||||
ProgressManager.checkCanceled();
|
||||
String name = exported.getName();
|
||||
if (name == null || namesInScope.contains(name)) return true;
|
||||
QualifiedName fqn = getFullyQualifiedName(exported);
|
||||
if (!isApplicableInInsertionContext(exported, fqn, position, typeEvalContext)) {
|
||||
counters.notApplicableInContext++;
|
||||
return true;
|
||||
}
|
||||
if (alreadySuggested.add(fqn)) {
|
||||
if (isPrivateDefinition(fqn, exported, originalFile)) {
|
||||
counters.privateNames++;
|
||||
return true;
|
||||
}
|
||||
LookupElementBuilder lookupElement = LookupElementBuilder
|
||||
.createWithSmartPointer(name, exported)
|
||||
.withIcon(exported.getIcon(0))
|
||||
.withExpensiveRenderer(new LookupElementRenderer<>() {
|
||||
@Override
|
||||
public void renderElement(LookupElement element, LookupElementPresentation presentation) {
|
||||
presentation.setItemText(element.getLookupString());
|
||||
presentation.setIcon(exported.getIcon(0));
|
||||
QualifiedName importPath = QualifiedNameFinder.findCanonicalImportPath(exported, originalFile);
|
||||
if (importPath == null) return;
|
||||
presentation.setTypeText(importPath.toString());
|
||||
}
|
||||
})
|
||||
.withInsertHandler(getInsertHandler(exported, position));
|
||||
result.addElement(PrioritizedLookupElement.withPriority(lookupElement, PythonCompletionWeigher.NOT_IMPORTED_MODULE_WEIGHT));
|
||||
counters.totalVariants++;
|
||||
if (counters.totalVariants >= maxVariants) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, scope);
|
||||
}, duration -> {
|
||||
LOG.debug(counters + " computed in " + duration + " ms");
|
||||
if (TRACING_WITH_SPUTNIK_ENABLED) {
|
||||
//noinspection UseOfSystemOutOrSystemErr
|
||||
System.out.println("\1h('Importable names completion','%d')".formatted((duration / 10) * 10));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addVariantsFromIndex(result,
|
||||
originalFile,
|
||||
PyFunctionNameIndex.KEY,
|
||||
getFunctionInsertHandler(parent),
|
||||
Conditions.and(fromAnotherScope, PyUtil::isTopLevel),
|
||||
PyFunction.class,
|
||||
Function.identity());
|
||||
private static boolean isApplicableInInsertionContext(@NotNull PyElement definition,
|
||||
@NotNull QualifiedName fqn, @NotNull PsiElement position,
|
||||
@NotNull TypeEvalContext context) {
|
||||
if (PyTypingTypeProvider.isInsideTypeHint(position, context)) {
|
||||
// Not all names from typing.py are defined as classes
|
||||
return definition instanceof PyClass || ArrayUtil.contains(fqn.getFirstComponent(), "typing", "typing_extensions");
|
||||
}
|
||||
if (PsiTreeUtil.getParentOfType(position, PyPattern.class, false) != null) {
|
||||
return definition instanceof PyClass;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
addVariantsFromIndex(result,
|
||||
originalFile,
|
||||
PyVariableNameIndex.KEY,
|
||||
parent instanceof PyStringLiteralExpression ? getStringLiteralInsertHandler() : getImportingInsertHandler(),
|
||||
Conditions.and(fromAnotherScope, PyUtil::isTopLevel),
|
||||
PyTargetExpression.class,
|
||||
Function.identity());
|
||||
private static boolean isInsideErrorElement(@NotNull PyReferenceExpression referenceExpression) {
|
||||
return PsiTreeUtil.getParentOfType(referenceExpression, PsiErrorElement.class) != null;
|
||||
}
|
||||
|
||||
private static boolean isDirectlyInsideClassBody(@NotNull PyReferenceExpression referenceExpression) {
|
||||
return referenceExpression.getParent() instanceof PyExpressionStatement statement &&
|
||||
ScopeUtil.getScopeOwner(statement) instanceof PyClass;
|
||||
}
|
||||
|
||||
private static @NotNull QualifiedName getFullyQualifiedName(@NotNull PyElement exported) {
|
||||
String shortName = StringUtil.notNullize(exported.getName());
|
||||
String qualifiedName = exported instanceof PyQualifiedNameOwner qNameOwner ? qNameOwner.getQualifiedName() : null;
|
||||
return QualifiedName.fromDottedString(qualifiedName != null ? qualifiedName : shortName);
|
||||
}
|
||||
|
||||
private static boolean isPrivateDefinition(@NotNull QualifiedName fqn, @NotNull PyElement exported, PsiFile originalFile) {
|
||||
if (containsPrivateComponents(fqn)) {
|
||||
QualifiedName importPath = QualifiedNameFinder.findCanonicalImportPath(exported, originalFile);
|
||||
return importPath != null && containsPrivateComponents(importPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean containsPrivateComponents(@NotNull QualifiedName fqn) {
|
||||
return ContainerUtil.exists(fqn.getComponents(), c -> c.startsWith("_"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Function<LookupElement, LookupElement> createClassElementHandler(@NotNull PsiFile file) {
|
||||
final PyFile pyFile = PyUtil.as(file, PyFile.class);
|
||||
if (pyFile == null) return Function.identity();
|
||||
private static GlobalSearchScope createScope(@NotNull PsiFile originalFile) {
|
||||
class HavingLegalImportPathScope extends QualifiedNameFinder.QualifiedNameBasedScope {
|
||||
private HavingLegalImportPathScope(@NotNull Project project) {
|
||||
super(project);
|
||||
}
|
||||
|
||||
final Set<QualifiedName> sourceQNames =
|
||||
ContainerUtil.map2SetNotNull(pyFile.getFromImports(), PyFromImportStatement::getImportSourceQName);
|
||||
@Override
|
||||
protected boolean containsQualifiedNameInRoot(@NotNull VirtualFile root, @NotNull QualifiedName qName) {
|
||||
return ContainerUtil.all(qName.getComponents(), PyNames::isIdentifier) && !qName.equals(QualifiedName.fromComponents("__future__"));
|
||||
}
|
||||
}
|
||||
|
||||
return le -> {
|
||||
final PyClass cls = PyUtil.as(le.getPsiElement(), PyClass.class);
|
||||
if (cls == null) return le;
|
||||
|
||||
final String clsQName = cls.getQualifiedName();
|
||||
if (clsQName == null) return le;
|
||||
|
||||
if (!sourceQNames.contains(QualifiedName.fromDottedString(clsQName).removeLastComponent())) return le;
|
||||
|
||||
return PrioritizedLookupElement.withPriority(le, PythonCompletionWeigher.PRIORITY_WEIGHT);
|
||||
};
|
||||
Project project = originalFile.getProject();
|
||||
var pyiStubsScope = GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.everythingScope(project), PyiFileType.INSTANCE);
|
||||
return PySearchUtilBase.defaultSuggestionScope(originalFile)
|
||||
.intersectWith(GlobalSearchScope.notScope(pyiStubsScope))
|
||||
.intersectWith(GlobalSearchScope.notScope(GlobalSearchScope.fileScope(originalFile)))
|
||||
.intersectWith(new HavingLegalImportPathScope(project));
|
||||
}
|
||||
|
||||
private InsertHandler<LookupElement> getFunctionInsertHandler(PsiElement parent) {
|
||||
if (parent instanceof PyStringLiteralExpression) {
|
||||
private @NotNull InsertHandler<LookupElement> getInsertHandler(@NotNull PyElement exported,
|
||||
@NotNull PsiElement position) {
|
||||
if (position.getParent() instanceof PyStringLiteralExpression) {
|
||||
return getStringLiteralInsertHandler();
|
||||
}
|
||||
if (parent.getParent() instanceof PyDecorator) {
|
||||
return getImportingInsertHandler();
|
||||
else if (exported instanceof PyFunction && !(position.getParent().getParent() instanceof PyDecorator)) {
|
||||
return getFunctionInsertHandler();
|
||||
}
|
||||
return getFunctionInsertHandler();
|
||||
return getImportingInsertHandler();
|
||||
}
|
||||
|
||||
private static <T extends PsiNamedElement> void addVariantsFromIndex(@NotNull CompletionResultSet resultSet,
|
||||
@NotNull PsiFile targetFile,
|
||||
@NotNull StubIndexKey<String, T> indexKey,
|
||||
@NotNull InsertHandler<LookupElement> insertHandler,
|
||||
@NotNull Condition<? super T> condition,
|
||||
@NotNull Class<T> elementClass,
|
||||
@NotNull Function<LookupElement, LookupElement> elementHandler) {
|
||||
final Project project = targetFile.getProject();
|
||||
final GlobalSearchScope scope = PySearchUtilBase.defaultSuggestionScope(targetFile);
|
||||
final Set<String> alreadySuggested = new HashSet<>();
|
||||
private static class Counters {
|
||||
int scannedNames;
|
||||
int privateNames;
|
||||
int totalVariants;
|
||||
int notApplicableInContext;
|
||||
|
||||
StubIndex stubIndex = StubIndex.getInstance();
|
||||
final Collection<String> allKeys = stubIndex.getAllKeys(indexKey, project);
|
||||
for (String elementName : resultSet.getPrefixMatcher().sortMatching(allKeys)) {
|
||||
stubIndex.processElements(indexKey, elementName, project, scope, elementClass, (element) -> {
|
||||
ProgressManager.checkCanceled();
|
||||
if (!condition.value(element)) return true;
|
||||
String name = element.getName();
|
||||
if (name == null) return true;
|
||||
QualifiedName importPath = QualifiedNameFinder.findCanonicalImportPath(element, targetFile);
|
||||
if (importPath == null) return true;
|
||||
String qualifiedName = importPath + "." + name;
|
||||
if (alreadySuggested.add(qualifiedName)) {
|
||||
LookupElementBuilder lookupElement = LookupElementBuilder
|
||||
.createWithSmartPointer(name, element)
|
||||
.withIcon(element.getIcon(0))
|
||||
.withTailText(" (" + importPath + ")", true)
|
||||
.withInsertHandler(insertHandler);
|
||||
resultSet.addElement(elementHandler.apply(lookupElement));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Counters{" +
|
||||
"scannedNames=" + scannedNames +
|
||||
", privateNames=" + privateNames +
|
||||
", totalVariants=" + totalVariants +
|
||||
", notApplicableInContext=" + notApplicableInContext +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,9 @@ import com.jetbrains.python.psi.*
|
||||
import com.jetbrains.python.psi.resolve.QualifiedNameFinder
|
||||
|
||||
/**
|
||||
* Provides basic functionality for extended completion.
|
||||
*
|
||||
* Extended code completion is actually a basic code completion that shows the names of classes, functions, modules and variables.
|
||||
*
|
||||
* To provide variants for extended completion override [doFillCompletionVariants]
|
||||
* Provides basic functionality for providing completion variants that should add an import statement or be expanded into a qualified name.
|
||||
*/
|
||||
abstract class PyExtendedCompletionContributor : CompletionContributor(), DumbAware {
|
||||
abstract class PyImportableNameCompletionContributor : CompletionContributor(), DumbAware {
|
||||
|
||||
protected val importingInsertHandler: InsertHandler<LookupElement> = InsertHandler { context, item ->
|
||||
addImportForLookupElement(context, item, context.tailOffset - 1)
|
||||
@@ -70,10 +66,6 @@ abstract class PyExtendedCompletionContributor : CompletionContributor(), DumbAw
|
||||
protected abstract fun doFillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet)
|
||||
|
||||
private fun shouldDoCompletion(parameters: CompletionParameters, result: CompletionResultSet): Boolean {
|
||||
if (!parameters.isExtendedCompletion) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (result.prefixMatcher.prefix.isEmpty()) {
|
||||
result.restartCompletionOnPrefixChange(StandardPatterns.string().longerThan(0))
|
||||
return false
|
||||
@@ -3,6 +3,7 @@ package com.jetbrains.python.codeInsight.completion
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionParameters
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet
|
||||
import com.intellij.codeInsight.completion.PrioritizedLookupElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression
|
||||
@@ -20,9 +21,12 @@ import com.jetbrains.python.psi.stubs.PyModuleNameIndex
|
||||
* The completion contributor ensures that completion variants are resolvable with project source root configuration.
|
||||
* The list of completion variants does not include namespace packages (but includes their modules where appropriate).
|
||||
*/
|
||||
class PyModulePackageCompletionContributor : PyExtendedCompletionContributor() {
|
||||
class PyModulePackageCompletionContributor : PyImportableNameCompletionContributor() {
|
||||
|
||||
override fun doFillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
if (!parameters.isExtendedCompletion) {
|
||||
return
|
||||
}
|
||||
|
||||
val targetFile = parameters.originalFile
|
||||
val inStringLiteral = parameters.position.parent is PyStringLiteralExpression
|
||||
@@ -34,15 +38,13 @@ class PyModulePackageCompletionContributor : PyExtendedCompletionContributor() {
|
||||
.toList()
|
||||
|
||||
val resolveContext = fromFoothold(targetFile)
|
||||
val builders = modulesFromIndex.asSequence()
|
||||
modulesFromIndex.asSequence()
|
||||
.flatMap { resolve(it, resolveContext) }
|
||||
.filter { PyUtil.isImportable(targetFile, it) }
|
||||
.mapNotNull { createLookupElementBuilder(targetFile, it) }
|
||||
.map { it.withInsertHandler(
|
||||
if (inStringLiteral) stringLiteralInsertHandler else importingInsertHandler)
|
||||
}
|
||||
|
||||
builders.forEach { result.addElement(it) }
|
||||
.map { it.withInsertHandler(if (inStringLiteral) stringLiteralInsertHandler else importingInsertHandler) }
|
||||
.map { PrioritizedLookupElement.withPriority(it, PythonCompletionWeigher.NOT_IMPORTED_MODULE_WEIGHT.toDouble()) }
|
||||
.forEach { result.addElement(it) }
|
||||
}
|
||||
|
||||
private fun resolve(module: PsiFile, resolveContext: PyQualifiedNameResolveContext): Sequence<PsiFileSystemItem> {
|
||||
|
||||
@@ -35,6 +35,7 @@ public class PyCodeInsightSettings implements PersistentStateComponent<PyCodeIns
|
||||
public boolean INSERT_TYPE_DOCSTUB;
|
||||
|
||||
public boolean PARENTHESISE_ON_ENTER = true;
|
||||
public boolean INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION = true;
|
||||
|
||||
@Override
|
||||
public PyCodeInsightSettings getState() {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.jetbrains.python.codeInsight.completion
|
||||
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.UiDslUnnamedConfigurable
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.bindSelected
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.codeInsight.PyCodeInsightSettings
|
||||
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
class PythonCodeCompletionConfigurable: UiDslUnnamedConfigurable.Simple(), Configurable {
|
||||
|
||||
override fun getDisplayName(): String {
|
||||
return PyBundle.message("configurable.PythonCodeCompletionConfigurable.display.name.python")
|
||||
}
|
||||
|
||||
override fun Panel.createContent() {
|
||||
val settings = PyCodeInsightSettings.getInstance()
|
||||
|
||||
group(PyBundle.message("configurable.PythonCodeCompletionConfigurable.border.title")) {
|
||||
row {
|
||||
checkBox(PyBundle.message("configurable.PythonCodeCompletionConfigurable.checkbox.suggest.importable.names"))
|
||||
.bindSelected({ settings.INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION },
|
||||
{ settings.INCLUDE_IMPORTABLE_NAMES_IN_BASIC_COMPLETION = it })
|
||||
.gap(RightGap.SMALL)
|
||||
contextHelp(PyBundle.message("configurable.PythonCodeCompletionConfigurable.checkbox.suggest.importable.names.help"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from mod import MyClass
|
||||
|
||||
MyClass
|
||||
@@ -0,0 +1,3 @@
|
||||
from mod import MyClass
|
||||
|
||||
MyCla<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
class MyClass:
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
from mod import MyClass
|
||||
|
||||
def f(p):
|
||||
match p:
|
||||
case MyClass
|
||||
@@ -0,0 +1,5 @@
|
||||
from mod import MyClass
|
||||
|
||||
def f(p):
|
||||
match p:
|
||||
case MyCla<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
class MyClass:
|
||||
pass
|
||||
@@ -1,3 +1,3 @@
|
||||
path = "something"
|
||||
path1 = "something"
|
||||
|
||||
pat<caret>
|
||||
@@ -1,2 +1,2 @@
|
||||
def my_func(*args, **kwargs):
|
||||
def func(*args, **kwargs):
|
||||
pass
|
||||
|
||||
@@ -2,10 +2,10 @@ from typing import overload
|
||||
|
||||
|
||||
@overload
|
||||
def my_func(p: int):
|
||||
def func(p: int):
|
||||
pass
|
||||
|
||||
|
||||
@overload
|
||||
def my_func(p1: str, p2: int):
|
||||
def func(p1: str, p2: int):
|
||||
pass
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
def __foo__():
|
||||
return "private"
|
||||
@@ -1,2 +0,0 @@
|
||||
def _foo():
|
||||
return "private"
|
||||
@@ -1,2 +0,0 @@
|
||||
def foo():
|
||||
return "public"
|
||||
@@ -1 +0,0 @@
|
||||
fo<caret>o
|
||||
@@ -1 +0,0 @@
|
||||
foo = "private"
|
||||
@@ -1 +0,0 @@
|
||||
foo = "non-private"
|
||||
@@ -1 +0,0 @@
|
||||
fo<caret>o
|
||||
@@ -2,5 +2,5 @@ class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class Bar:
|
||||
class UniqueBar:
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from module import (
|
||||
Foo,
|
||||
Bar,
|
||||
UniqueBar,
|
||||
)
|
||||
|
||||
print(Foo(), Bar)
|
||||
print(Foo(), UniqueBar)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from module import Foo
|
||||
|
||||
print(Foo(), Ba<caret>)
|
||||
print(Foo(), UniqueBa<caret>)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
def func():
|
||||
def test_func():
|
||||
pass
|
||||
|
||||
@@ -1 +1 @@
|
||||
fun<caret>
|
||||
test_fun<caret>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
def func():
|
||||
def test_func():
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from mod import unique_class
|
||||
|
||||
|
||||
match "foo":
|
||||
case unique_classfd
|
||||
@@ -0,0 +1,2 @@
|
||||
match "foo":
|
||||
case unique_<caret>
|
||||
@@ -0,0 +1,7 @@
|
||||
def unique_func():
|
||||
pass
|
||||
|
||||
unique_var = 42
|
||||
|
||||
class unique_class:
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
from mod import unique_class
|
||||
|
||||
x: unique_class
|
||||
@@ -0,0 +1 @@
|
||||
x: unique_<caret>
|
||||
@@ -0,0 +1,7 @@
|
||||
def unique_func():
|
||||
pass
|
||||
|
||||
unique_var = 42
|
||||
|
||||
class unique_class:
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
from typing import Final
|
||||
|
||||
|
||||
class C:
|
||||
attr: Final(<caret>)
|
||||
@@ -0,0 +1,2 @@
|
||||
class C:
|
||||
attr: Fina<caret>
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
@_SpecialForm
|
||||
def Final(self, parameters):
|
||||
"""Special typing construct to indicate final names to type checkers.
|
||||
|
||||
A final name cannot be re-assigned or overridden in a subclass.
|
||||
|
||||
For example::
|
||||
|
||||
MAX_SIZE: Final = 9000
|
||||
MAX_SIZE += 1 # Error reported by type checker
|
||||
|
||||
class Connection:
|
||||
TIMEOUT: Final[int] = 10
|
||||
|
||||
class FastConnector(Connection):
|
||||
TIMEOUT = 1 # Error reported by type checker
|
||||
|
||||
There is no runtime checking of these properties.
|
||||
"""
|
||||
item = _type_check(parameters, f'{self} accepts only single type.')
|
||||
return _GenericAlias(self, (item,))
|
||||
@@ -0,0 +1,2 @@
|
||||
class C:
|
||||
unique_<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
class C:
|
||||
unique_<caret>
|
||||
@@ -0,0 +1 @@
|
||||
unique_var = 42
|
||||
@@ -0,0 +1,2 @@
|
||||
match "foo":
|
||||
case
|
||||
@@ -0,0 +1,2 @@
|
||||
match "foo":
|
||||
cas<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
def case_fold(s):
|
||||
...
|
||||
@@ -0,0 +1,5 @@
|
||||
from mod import unique_var
|
||||
|
||||
|
||||
class C:
|
||||
attr = unique_var
|
||||
@@ -0,0 +1,2 @@
|
||||
class C:
|
||||
attr = unique_<caret>
|
||||
@@ -0,0 +1 @@
|
||||
unique_var = 42
|
||||
@@ -0,0 +1,3 @@
|
||||
from typing import Tuple
|
||||
|
||||
attr: Tuple<caret>
|
||||
@@ -0,0 +1 @@
|
||||
attr: Tup<caret>
|
||||
@@ -0,0 +1,12 @@
|
||||
Tuple = _TupleType(tuple, -1, inst=False, name='Tuple')
|
||||
Tuple.__doc__ = \
|
||||
"""Deprecated alias to builtins.tuple.
|
||||
|
||||
Tuple[X, Y] is the cross-product type of X and Y.
|
||||
|
||||
Example: Tuple[T1, T2] is a tuple of two elements corresponding
|
||||
to type variables T1 and T2. Tuple[int, float, str] is a tuple
|
||||
of an int, a float and a string.
|
||||
|
||||
To specify a variable-length tuple of homogeneous type, use Tuple[T, ...].
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
unique_
|
||||
@@ -0,0 +1 @@
|
||||
unique_<caret>
|
||||
@@ -0,0 +1 @@
|
||||
unique_var = 42
|
||||
@@ -0,0 +1,3 @@
|
||||
from pkg import unique_var
|
||||
|
||||
unique_var
|
||||
@@ -0,0 +1 @@
|
||||
unique_<caret>
|
||||
@@ -0,0 +1 @@
|
||||
from ._mod import unique_var
|
||||
@@ -0,0 +1 @@
|
||||
unique_var = 42
|
||||
@@ -0,0 +1 @@
|
||||
mai<caret>
|
||||
@@ -0,0 +1 @@
|
||||
mai<caret>
|
||||
@@ -0,0 +1,2 @@
|
||||
def main():
|
||||
pass
|
||||
@@ -4,7 +4,6 @@ package com.jetbrains.python;
|
||||
import com.intellij.codeInsight.completion.CompletionType;
|
||||
import com.intellij.codeInsight.lookup.Lookup;
|
||||
import com.intellij.codeInsight.lookup.LookupElement;
|
||||
import com.intellij.openapi.project.DumbService;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.psi.PsiDirectory;
|
||||
import com.intellij.psi.PsiElement;
|
||||
@@ -123,11 +122,6 @@ public class PyClassNameCompletionTest extends PyTestCase {
|
||||
);
|
||||
}
|
||||
|
||||
// PY-20976
|
||||
public void testOrderingUnderscoreInPath() {
|
||||
doTestCompletionOrder("b.foo", "_a.foo");
|
||||
}
|
||||
|
||||
// PY-20976
|
||||
public void testOrderingSymbolBeforeModule() {
|
||||
doTestCompletionOrder("b.foo", "a.foo");
|
||||
@@ -153,21 +147,16 @@ public class PyClassNameCompletionTest extends PyTestCase {
|
||||
runWithAdditionalFileInLibDir(
|
||||
"sys.py",
|
||||
"path = 10",
|
||||
(__) -> doTestCompletionOrder("combinedOrdering.path", "first.foo.path", "sys.path", "_second.bar.path")
|
||||
(__) -> doTestCompletionOrder("combinedOrdering.path1", "first.foo.path", "sys.path")
|
||||
);
|
||||
}
|
||||
|
||||
// PY-20976
|
||||
public void testOrderingUnderscoreInName() {
|
||||
doTestCompletionOrder("c.foo", "b._foo", "a.__foo__");
|
||||
}
|
||||
|
||||
// PY-44586
|
||||
public void testNoDuplicatesForStubsAndOverloads() {
|
||||
doExtendedCompletion();
|
||||
List<String> allVariants = myFixture.getLookupElementStrings();
|
||||
assertNotNull(allVariants);
|
||||
assertEquals(1, Collections.frequency(allVariants, "my_func"));
|
||||
assertEquals(1, Collections.frequency(allVariants, "func"));
|
||||
}
|
||||
|
||||
// PY-45541
|
||||
@@ -176,12 +165,12 @@ public class PyClassNameCompletionTest extends PyTestCase {
|
||||
LookupElement reexportedFunc = ContainerUtil.find(lookupElements, variant -> variant.getLookupString().equals("my_func"));
|
||||
assertNotNull(reexportedFunc);
|
||||
TestLookupElementPresentation funcPresentation = TestLookupElementPresentation.renderReal(reexportedFunc);
|
||||
assertEquals(" (pkg)", funcPresentation.getTailText());
|
||||
assertEquals("pkg", funcPresentation.getTypeText());
|
||||
|
||||
LookupElement notExportedVar = ContainerUtil.find(lookupElements, variant -> variant.getLookupString().equals("my_var"));
|
||||
assertNotNull(notExportedVar);
|
||||
TestLookupElementPresentation varPresentation = TestLookupElementPresentation.renderReal(notExportedVar);
|
||||
assertEquals(" (pkg.mod)", varPresentation.getTailText());
|
||||
assertEquals("pkg.mod", varPresentation.getTypeText());
|
||||
}
|
||||
|
||||
// PY-45566
|
||||
@@ -208,7 +197,7 @@ public class PyClassNameCompletionTest extends PyTestCase {
|
||||
assertNotNull(variants);
|
||||
List<String> variantQNames = ContainerUtil.mapNotNull(variants, PyClassNameCompletionTest::getElementQualifiedName);
|
||||
assertDoesntContain(variantQNames, "mypkg.test.test_mod.test_func");
|
||||
assertContainsElements(variantQNames, "mod.func", "tests.test_func", "mypkg.mod.func");
|
||||
assertContainsElements(variantQNames, "mod.test_func", "tests.test_func", "mypkg.mod.test_func");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user