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:
Mikhail Golubev
2023-08-07 10:35:28 +03:00
committed by intellij-monorepo-bot
parent 7efc1bb1f9
commit 52850e21d8
63 changed files with 490 additions and 159 deletions

View File

@@ -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"

View File

@@ -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}

View File

@@ -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";

View File

@@ -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 +
'}';
}
}
}

View File

@@ -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

View File

@@ -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> {

View File

@@ -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() {

View File

@@ -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"))
}
}
}
}

View File

@@ -0,0 +1,3 @@
from mod import MyClass
MyClass

View File

@@ -0,0 +1,3 @@
from mod import MyClass
MyCla<caret>

View File

@@ -0,0 +1,2 @@
class MyClass:
pass

View File

@@ -0,0 +1,5 @@
from mod import MyClass
def f(p):
match p:
case MyClass

View File

@@ -0,0 +1,5 @@
from mod import MyClass
def f(p):
match p:
case MyCla<caret>

View File

@@ -0,0 +1,2 @@
class MyClass:
pass

View File

@@ -1,3 +1,3 @@
path = "something"
path1 = "something"
pat<caret>

View File

@@ -1,2 +1,2 @@
def my_func(*args, **kwargs):
def func(*args, **kwargs):
pass

View File

@@ -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

View File

@@ -1,2 +0,0 @@
def __foo__():
return "private"

View File

@@ -1,2 +0,0 @@
def _foo():
return "private"

View File

@@ -1,2 +0,0 @@
def foo():
return "public"

View File

@@ -2,5 +2,5 @@ class Foo:
pass
class Bar:
class UniqueBar:
pass

View File

@@ -1,6 +1,6 @@
from module import (
Foo,
Bar,
UniqueBar,
)
print(Foo(), Bar)
print(Foo(), UniqueBar)

View File

@@ -1,3 +1,3 @@
from module import Foo
print(Foo(), Ba<caret>)
print(Foo(), UniqueBa<caret>)

View File

@@ -1,2 +1,2 @@
def func():
def test_func():
pass

View File

@@ -0,0 +1,5 @@
from mod import unique_class
match "foo":
case unique_classfd

View File

@@ -0,0 +1,2 @@
match "foo":
case unique_<caret>

View File

@@ -0,0 +1,7 @@
def unique_func():
pass
unique_var = 42
class unique_class:
pass

View File

@@ -0,0 +1,3 @@
from mod import unique_class
x: unique_class

View File

@@ -0,0 +1,7 @@
def unique_func():
pass
unique_var = 42
class unique_class:
pass

View File

@@ -0,0 +1,5 @@
from typing import Final
class C:
attr: Final(<caret>)

View File

@@ -0,0 +1,2 @@
class C:
attr: Fina<caret>

View File

@@ -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,))

View File

@@ -0,0 +1,2 @@
class C:
unique_<caret>

View File

@@ -0,0 +1,2 @@
class C:
unique_<caret>

View File

@@ -0,0 +1,2 @@
match "foo":
cas<caret>

View File

@@ -0,0 +1,2 @@
def case_fold(s):
...

View File

@@ -0,0 +1,5 @@
from mod import unique_var
class C:
attr = unique_var

View File

@@ -0,0 +1,2 @@
class C:
attr = unique_<caret>

View File

@@ -0,0 +1,3 @@
from typing import Tuple
attr: Tuple<caret>

View File

@@ -0,0 +1 @@
attr: Tup<caret>

View File

@@ -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, ...].
"""

View File

@@ -0,0 +1 @@
unique_<caret>

View File

@@ -0,0 +1,3 @@
from pkg import unique_var
unique_var

View File

@@ -0,0 +1 @@
unique_<caret>

View File

@@ -0,0 +1 @@
from ._mod import unique_var

View File

@@ -0,0 +1 @@
unique_var = 42

View File

@@ -0,0 +1 @@
mai<caret>

View File

@@ -0,0 +1 @@
mai<caret>

View File

@@ -0,0 +1,2 @@
def main():
pass

View File

@@ -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");
});
}